Mini Tokyo 3D バージョン 3.6.0 リリース

Mini Tokyo 3D バージョン 3.6.0 がリリースされました (GitHub)。3.5.0 からの追加・修正機能を見ていきましょう。

実験的に GTFS/GTFS-RT をサポート

実験的機能として、Mini Tokyo 3D がついに GTFS/GTFS-RT 形式の交通機関に対応しました。Mini Tokyo 3D にアクセスする際の URL の末尾に、「?」に続けてキーと値のペア(クエリパラメータ)を記述することで、特定の GTFS データセットおよび GTFS Realtime VehiclePosition フィードを指定します(詳細な説明)。

https://minitokyo3d.com/?gtfsurl=<GTFS zipファイル>&gtfsvpurl=<GTFS-RT VehiclePosition>&gtfscolor=<色コード>

今のところ、shapes.txt を含んでおり GTFS-RT VehiclePosition が提供されている交通機関のリアルタイム表示のみの対応です。例えば都営バス (東京) の場合は次の通りです。

https://minitokyo3d.com/?gtfsurl=https%3A%2F%2Fapi-public.odpt.org%2Fapi%2Fv4%2Ffiles%2FToei%2Fdata%2FToeiBus-GTFS.zip>fsvpurl=https%3A%2F%2Fapi-public.odpt.org%2Fapi%2Fv4%2Fgtfs%2Frealtime%2FToeiBus>fscolor=9FC105

使用メモリ量の削減

内部のデータクラスの整備とデータ構造の最適化により、JavaScript の使用ヒープサイズを約26%削減しました。

PLATEAU プラグインが全国に対応

PLATEAU プラグインをアップデートしました。2023年4月の PLATEAU View 2.0 向けに更新された配信データに対応しています。位置に合わせて動的にデータをロード/アンロードすることでテクスチャ付き LOD2 に対応した全国213都市の表示に対応。降雨アニメーション表示時の不具合も修正しました。

ドイツ語サポート

コミュニティーのコントリビューションにより、ドイツ語に対応しました。

JR 両毛線水戸線東海道線熱海〜沼津間追加

JR 両毛線水戸線が全線開通しました。また、東海道線の熱海〜沼津間が開通。熱海〜函南間は総延長7,804メートルの丹那トンネルを通過します。熱海での踊り子号の分割併合や、サンライズ瀬戸・出雲の走行が見られます。さらに、JR 東日本管内では現在最も長い距離(前橋〜沼津241km)を走る普通列車 1943E を全区間追跡できるようになりました。

ダイヤ改正対応

都営浅草線京急、京成、北総鉄道芝山鉄道新京成の 2024/11/23 ダイヤ改正に対応しました。

ズームレベルによるライブカメラアイコンの表示切り替え

ライブカメラが増えてきたため、小縮尺時(ズームレベルを下げた時)にライブカメラアイコンを非表示にするようにしました。また、この改良に伴い Marker クラスの次のコンストラクタオプションで最小ズームレベルがサポートされました。

プロパティ 説明
minZoom マーカーの最小のズームレベル

詳細は API リファレンスをご覧ください。

ライブカメラの拡充

ライブカメラプラグインで表示される、沿線のライブカメラがさらに追加されました。

CSP で制限されたページで Mapbox GL JS を使うには

この記事は Mapbox Advent Calendar 2024 の5日目の記事です。

コンテンツセキュリティポリシー (CSP)

コンテンツセキュリティポリシーとは、特定の種類の攻撃による影響を軽減するために追加できるセキュリティのしくみです。クロスサイトスクリプティング (XSS) をはじめとする様々なウェブセキュリティ脆弱性への対策として有効です。レスポンスヘッダーや <meta> 要素にポリシーを指定することで有効になります。

Mapbox 公式ドキュメントでも、Mapbox GL JS を使う際に指定すべき CSP ディレクティブが明示されています

ディレクティブ ソース 説明
worker-src blob: WorkerSharedWorkerServiceWorker スクリプトのソースを blob: に限定
child-src blob: ウェブワーカーと <frame><iframe> などの要素を使用して読み込んだコンテンツのソースを blob: に限定
img-src data: blob: 画像やファビコンのソースを data:blob: に限定
connect-src https://*.tiles.mapbox.com https://api.mapbox.com https://events.mapbox.com fetch()XMLHttpRequest などのスクリプトインターフェイスを使用して読み込むことができる URL を限定

厳格な CSP 環境では

