時間経過とともに色合いが変わる地図の作成

この記事は Mapbox Advent Calendar 2020 の23日目の記事です。

Mapbox GL JS では地図がベクトルタイルとして配信されるがゆえに、クライアント側で自由に描画をカスタマイズできるという利点があります。例えば、マップをモノクロ白地図風にしたり、ダークな背景に特定の道路だけ明るくして目立たせる、みたいなことが簡単にできます。

東京の公共交通のリアルタイム3Dマップ Mini Tokyo 3D では、これを活用して昼間なら明るい街、夜なら暗い街、そして夕方には夕焼け風、といった具合に時間の経過とともに色合いが変わる様子を表現しています。本記事では、これをどのように実現しているかを紹介します。

f:id:nagixx:20201223182309j:plain

まずはマップのスタイルから、色調整の対象となるレイヤーとその Paint プロパティを選別し、初期設定値をオブジェクトの配列に保存しておきます。これは後で、色を調整するための基準カラーとして使用します。

// 色調整の対象となる Paint プロパティキー
const paintPropertyKeys = {
    'background': ['background-color'],
    'line': ['line-color'],
    'fill': ['fill-color', 'fill-outline-color'],
    'fill-extrusion': ['fill-extrusion-color']
};

// 色調整の対象となるレイヤータイプ
const layerTypes = Object.keys(paintPropertyKeys);

// 初期カラー情報を格納する配列
const styleColors = [];

map.getStyle().layers.filter(
    layer => layerTypes.includes(layer.type)
).forEach(({id, type}) => {
    for (const key of paintPropertyKeys[type]) {
        let prop = map.getPaintProperty(id, key);

        if (typeof prop === 'string') {
            const [r, g, b, a] = parseCSSColor(prop);
            styleColors.push({id, key, r, g, b, a});
        }
    }
});

paintPropertyKeys には、レイヤータイプをキーとして、値には各レイヤーの対象となる Paint プロパティキーの配列を格納しており、すぐ下のループの中で色調整の対象となるレイヤーとプロパティの選択に利用しています。なお、Paint プロパティのカラー値には CSS カラーの文字列が入っていますので、それをパースするために css-color-parser-js を使っています。

取得したカラー情報は、レイヤー ID、プロパティキーとともに styleColors に入ります。この部分は初回に一回だけ実行していれば大丈夫です。

次に、「いつが昼でいつが夜か」という基準が必要ですが、これにはその場所の日の出・日の入時刻を知る必要があります。日の出・日の入時間は季節や緯度・経度によって変わるので、実際に求めるのは結構複雑です。そこで、ここでは SunCalc という JavaScript ライブラリを使っています。

// 日照に関する時刻を格納するオブジェクト
const times = SunCalc.getTimes(new Date(), 35.6814, 139.7670);

// 日の出時刻を表す UNIX epoch
const sunriseTime = times.sunrise.getTime();

// 日の入時刻を表す UNIX epoch
const sunsetTime = times.sunset.getTime();

上記のコードの通り、SunCalc.getTimes() の引数に現在時刻を表す Date オブジェクト、緯度、経度(例では東京)を渡すと、日照に関する時刻を格納するオブジェクトが得られます。その中の sunrise および sunset プロパティが日の出・日の入時刻を表す Date オブジェクトで、sunriseTime および sunsetTime には UNIX epoch (1970年1月1日からの経過ミリ秒) が入ります。

そして、「夜」「日の出」「昼」「日の入」における基準の色合いを決めておきます。下の表は、それぞれの状態における RGB 要素に乗じる係数と、この係数を白に乗じた場合の色見本を示しています。

状態 R G B 色見本
0.4 0.4 0.5  
日の出 0.8 0.9 1.0  
1.0 1.0 1.0  
日の入 1.0 0.9 0.8  

さらに、日の出前後の1時間ずつ、日の入前後の1時間ずつを色合いの推移の時間として、夜→日の出→昼、昼→日の入→夜の推移を滑らかにすることを考えます。

// 現在時刻を表す UNIX epoch
const now = Date.now();

// 時間推移の係数(0〜1)
let t;

// カラー係数
let cr, cg, cb;

if (now >= sunriseTime - 3600000 && now < sunriseTime) {
    // 夜〜日の出
    t = (now - sunriseTime) / 3600000 + 1;
    cr = .4 * (1 - t) + .8 * t;
    cg = .4 * (1 - t) + .9 * t;
    cb = .5 * (1 - t) + 1 * t;
} else if (now >= sunriseTime && now < sunriseTime + 3600000) {
    // 日の出〜昼
    t = (now - sunrise) / 3600000;
    cr = .8 * (1 - t) + 1 * t;
    cg = .9 * (1 - t) + 1 * t;
    cb = 1;
} else if (now >= sunriseTime + 3600000 && now < sunsetTime - 3600000) {
    // 昼
    cr = cg = cb = 1;
} else if (now >= sunsetTime - 3600000 && now < sunsetTime) {
    // 昼〜日の入
    t = (now - sunsetTime) / 3600000 + 1;
    cr = 1;
    cg = 1 * (1 - t) + .9 * t;
    cb = 1 * (1 - t) + .8 * t;
} else if (now >= sunsetTime && now < sunsetTime + 3600000) {
    // 日の入〜夜
    t = (now - sunsetTime) / 3600000;
    cr = 1 * (1 - t) + .4 * t;
    cg = .9 * (1 - t) + .4 * t;
    cb = .8 * (1 - t) + .5 * t;
} else {
    // 夜
    cr = cg = .4;
    cb = .5;
}

上のコードでは、先ほど求めた日の出・日の入時刻と現在時刻を比較して、状態により条件分岐しています。夜または昼であればカラー係数は固定ですが、日の出前後の1時間、日の入前後の1時間の期間は t を 0〜1 の間で変化する係数として、カラー係数に乗じることで滑らかにカラー係数が変化するようにしています。

for (const {id, key, r, g, b, a} of styleColors) {
    const prop = `rgba(${[r * cr, g * cg, b * cb, a].join(',')})`;
    map.setPaintProperty(id, key, prop);
});

最後に、冒頭で保存した各レイヤーの各プロパティの基準カラー情報からレイヤー ID、プロパティキー、カラー要素を取得し、RGB 要素にカラー係数を乗じて Paint プロパティに設定しています。

以上の処理を例えば1分毎に呼び出すことで、時間に応じて色合いが変わる地図が実現できます。

追記

・・・と、ここまで書いて気づいたのですが、Paint プロパティのカラー値には CSS カラーを表す文字列だけではなく、動的にスタイルを変えるための Expressions 用の配列やオブジェクトが入っている場合もあるため、実際にはもっと複雑なことをしていたのでした。

ご興味ある方は、初期カラー取得コードカラー更新コードあたりを見てください。