🏬

PLATEAUで銀ぶらしよう

2024/08/01に公開

こんにちは、Webアプリケーションチーム、マネージャーの吉田裕紀です。私は天地人のGISプラットフォームTenchijin COMPASSに立ち上げから携わってきているのですが、開発がスタートした2019年当時よりも地理空間情報技術を巡る状況もどんどん進化していると感じる日々です。

天地人が扱う宇宙・リモートセンシング分野はもちろん、「100年に一度の大変革」とも言われる自動車業界の自動運転技術、流通から空飛ぶ車まで実用化の進むドローン、メタバースと共進化するデジタルツインなど、地理空間情報技術が活躍するフィールドは今後ますます重要になるのではないでしょうか。

その中でも特に日本の最注目トピックのひとつが、国土交通省が主導するデジタルツインプラットフォーム「PLATEAU」

PLATEAUには公式Viewerも用意されていますが、今回は自作のWebアプリケーションにPLATEAUのデータを表示してみたいと思います!

PLATEAU(プラトー)とは

改めて、PLATEAUとは:

PLATEAU by MLIT
2020年にスタートした、国土交通省が様々なプレイヤーと連携して推進する、日本全国の都市デジタルツイン実現プロジェクト。3D都市モデルの整備・活用・オープンデータ化のエコシステムを構築することで、まちづくりのデジタル・トランスフォーメーションを推進する。

https://www.mlit.go.jp/plateau/

ただの3Dじゃないよ、3D都市モデル

実際のところ3D地図は他のオンライン地図プラットフォームでもしばらく前から存在していました。しかし、既存の3Dモデルと大きく違う点は、PLATEAUの「3D都市モデル」の特徴でしょう。

キーワードは、「ジオメトリ(形状)」と「セマンティクス(意味)

3D都市モデルは、「ジオメトリとセマンティクスの統合モデル」とも呼ばれます。
ジオメトリ(Geometory)とは幾何のことで、建物などの物理的な位置や形状のことです。セマンティクス(Semantics)とは意味情報のことで、先に説明した、地物の種類や地物に与えられた「用途」「構造」「築年」などの属性情報のことです。3D都市モデルでは、建物全体に属性が付いているだけでなく、ひとつひとつの面(ポリゴン)に対しても、属性が付けられています。そのため、データによっては、屋根・床、内壁・外壁などの区別が付いていることもあります。

つまり、PLATEAUで表示される3D都市モデルでは建物の「見た目」だけではなく、構造や用途など「性質」の情報も合わせて持っているということです。

これまで一般に公開されていた多くの3D地図は上空または地上から3Dスキャンしたメッシュデータを元に作られていました。それゆえに広範囲がカバーされる一方、各構造物の区別がついていない状態が基本となります。対して、PLATEAUで公開されている3D都市モデルでは形状ひとつひとつに意味が持たせられています

また、これらはLOD(Level of Detail)という単位でレベル分けされ、CityGMLと呼ばれるXML形式のデータで記述されています。

LODによる詳細度の違い
(PLATEAU Learning TOPIC 1より)

これらの特徴ゆえに従来の3D地図ではできなかった建物単位、はたまた部屋単位での様々な計画策定が可能になるわけです。

オープンデータとしてのただならぬ気合い

また、技術面以外でも:

  • 驚くほど豊富で丁寧な公式ドキュメント、学習用素材
  • デザイン、映像など作り込まれた本気のクリエイティブ

これらからも既存のGISプレイヤーだけでなく積極的に新規層やクリエイター層を取り込んで、このオープンデータを盛り上げようという気合いが伝わってきますね。

https://www.mlit.go.jp/plateau/learning/

……ということで、これ以上の詳しい解説は公式にお任せしましょう。

つかってみよう!

今回はPLATEAUが公開している配信サービスのデータをJavascriptでブラウザに表示してみようと思います。

PLATEAU配信サービス(試験運用)

PLATEAUでは2024年7月現在、データの配信サービスを試験運用中のようです。
PLATEAU公式Githubレポジトリにそれぞれに関してのチュートリアルも公開されています。

https://github.com/Project-PLATEAU/plateau-streaming-tutorial/tree/main

建物データ(3D Tiles)は、区市町村ごとに取得できるようになっているようです。
データセット一覧は現在、Github記載の以下のAPIから取得できます。

Cesiumを使うよ