各ディレクティブに少なくとも上記のソースが指定されていれば Mapbox GL JS は問題なく動作するというものですが、このうち worker-srcchild-src で必要な blob: の指定は、環境によっては許可されていない場合があります。

Content Security Policy Level 2 仕様の 4.2.2.1 の項に記載がありますが、blob: のような URL が指すコンテンツは、レスポンスボディから派生したり、Document コンテキストで実行されたりするため、安全でない可能性があるためです。blob: を許可することは、'unsafe-eval' と等価とされています。eval() がなんとなく危険、というのは聞いたことがありますよね?

Mapbox GL JS は通常、1つの JavaScript ファイル内でウェブワーカーを生成し、Blob を使って読み込むため、このような環境ではそのままでは動作しません(ウェブワーカーは UI 操作を邪魔することなくバックグラウンドでタイルのローディングなどを行うために使われる)。

そこで、mapbox-gl.js の代わりにウェブワーカーコードを除いた mapbox-gl-csp.js ファイルを使用し、ワーカーファイル mapbox-gl-csp-worker.js へのパスを別途手動で設定する必要があります。ワーカーファイルは同一オリジンポリシーに従わなければならないことに注意してください。つまり、mapbox-gl-csp-worker.js はそれを読み込むページと同じオリジンから提供されなければなりません。

<script src="https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl-csp.js"></script>
<script>
mapboxgl.workerUrl = 'https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl-csp-worker.js';
/* ... */
</script>

さらに特殊な環境では

さて、ここまでは公式ドキュメントにも説明がありますし、ほとんどの環境ではこれ以上の対応は必要ないかと思います。しかし、ここからが本番です(笑)。

私の目の前にあるのは、 HTML/CSSJavaScript を与えてやれば <iframe> の中で動的なページを表示できる、いわゆるサンドボックス環境です。ここでは、埋め込みページの <meta> 要素で、child-src 'none'(そして worker-src は無し、つまり child-src にフォールバック)という CSP が適用されており、これは変更することはできません。その上、JavaScript 実行時に渡ってくる window オブジェクトからは、律儀なことに Worker クラスが消されています。意地でもウェブワーカーを使わせないぞという強い意志を感じます。

それでも Mapbox GL JS を使って地図を表示したいので、色々こねくり回して作り上げたコードがこちらです。これを動かすには、https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl-csp-worker.js の中身と、Mapbox アクセストークンをそれぞれ指定の場所に、コピペしておく必要があります。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src http: https: 'unsafe-inline'; style-src http: https: 'unsafe-inline'; img-src http: https: data:; font-src http: https:; connect-src http: https:; media-src http: https:; object-src 'none'; child-src 'none'; frame-src 'none'">
<title>Mapbox GL JS map</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl-csp.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.workerClass = (function(window) {
  const WW_CONTEXT_WHITELIST = [
    'createImageBitmap', 'fetch', 'ImageBitmap', 'ImageData',
    'OffscreenCanvas', 'Request', 'AbortController'
  ];

  return function() {
    const me = this;
    let worker_context;

    // Allow main thread to specify event listeners
    const ui_listeners = {};
    this.addEventListener = (event_name, fn) => {
      // listen for events from worker thread
      if (!ui_listeners[event_name])
        ui_listeners[event_name] = [];
      ui_listeners[event_name].push(fn);
    };

    // onmessage handler
    this.addEventListener('message', e => {
      if (typeof me.onmessage !== 'undefined') {
        me.onmessage(e);
      }
    });

    function WorkerGlobalScope() {}

    /**** Worker context accessible to worker *****/
    function WorkerContext() {
      const worker_listeners = {};
      this.addEventListener = (event_name, fn) => {
        // listen for events from UI thread
        if (!worker_listeners[event_name])
          worker_listeners[event_name] = [];
        worker_listeners[event_name].push(fn);
      };

      // onmessage handler
      this.addEventListener('message', e => {
        if (typeof worker_context.onmessage !== 'undefined') {
          try {
            worker_context.onmessage(e);
          } catch (error) {
            triggerEvent(ui_listeners, 'error', error, true);
          }
        }
      });

      this.postMessage = msg => {
        triggerEvent(ui_listeners, 'message', msg);
      };

      this.__processPostMessage = msg => {
        triggerEvent(worker_listeners, 'message', msg);
      };

      this.close = () => {};

      // window context
      for (const p of WW_CONTEXT_WHITELIST) {
        if (typeof window[p] === 'function' && !window[p].prototype) {
          this[p] = window[p].bind(window);
        } else {
          this[p] = window[p];
        }
      }
      this.WorkerGlobalScope = WorkerGlobalScope;
    }
    WorkerContext.prototype = new WorkerGlobalScope();
    worker_context = new WorkerContext();
    this.postMessage = msg => {
      worker_context.__processPostMessage(msg);
    };

    this.terminate = () => {};

    function triggerEvent(listeners_map, event_name, event_data, no_wrapping) {
      const event_obj = no_wrapping ? event_data : {data: event_data};

      if (!listeners_map[event_name]) return;
      for (const listener of listeners_map[event_name]) {
        listener(event_obj);
      }
    }

    const mask = {};

    // worker context
    for (const p in worker_context) {
      mask[p] = worker_context[p];
    }
    // set self context
    mask['self'] = worker_context;

    with(mask) {
      /* ここに https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl-csp-worker.js をコピペ */
    }
  }
})(window);

mapboxgl.accessToken = 'ここに Mapbox アクセストークンを入れる';

const map = new mapboxgl.Map({
  container: 'map',
  center: [139.7670, 35.6814],
  zoom: 16,
  pitch: 60
});
</script>
</body>
</html>

これ、一体何をしているかというと、Worker クラスが存在しないので、mapbox-gl-csp-worker.js を内蔵する擬似 Worker クラスを作って、メインスレッドで動かしてしまうという力技です。ウェブワーカー未対応のブラウザで Worker コードを動かすためのこのライブラリを参考にしました。

ウェブワーカーと違ってマルチスレッドで動かないのでややぎこちないですが、動作としてはほぼ問題ないのではないでしょうか。惜しいのは、Mapbox GL JS v3 で導入された 3D ランドマークに使われている batched-model が表示されないところですね。これはローディングに WebAssembly コードを使っているため、script-src 'unsafe-eval' が必要になってしまいます(これ、ウェブワーカーとして実行する場合には制限にかからないのですが、どういうしくみでしょうね)。

このテクニックを使用した、続きの記事を書きたいと思いますので、お楽しみに!

Mini Tokyo 3D × PLATEAU 2024年版

この記事は 3D都市モデル Project PLATEAU Advent Calendar 2024 の3日目の記事です。

2年前に Mini Tokyo 3D × PLATEAU という記事を書きました。その後、2023年4月に PLATEAU VIEW 2.0 のリリースとそれに合わせた配信データの更新があり、おそらくそのタイミングで 3D Tiles の仕様変更があったため、上記の記事の内容は新しいデータでは動かなくなっていました。さらに、2024年5月には PLATEAU VIEW 3.0 のリリースと配信データ更新、データカタログ API の公開もあり、「ここらで Mini Tokyo 3D の PLATEAU プラグインも最新のデータに合わせて刷新するか」と思い立ち色々試してみたのが本記事の内容です。2024年11月6日の PLATEAU LT 07 でのトークの詳細でもあります。

ちなみに前回の記事をご覧いただいてない方向けに簡単に解説すると、Mini Tokyo 3D は東京の公共交通のリアルタイム 3D マップで、Mapbox を地図ライブラリとして利用しているウェブアプリケーションです。PLATEAU plugin for Mini Tokyo 3D は Mini Tokyo 3D 上で動作するプラグインで、PLATEAU配信サービス(試験運用) チュートリアルで配信されているデータを利用して、テクスチャ付き建築物のリアルな 3D 風景の中を列車や旅客機が移動する様子を楽しめます。

データ仕様変更への対応

まず、以前は README に列挙されていた 3D Tiles の URL ですが、現在はデータカタログ API を使って取得する形になっています。https://api.plateauview.mlit.go.jp/datacatalog/plateau-datasets をコールすると 3D Tiles/MVT のデータ一覧が JSON のレスポンスとして返ってくるので、その中の url フィールドを使います。

次に、更新されたデータでは建築物の表示ができなくなっていましたので、それに対応する必要があります。Tile3DLayer の onTileLoad に引数として渡ってくるデータと onTileLoad での処理内容を比べると、前回は content.batchTableJson._gml_id の長さから項目数を取得していましたが、_gml_idgml_id に名称変更されていたためにエラーになっていることがわかりました。gml_id の長さを取得するように変更してもいいのですが、content.featureTableJson.BATCH_LENGTH を使う方がより良いので、そのように変更します。また、Tileset によっては content.batchTableJson._zmin が存在しないこともあるので、そのチェックを入れています。