ブラウザでGISを扱うにあたって、Webフロントエンド用マップライブラリにもいくつか種類がありますが、今回はPLATEAUの公式ドキュメントでも登場するCesiumJSを使っていきます。

Cesiumはオープンソースの3D地理空間可視化プラットフォームで、多様な開発に対応しています。そのJavascript用ライブラリがCesiumJSという位置付けで、Webブラウザ上で地球や3D地図を表現できる高機能なライブラリとなっています。

https://cesium.com/

また、CesiumにはCesium ionという3Dデータのホスティング・配信サービスがあり、PLATEAUのデータもここから配信がされています。

今回は地形データ(PLATEAU-Terrain)をこのCesium ionから取得して使います。
今回はPLATEAUの公式Docに記載されている以下のTokenとアセットIDを使用します。

cesiumToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI5N2UyMjcwOS00MDY1LTQxYjEtYjZjMy00YTU0ZTg5MmViYWQiLCJpZCI6ODAzMDYsImlhdCI6MTY0Mjc0ODI2MX0.dkwAL1CcljUV7NA7fDbhXXnmyZQU_c-G5zRx8PtEcxE';

assetId = 770371;

東京都中央区を表示する

それではいよいよ始めましょう。今回は天地人のオフィスのある東京都中央区のデータを取得してみます。

今回はフロントエンドのみですが、通信が発生するのでローカルサーバ上で動作させることを前提としています。ローカルサーバの環境がない場合は、http-serverなどを使うのが簡単です。

npm install --global http-server
cd path/to/your/directory
http-server
ソースコード例
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <link href="style.css" rel="stylesheet" />
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.99/Build/Cesium/Cesium.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.99/Build/Cesium/Widgets/widgets.css" rel="stylesheet" />
</head>
<body>
    <div id="cesium-container"></div>
    <script src="main.js"></script>
</body>
</html>
main.js
/* -- PLATEAU-Terrain配信サービスにアクセスするトークンとアセットID。
*  公式Tutorialより: https://www.mlit.go.jp/plateau/learning/tpc06-1/ 
*/
const cesiumToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI5N2UyMjcwOS00MDY1LTQxYjEtYjZjMy00YTU0ZTg5MmViYWQiLCJpZCI6ODAzMDYsImlhdCI6MTY0Mjc0ODI2MX0.dkwAL1CcljUV7NA7fDbhXXnmyZQU_c-G5zRx8PtEcxE';
const assetId = 770371;
Cesium.Ion.defaultAccessToken = cesiumToken;

// -- CesiumのViewerを作成
const viewer = new Cesium.Viewer('cesium-container', {
    shadows: true
});

// -- PLATEAUのTerrain(地形)データを取得
const terrainProvider = new Cesium.CesiumTerrainProvider({
    url: Cesium.IonResource.fromAssetId(assetId),
});
viewer.terrainProvider = terrainProvider;

// -- PLATEAUのオルソ画像を取得
const imageProvider = new Cesium.UrlTemplateImageryProvider({
    url: 'https://api.plateauview.mlit.go.jp/tiles/plateau-ortho-2023/{z}/{x}/{y}.png',
    maximumLevel: 19
});
viewer.scene.imageryLayers.addImageryProvider(imageProvider);

// -- PLATEAUの3D-Tilesを取得
const plateauUrl = {
    chuoku: {
        lod2_tex: "https://assets.cms.plateau.reearth.io/assets/01/8c112f-4957-409a-9b43-d86308c7b74a/13102_chuo-ku_pref_2023_citygml_1_op_bldg_3dtiles_13102_chuo-ku_lod2/tileset.json",
        lod2_noTex: "https://assets.cms.plateau.reearth.io/assets/4c/f2436a-e2be-40e2-83da-f1781f36e30b/13102_chuo-ku_pref_2023_citygml_1_op_bldg_3dtiles_13102_chuo-ku_lod2_no_texture/tileset.json",  
    },
}
const tileset = new Cesium.Cesium3DTileset({ 
    url: plateauUrl.chuoku.lod2_tex 
});
viewer.scene.primitives.add(tileset);

// -- 初期表示時のカメラ位置を設定(天地人日本橋オフィス上空)
viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(139.776646, 35.687755, 200.0),
    orientation: {
        heading: Cesium.Math.toRadians(200),
        pitch: Cesium.Math.toRadians(-7),
    },
});