onTileLoad: ({content}) => {
    const zmin = content.batchTableJson._zmin;
    const cartographicOrigin = content.cartographicOrigin;

    if (zmin) {
        const buffer = content.batchTableBinary.buffer;
        const len = content.featureTableJson.BATCH_LENGTH;
        const zMinView = new DataView(buffer, zmin.byteOffset, len * 8);
        const zMins = [];

        for (let i = 0; i < len; i++) {
            zMins.push(zMinView.getFloat64(i * 8, true));
        }
        zMins.sort((a, b) => a - b);
        cartographicOrigin.z -= zMins[Math.floor(len / 2)];
    }
    cartographicOrigin.z -= 36.6641;
}

とりあえずこれでエラーは解消し、選択した 3D Tiles のモデルを Tile3DLayer で表示できるようになりました。が、相変わらずブラウザで表示させるには重たいです。

動的なデータのロード/アンロード

前回はロードしておく 3D Tiles タイルセットは都内6つの区に限定していて、それでもメモリは不足気味でした。そこで、見えている部分の必要なモデルだけ動的にロードしてメモリを使うようにすればよいだろうという考えのもと、次の方針を立てました。

  • 事前に 3D Tiles の URL と領域を表す GeoJSON ポリゴンを用意
  • 地図が移動されたら、ポリゴンを使ってビューポート内の領域を特定
  • 新たに表示領域になった 3D Tiles をロードし、非表示になった領域の 3D Tiles をアンロード

もう表示対象は全国の都市の 3D Tiles にしてしまえということで、上述のデータカタログ API で取得してきた JSON から、データタイプが建築物(type_en'bldg')かつ LOD が 2(lod2)かつテクスチャ付き(texture が存在)のデータセットを抜き出します。そして市区町村コード(ward_code または city_code)と URL(url)を取得します。2024年12月現在、これは213市区町村の分があります。

次に、国土数値情報ダウンロードサイトの行政区域データのページから、2023年版全国データ N03-20230101_GML.zip をダウンロードします。最新の2024年版ではダメです。というのも、PLATEAU の 3D Tiles の最新データは2023年度に整備されており、その後浜松市で行政区の再編があって市区町村コードが一致しなくなるからです。

N03-20230101_GML.zip の中には N03-23_230101.geojson というGeoJSON ポリゴンデータが入っており、各 Feature のプロパティに含まれる行政区域コード(N03_007)を参照すれば、3D Tiles とポリゴンデータを対応づけることができます。

さて、このポリゴンデータを使えば、見えている地図の範囲に対象の行政区域が含まれるかを判定できそうですが、例えば地図の中心の座標を判定に使ったとすると、ポリゴンの領域が少しでも中心からずれれば範囲外と判定されてしまいます。つまり、地図上の半分くらいモデルが表示されない状態ができてしまう可能性があります。これに対処するために、ポリゴン領域を少し拡大して、隣り合うポリゴンをある程度オーバーラップさせることで、地図の中心から多少離れても領域内と判定させることができます。

このポリゴン領域の拡張には Python のライブラリ Shapelybuffer 関数を使います。また、正確な領域の判定は不要なので、simplify 関数を使って頂点数を削減します。最後に拡張したポリゴンに、市区町村コードと 3D Tiles の URL をプロパティとして付けて、GeoJSON 形式で出力します。ここまでの一連の処理を行う Python スクリプトは次の通りです。

import json
import requests
from shapely import from_geojson, to_geojson, from_wkt, to_wkt, MultiPolygon, get_coordinates

datasets = requests.get('https://api.plateauview.mlit.go.jp/datacatalog/plateau-datasets').json()['datasets']
models = [{
  'name': dataset['name'],
  'code': dataset['ward_code'] or dataset['city_code'],
  'url': dataset['url']
} for dataset in datasets if dataset['type_en'] == 'bldg' and dataset['lod'] == '2' and dataset['texture']]
print('Processing {} models...'.format(len(models)))

with open('N03-23_230101.geojson') as res:
  data = json.load(res)
with open('plateau-models.geojson', 'w') as f:
  f.write('{"type":"FeatureCollection","features":[\n')
  for model in models:
    geometries = []
    for feature in data['features']:
      if feature['properties']['N03_007'] == model['code']:
        geometries.append(from_geojson(json.dumps(feature)))

    if len(geometries) == 0:
      print('{} ({}) is empty'.format(model['name'], model['code']))
      continue

    geometry = MultiPolygon(geometries).buffer(0.01).simplify(0.001)

    f.write('{')
    f.write('"type":"Feature",')
    f.write('"geometry":{},'.format(to_geojson(from_wkt(to_wkt(geometry, rounding_precision=7)))))
    f.write('"properties":{')
    f.write('"code":"{}",'.format(model['code']))
    f.write('"url":"{}"'.format(model['url']))
    f.write('}')
    f.write('}')
    if model != models[-1]:
      f.write(',')
    f.write('\n')
  f.write(']}\n')

生成された全国213市区町村の領域データはこんな感じです。オーバーラップしていますよね。

次はフロントエンド側です。Mapbox の Map を作成したら、事前に先ほどの GeoJson ファイルをデータソースとして、透明(fill-opacity0)な fill レイヤーを追加しておきます。

map.addLayer({
  id: 'plateau-model',
  type: 'fill',
  source: {
    type: 'geojson',
    data: MODEL_URL
  },
  paint: {
    'fill-opacity': 0
  }
});

透明なので地図上には何も表示されませんが、queryRenderedFeatures を使って画面内の任意の座標に存在する Feature を取得することが可能です。このメソッドは CPU の幾何計算ではなく、GPU ピッキングを行う手法のため、パフォーマンス上も有利です。次のようにすれば、move イベントが発生するたびに画面中央に存在する(複数の)Feature を取得して、市区町村コードと 3D Tiles URL を使った処理を進めることができます。

map.on('move', () => {
  const {width, height} = map.getContainer().getBoundingClientRect();
  const features = map.queryRenderedFeatures([width / 2, height / 2], {layers: ['plateau-model']});
  
  for (const feature of features) {
    const {code, url} = feature.properties;

    /* ... */
  }
});

あとは新たに領域が検出されたら新しい Tile3DLayer を追加し、画面外に外れた領域の Tile3DLayer を削除するようにするだけで、動的なデータのロード/アンロードは実装完了です。

テクスチャの軽量化

これで全国の都市モデルが表示できるようになったものの、場所によってはまだ非常に動作が重たいですし、フリーズしてしまうこともありました(私の MacBook Pro のメモリ 16GB なので・・)。

ブラウザの開発者ツールのプロファイルを見ても、大して JavaScript ヒープは消費しておらず、一方でシステムのメモリはいっぱいいっぱい。macOS のユニバーサルメモリのことを考えると、おそらく GPU がテクスチャデータ用に確保しているメモリだろうと想像がつきます。実際、テクスチャなしであれば PLATEAU のモデルは軽快に動作します。

そこで、テクスチャデータを圧縮できる場所はないだろうかと探していたところ、テクスチャ画像は 3D Tiles に含まれる GLTF データの一部として存在しており、Tile3DLayer の引数として渡ってくる Tile オブジェクトからアクセスできること、そしてロード直後は画像データの差し替えが可能なことがわかりました。

Mini Tokyo 3D の利用ケースであれば、そこまで精細なテクスチャ画像は不要なため、画像の高さ、幅ともに 1/4 にして、データサイズとしては約 1/16 になることを目論みました。

これに加えて featureTableBinaryfeatureTableJsonbatchTableBinarybatchTableJson の各データもロード後は不要なので、参照を削除しておきます。

onTileLoad: ({content}) => {
  /* ... */
  content.featureTableBinary = null;
  content.featureTableJson = null;
  content.batchTableBinary = null;
  content.batchTableJson = null;

  for (const item of content.gltf.images || []) {
    const image = item.image;
    const resizeWidth = image.width / 4;
    const resizeHeight = image.height / 4;

    createImageBitmap(image, {resizeWidth, resizeHeight}).then(resizedImage => {
      item.image = resizedImage;
    });
  }
}

この最後のテクスチャの軽量化がかなり有効だったようです。全国どこに移動しても、スムーズに建築物モデルが表示できるようになりました!

ぜひお試しを

そこそこ軽くなった PLATEAU プラグインをお楽しみいただくには、https://minitokyo3d.com にアクセスし、画面右側の下から3つ目の「レイヤー設定」ボタンを押して、「PLATEAU」レイヤーを選択して有効にしてください(デフォルトでは表示オフになっています)。

Mini Tokyo 3D、PLATEAU Plugin for Mini Tokyo 3D は GitHub で公開されているので、チェックしてね!

Mini Tokyo 3D バージョン 3.5.0 リリース