加えて、天地人のオフィスの場所にCesiumのBillboard機能を使って会社のロゴを配置してみて……

Billboard機能のコードサンプル
const position = Cesium.Cartesian3.fromDegrees(139.7746661, 35.6826155, 180);
viewer.entities.add({
    position: position,
    billboard: {
        image: "./img/logo-pin.svg",
        width: 80, 
        height: 112
    }
});

日本橋のスクリーンショット
表示できました!

上のスクリーンショットでは、三越前あたりから銀座方面を向いています。右下の屋上に丸い部分がある建物が日本橋三越新館ですね。

左奥の方に見える建物群は勝どき・晴海の超高層マンションエリアでしょうか、右側の線路の向こう側は千代田区になります。

地形データも読み込んでいるため、遠くの方に神奈川方面の山もうっすら見えますね。

天地人のオフィスがあるコレド日本橋ビルの横は現在工事中なので、丁度建物が見やすくなっています。これを見てふと思いましたが、工事の度に街並みは変化していきます。古くなった3D都市モデルは都市のデジタルアーカイブとしての役割を持ち始めるのかもしれませんね。

銀座ホコ天のスクリーンショット
視点を下げてみて、デジタル銀ぶら。GUごしに和光ビルが見えます。
(Cesiumでは、マウスでドラッグ移動する際に、Ctrlキー、Shiftキーを押しながらで視点移動ができます。)

しかし、テクスチャ付きのデータはさすがに結構重いようで、自分のPCは悲鳴をあげています……。
使用用途によってはテクスチャなしのデータを使う方が快適そうです。

セマンティクスも見てみよう

次にクリックイベントでセマンティクスとなる属性値を表示してみます。
Cesiumではデフォルトで3Dモデルをクリックするとテーブル表示する機能もありますが、今回はある程度カスタムして表示してみたいと思います。
この場合、Entityに対して、descriptionに指定したHTML文字列を代入します。

ソースコード例

// -- テーブルを生成する関数
const createTable = (pickedFeature) => {
    const paramKeyObjs = [
        { key: "bldg:measuredHeight", label: "高さ"},
        { key: "uro:BuildingDetailAttribute_uro:surveyYear", label: "調査年"},
    // ...表示したいパラメーターのkeyとそのラベルを書いていく    
        { key: "_lod_type", label: "LOD Type"},        
    ];

    const params = paramKeyObjs.map( keyObj => {
        return {
            label: keyObj.label, 
            val: pickedFeature.getProperty(keyObj.key)     
        }
    });

    const table = document.createElement("table");
    const tbody = document.createElement("tbody");
    params.forEach((param) => {
        const tr = document.createElement("tr");
        const td1 = document.createElement("td");
        td1.textContent = param.label;
        const td2 = document.createElement("td");
        td2.textContent = param.val;
        tr.appendChild(td1);
        tr.appendChild(td2);
        tbody.appendChild(tr);
    });
    table.appendChild(tbody);
    return table;
}

// -- クリックイベントのハンドラ
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function (movement) {
    const pickedFeature = viewer.scene.pick(movement.position);
    if (Cesium.defined(pickedFeature) && pickedFeature instanceof Cesium.Cesium3DTileFeature) {
        const table = createTable(pickedFeature).outerHTML;
        viewer.selectedEntity = new Cesium.Entity({
            id: "建物情報",
            description: table,
        });
    } else {
        viewer.selectedEntity = undefined;
    }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);


// -- カーソルをあてたときのハイライトのスタイル
const highlighted = {
    feature: undefined,
    originalColor: new Cesium.Color()
};

// -- マウス移動イベントのハンドラ
handler.setInputAction(function (movement) {
    const pickedFeature = viewer.scene.pick(movement.endPosition);
    if (Cesium.defined(pickedFeature) && pickedFeature instanceof Cesium.Cesium3DTileFeature) {
        if (highlighted.feature !== pickedFeature) {
            if (Cesium.defined(highlighted.feature)) {
                highlighted.feature.color = highlighted.originalColor;
            }
            
            highlighted.feature = pickedFeature;

            Cesium.Color.clone(pickedFeature.color, highlighted.originalColor);
            pickedFeature.color = Cesium.Color.YELLOW.withAlpha(0.5);
        }
    } else if (Cesium.defined(highlighted.feature)) {
        highlighted.feature.color = highlighted.originalColor;
        highlighted.feature = undefined;
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);




歌舞伎座タワーのスクリーンショット

無事表示できました。建物データに沢山の属性値が埋め込まれていることがわかります。
スクリーンショットは意外に背が高い歌舞伎座です。

築地のスクリーンショット
築地場外市場の一角、築地魚河岸の海幸橋棟。

時間経過の表現

Cesiumは素晴らしく、デフォルトでTimeline機能を搭載しているため、画面下のスライダーで時間別の影のシミュレーションができます。

細部の形状までつくられている3D都市モデルと影のシミュレーションは非常に相性がよさそうですね。

影のシミュレーションGIF

衛星画像の表示

最後にせっかくなので、天地人らしく衛星データも重ねてみましょう。今回は去年7月の平均地表面温度のデータを重ねてみます。
データはGeoTIFFフォーマットなので、今回はgeotiff.jsを使用しました。色の計算にはchroma.jsを使っています。
https://geotiffjs.github.io/
(ソースコード内の衛星データlst-tokyo-8days-avg-2023-07.tif'は天地人独自のデータとなります。実装例としてご参考ください。)

ソースコード例
function getColorForTemperature(value) {
    const colorScale = chroma.scale(['blue', 'cyan', 'green', 'yellow', 'orange', 'red']).domain([0, 1]);
    const color = colorScale(value).rgb();
    return color;
}

// -- GeoTIFFの読み込み
const tiff = await GeoTIFF.fromUrl('data/lst-tokyo-8days-avg-2023-07.tif');
const image = await tiff.getImage();
const width = image.getWidth();
const height = image.getHeight();
const rasters = await image.readRasters();
const data = rasters[0];

// -- canvasで可視化
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
const imageData = context.createImageData(width, height);
for (let i = 0; i < data.length; i++) {
    if (isNaN( data[i])) {
        imageData.data[i * 4 + 3] = 0;     
    }else{
        const lst = data[i] * 0.02 - 273.15; // スケール値と摂氏計算
        const minVal = 25;
        const maxVal = 45;
        const value = (lst - minVal) / (maxVal - minVal);
        const color = getColorForTemperature(value);
        imageData.data[i * 4] = color[0]; // R
        imageData.data[i * 4 + 1] = color[1]; // G
        imageData.data[i * 4 + 2] = color[2]; // B
        imageData.data[i * 4 + 3] = 127; // A
    }
}
context.putImageData(imageData, 0, 0);
const bounds = image.getBoundingBox();
const extent = Cesium.Rectangle.fromDegrees(bounds[0], bounds[1], bounds[2], bounds[3]);

// -- Cesiumのレイヤーとして表示
const geotiffLayer = viewer.scene.imageryLayers.addImageryProvider(new Cesium.SingleTileImageryProvider({
    url: canvas.toDataURL(),
    rectangle: extent
}));
geotiffLayer.alpha = 0.5;

今回は25℃〜45℃の範囲で、青から赤へ色をつけています。
地表面温度のスクリーンショット

今回は、メッシュサイズが約1km四方のデータになるので、少し引いて見ると地域別の温度差が見えてきます。こうやって見てみると中央区は比較的涼しかったようです。

他にもより局所的なデータや高度情報を含むデータなんかを3Dで重ねると、より一層PLATEAUのデータを活かせる気がします。

まだまだありそう

今回はさっと代表的な建物モデルのデータに触ってみましたが、他にも多様なデータが公開されているようです。この他にも道路モデルや植生モデル(木が生えるぞ!)のほか、日本橋の地名の由来にもなっている「日本橋」の詳細モデルなど、多様なモデルが用意されているみたいです。

また、これらのデータを別のデータやプラットフォームなんかと組み合わせると、さらに可能性は無限に増えそうですね。

妄想を膨らませながら、引き続き実際にデータを触って遊んでみようと思います!

今回は以上!


採用情報

株式会社天地人では、人工衛星などの宇宙ビッグデータを活用し、地球規模の課題に取り組むためのオンラインGISプラットフォーム天地人コンパスを開発しています。

私たちと一緒に天地人コンパスを開発してくれる仲間を募集しております。ご興味のある方は以下のページよりエンジニアリングの募集の求人にてご確認下さい。

https://www.wantedly.com/companies/company_5025838/projects

Discussion