Mini Tokyo 3D バージョン 3.5.0 がリリースされました (GitHub)。3.4.0 からの追加・修正機能を見ていきましょう。

新しい駅パネルの導入と列車発車標の追加

駅情報パネルをリニューアルしました。従来の駅出口情報に加えて、各路線の発車標(先発・次発の列車を表示)、経路検索を統合。すべての駅がクリックして選択できるようになりました。さらに、画面右上の経路検索ボタンはバージョン 2.4 以前の駅名検索ボタンに戻し、駅にすぐに飛べるようにしています。

スペイン語サポート

コミュニティーのコントリビューションにより、スペイン語に対応しました。

東海道貨物線中央本線高尾〜大月間が開通

東海道貨物線経由の東京着および新宿発着の特急湘南の表示に対応しました。東戸塚駅を過ぎたところで羽沢線の長いトンネルに突入し、羽沢横浜国大駅を横目に既存の相鉄直通線に合流します。マニアも納得のルートを再現しています。

また、中央本線の高尾〜大月間が開通。小仏峠の下をくぐる高尾〜相模湖間は、首都圏でも屈指の長さの駅間距離9.5kmです。

地下駅出口情報の更新

地下駅出口のバリアフリー施設情報、利用可能時間の情報の追加を着々と進めています。今回は、埼玉高速鉄道りんかい線つくばエクスプレス東葉高速鉄道京浜急行東京モノレール、JR 京葉線の各路線の駅に対応しました。また、東京メトロ東西線南砂町駅の改良工事のために使用休止になった出口と新設された出口の情報も追加しています。

さらに、北総線の矢切付近を地下化、矢切駅に出口を追加しました。京王線の国領〜布田〜調布付近と京王八王子付近も地下化して、各駅の出口を追加しています。

ダイヤ改正対応

JR、りんかい線東京メトロ都営地下鉄東武、西武、京王、小田急、東急、相鉄、横浜高速鉄道埼玉高速鉄道東葉高速鉄道つくばエクスプレス関東鉄道、流鉄、小湊鉄道いすみ鉄道金沢シーサイドライン東京モノレール、千葉モノレールの2024年3月18日のダイヤ改正に対応。新京成線の2024年3月23日のダイヤ改正に対応。また、JR 京葉線武蔵野線内房線外房線の2024年9月1日のダイヤ改正に対応しています。

2023年7月15日に登場した東武特急スペーシア X(東武 N100 系)は3月のダイヤ改正で1日6往復に増発。遅ればせながら Mini Tokyo 3D にも投入されました。

駅選択操作の拡張に伴うメソッドおよびイベントでの駅 ID サポート

バージョン 3.5.0 では、駅の選択操作の拡張に伴い、Map クラスの次のコンストラクタオプションで駅 ID がサポートされました。

プロパティ 説明
selection 駅 ID を指定すると駅を選択する

また、Map クラスの次のメソッドで駅 ID がサポートされました。

メソッド 説明
getSelection 選択された駅の ID の配列を返す
setSelection 駅 ID を指定すると駅を選択する

さらに、Mapクラスの次のイベントで駅 ID がサポートされました。

イベント 説明
deselection 駅の選択が解除された場合に発生
selection 駅が選択された場合に発生

詳細は API リファレンスをご覧ください。

ライブカメラの拡充

ライブカメラプラグインで表示される、沿線のライブカメラがさらに追加されました。

花火大会情報の表示

花火プラグインがアップデートされました。データベースから動的にデータを取得し、当日の花火大会の情報が画面左側に表示されます。ボタンを押すと開催場所にジャンプします。2024年の夏からは首都圏の中規模以上の花火大会をすべて登録。ライブカメラとシンクロして、デジタルツイン上で花火大会を楽しむことができます。

Mini Tokyo 3D バージョン 3.4.0 リリース

Mini Tokyo 3D バージョン 3.4.0 がリリースされました (GitHub)。3.3.0 からの追加・修正機能を見ていきましょう。

実際の太陽の位置に合わせた光源の設定

建築物の光の反射はこれまで固定ライトを使っていましたが、太陽の位置に合わせた光の反射に変更しました。時間と共に、建築物の見え方が変化してリアリティが少し向上しています。太陽の天球上の位置は正確に計算しているので、季節によっても影のつき方が変わります!

遮蔽による光の減衰を考慮した建物の影の描写

建築物のシェーディング(陰影の付与)にアンビエントオクルージョン(遮蔽による光の減衰を考慮して影を描写する手法)を適用しました。左が適用前、右が適用後です。効果は大きく、かなり風景のリアリティが増しています。

路線のライン形状の改善

地下鉄路線の形状データを更新しました。線形がだいぶ改善され、無理のない形状になっています。これは OpenStreetMap のデータ品質が3年前から大きく向上した成果です。

地下駅出口情報の更新

地下駅出口のバリアフリー施設情報、利用可能時間の情報の追加を着々と進めています。今回は、東京メトロ有楽町線半蔵門線南北線副都心線と、都営浅草線三田線新宿線大江戸線の各路線の駅に対応しました。また、西新宿駅虎ノ門ヒルズ駅、神谷町駅などの開発が進んでいるエリアに新設された出口も追加しています。

ダイヤ改正対応

りんかい線の 2023/3/18 ダイヤ改正東京メトロ銀座線の 2023/4/29 ダイヤ改正に対応。また、流鉄流山線の 2023/7/1 ダイヤ改正に対応。さらに、都営浅草線京急、京成、北総鉄道芝山鉄道の 2023/11/25 ダイヤ改正に対応しています。

ライブカメラの拡充

ライブカメラプラグインで表示される、沿線のライブカメラがさらに追加されました。

WebGL 2.0 対応

デフォルトで WebGL 2.0 を使った描画を行うようになりました。基本的には描画品質や性能は変わらないと思いますが、開発者がレイヤーの追加を行う場合には WebGL 2.0 ベースの gl コンテキストやレンダラー、GLSL ES 3.0 などを利用することができるようになります。

Bluesky に画像をポストするボットのコード

一般公開が始まった Bluesky、ユーザー数が一気に増えたので自動でポストを行うボットのニーズも増えてきたかと思います。いずれ誰かがもっと体系的な情報を整備すると思いますが、とりあえずボットを動かすことができたので、2024年2月時点でのポストを行うコードを公開しておこうと思います。ほんとに簡単です。

Japan EQ LocatorWorld EQ Locator では地震が発生するたびに、テキスト、サイトの URL、地震スクリーンショットの画像をポストしています。以下では、これらのデータはすでに準備ができているものとして、Bluesky へのポストの部分のみに注目します。なお、使用言語は Python です。

まず AT Protocol SDK atproto と画像ライブラリ Pillow をインストールしておきます。

pip install atproto Pillow

そして、コードの先頭でこれらのパッケージのクラスやモジュールをインポートします。なお、BytesIO は変換した画像のバッファとして使用します。

from atproto import Client, client_utils
from PIL import Image
from io import BytesIO

次に、AT Protocol クライアントを作成します。Client のコンストラクタの引数には base_url として 'https://bsky.social' を指定します。そして、自分の Bluesky ハンドル名とパスワードを使ってログインします。

client = Client(base_url='https://bsky.social')
client.login('<handle>.bsky.social', '<password>')

で、こちらがポストのテキストを用意する部分。text には投稿内容、url にはリンクが格納されている前提です。ポイントは、client_utils.TextBuilder を使ってテキストを組み立てている点です。X と異なり、Bluesky ではテキスト中の URL は自動ではリンクにならないため、client_utils.TextBuilder を使ってリンクや装飾などが付いたリッチテキストを組み立てる必要があります。

text = f'{year}年{month}月{day}日{hour}時{minute}分頃、' \
    f'{location}を震源とする地震がありました。' \
    f'震源の深さは{depth}、地震の規模は{magnitude}。'
url = 'https://nagix.github.io/japan-eq-locator/?' \
    + urllib.parse.urlencode(quake)

text_builder = client_utils.TextBuilder() \
    .text(text + '\n') \
    .link('nagix.github.io/japan-eq-loc...', url)

続いて画像を用意します。img_file に画像のパス名が入っている前提です。画像を未加工でポストするなら、ファイルをバイナリとして open して read() で返ってくる bytes オブジェクトを取得するだけて良いのですが、このケースでは読み込んだ PNG ファイルのサイズが Bluesky でポストできる画像のサイズの上限 1,000,000 バイトを超える恐れがあったため、Pillow を使って読み込み、RGB モードに変換(アルファ成分を除去)した上で JPEG としてバッファに書き出し、bytes オブジェクトを取得しています。

img_file = 'screenshot.png'

img = Image.open(img_file).convert('RGB')
with BytesIO() as f:
    img.save(f, format='JPEG')
    jpg = f.getvalue()

最後に、引数に組み立てたテキストデータ、画像データ、画像の代替文字列(ALT テキスト、ここではリンクを除いた本文を使用)、言語を指定して send_image() で送ります。

client.send_image(text_builder, jpg, text, langs=['ja'])

以上です!思ったより簡単ですね?もし詳しい SDK の解説を見たい方は、こちらのリファレンスが参考になると思います。

atproto.blue

Mini Tokyo 3D バージョン 3.3.0 リリース

Mini Tokyo 3D バージョン 3.3.0 がリリースされました (GitHub)。3.2.0 からの追加・修正機能を見ていきましょう。

地下駅出口情報の更新とバリアフリー設備の表示

地下駅出口のバリアフリー施設情報、利用可能時間の表示に対応しました。駅を選択した時に、階段、エスカレーター、エレベーター、車いすスロープの有無が確認できたり、現在時刻でその出口が閉まっているとオレンジ色の表示で区別されます。さらに、マップ上でもアイコンが表示されるため、どの出口がバリアフリーになっているかがよりわかりやすくなっています。今のところ、対応しているのは東京メトロ銀座線、丸の内線、日比谷線東西線、千代田線の各路線です。

ところでここ数年、東京メトロではエレベータの統一されたサインシステムを導入していることに気づきました。エレベータのアイコンに rbv といった文字と色で区別するようです。今回判明したものはその情報も付けてあります。

f:id:nagixx:20210130000806j:plain

PLATEAU プラグイン

プロジェクト PLATEAU が提供する東京の3D都市モデルを Mini Tokyo 3D と組み合わせて表示します。詳細な建築物の形状データとテクスチャーを利用して、非常にリアルな都市の景観を表示することができます。画面右側の下から3つ目「レイヤー設定」ボタンを押して「PLATEAU」レイヤーを選択してみてください。非常に重くてスマホ等では動かないので、GPU 搭載 PC での利用をお薦めします。

PLATEAU では東京都23区に加え、首都圏の他の都市モデルもありますが、負荷軽減のため現時点では千代田区中央区、港区、新宿区、品川区、渋谷区、豊島区のみに絞っています。建築物が表示されるまでに結構時間がかかるので気長にお待ちくださいね。

高リフレッシュレートのディスプレイに対応

Mini Tokyo 3D は 60Hz のリフレッシュレートを想定して実装されていましたが、最近は高リフレッシュレートのディスプレイやモバイルデバイスがよく見られるようになり、その場合、視点移動アニメーションや花火プラグインのアニメーションが速過ぎる問題がありました。リフレッシュレートに関わらずアニメーション速度が一定になるように修正を加え、高リフレッシュレートのディスプレイではより滑らかなアニメーション動作になりました。

フランス語サポート

コミュニティーのコントリビューションにより、フランス語に対応しました。

ダイヤ改正対応

東京モノレールの 2022/11/7 ダイヤ改正に対応。都営浅草線京急、京成、北総、芝山鉄道新京成の 2022/11/26 ダイヤ改正に対応。また、JR 東日本、東京メトロ都営地下鉄横浜市営地下鉄東武、西武、京王、小田急、東急、相鉄、横浜高速鉄道埼玉高速鉄道東葉高速鉄道つくばエクスプレス江ノ電関東鉄道秩父鉄道小湊鉄道多摩モノレール、千葉モノレールの 2022/3/12 ダイヤ改正に対応。さらに、金沢シーサイドラインの 2023/3/25 ダイヤ改正ユーカリが丘線の 2023/4/1 ダイヤ改正に対応しています。

旅客機のライブ発着の再開

2022年2月以降しばらくの間、羽田空港・成田空港に発着する旅客機は JAL 便と ANA 便しか表示されていませんでしたが、他の航空会社含め全便の表示を再開しました。

路線のラインが表示されない不具合の修正

カメラを地平線の見える角度まで傾けた時に、遠方の路線のラインが表示されない不具合を修正しました。

新しいレイヤーインターフェースの追加

カスタマイズに利用可能なインターフェースが追加されました。いずれも、開発者が独自の情報を地図上に重ねて表示したいときに、手軽にカスタムレイヤーを作るための方法を定義しています。

インターフェース 説明
GeoJsonLayerInterface GeoJSON データを表示するためのインターフェース
Tile3DLayerInterface 3D Tiles 仕様に準拠した形式の 3D タイルデータを表示するためのレイヤーインターフェース

詳細は API リファレンスをご覧ください。