4
3
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

地図プラットフォームを使用して現実世界をシミュレーションしてみた

Posted at

はじめに

昨年、以下記事を投稿し、地図プラットフォームであるHERE Maps API for Javascriptを使用して旅行ガイドを作ってみました。

今回はこれを拡張し、HERE Maps API for Javascriptのみではなく、その他のAPIを駆使して、日曜プログラミングレベルで、どの程度現実世界をシミュレーションできるか試してみました。以下のGIFイメージでは、

  • 目的地のシドニーオペラハウスを俯瞰的に確認
  • 目的地のシドニーオペラハウスの現場の写真イメージを確認
  • 目的地のシドニーオペラハウスの経路と到達時刻を確認
  • 目的地のシドニーオペラハウスの経路写真イメージを確認
  • 目的地のシドニーオペラハウスをChatGPTに解説してもらう

test_edit_0.gif

チャレンジした内容

今回も同じく海外旅行へ行った際のユースケースです。例えば、見知らぬ街に行った場合に、事前にデジタル技術を駆使してシミュレーションすれば、どの程度の手助けになるだろうか?というテーマとなります。と大層なことを申し上げていますが、単に昨年作成した旅行ガイドWebアプリ(シドニーの歩き方)の拡張版となります。

シナリオ

  1. 目的地をデジタル技術を使用してシミュレーションする。
  2. シミュレーション結果を元に、自分の脳内イメージを作る。
  3. 現地に赴く。(自分の脳内イメージと答え合わせ)

実装内容

HERE Maps API for Javascript Harp engineの実装

まず、昨年の旅行ガイドWebアプリ(シドニーの歩き方)と比較してHERE Maps API for JavascriptのレンダリングエンジンをHarp engineに変更し、さらにtiltを60度に変更することでより現実世界らしくし、地図描画もスムーズでかつ見やすくしました。

  • 昨年の旅行ガイド
    image.png
  • 今年の旅行ガイド
    test2.png

残念ながら、HERE Maps API for Javascript Harp engineはまだ日本地図に対応しておりませんが、海外の地図(例えばオーストラリア)では、HERE Mapオブジェクトを定義する際に以下のラインを追加するだけで、Harp enigneに対応することが可能です。

Harp engine
    const H = window.H;
    const platform = new H.service.Platform({
        apikey: <APIKEY>
    });
    const defaultLayers = platform.createDefaultLayers(
+      {engineType: H.Map.EngineType.HARP}
    );
    var map = new H.Map(mapRef.current, defaultLayers.vector.normal.map, {
      zoom: 17,
      center: { lat: gps.latitude, lng: gps.longitude },
      pixelRatio: window.devicePixelRatio || 1,
+      engineType: H.Map.EngineType.HARP
  });

Harp engineの詳細については以下のリンクに記載されています。

Google Photo realistic 3D Tilesの実装

次に、昨年GoogleよりリリースされましたGoogle Photo Realistic 3D tilesを使用して、目的地のイメージを俯瞰的に把握してみたいと思います。本実装を行うにあたり、以下の記事が大変参考になりました。ありがとうございます。

次のようなReactコンポーネントをMaplibre GL JS +deck.glで実装しました。

Google Photo realistic 3D tiles
import * as React from 'react';
import maplibregl from 'maplibre-gl';
import { useRef } from 'react';
import {Tile3DLayer} from '@deck.gl/geo-layers';
import {MapboxOverlay} from '@deck.gl/mapbox';
import { _TerrainExtension } from '@deck.gl/extensions';


const Maplibre3 = (props) => {
    const origin = props.origin;
    const google_apikey = props.google_apikey;
    const GOOGLE_API_KEY = google_apikey;
    const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
    console.log(origin);
    const data = [
        {name: 'Colma (COLM)', coordinates: [origin.longitude, origin.latitude,0]}
    ];
    const ICON_MAPPING = {
      marker: {x: 0, y: 0, width: 128, height: 128, mask: true}
  };

    
    // Create a reference to the HTML element we want to put the map on
    const map = useRef(null);
    const mapContainer = useRef(null);
    /**
     * Create the map instance
     * While `useEffect` could also be used here, `useLayoutEffect` will render
     * the map sooner
     */
      React.useEffect(() => {
      if (map.current) return;
        map.current = new maplibregl.Map({
            container: mapContainer.current,
        style: {
            version: 8,
            sources: {},
            layers: []
        },
        hash: true,
        zoom: 16,
        center: [origin.longitude,origin.latitude],
        pitch: 60,
        bearing: 0,
        attributionControl: true,
        antialias: true 
        })
        map.current.setCenter([origin.longitude,origin.latitude]);
        map.current.on('load', () => {
          const overlay = new MapboxOverlay({
            interleaved: true,
            layers: [
              new Tile3DLayer({
                id: 'google-3d-tiles',
                data: TILESET_URL,
                loadOptions: {
                  fetch: {
                    headers: {
                      'X-GOOG-API-KEY': GOOGLE_API_KEY
                    }
                  }
                },
                onTilesetLoad: tileset3d => {
                  tileset3d.options.onTraversalComplete = selectedTiles => {
                    const credits = new Set();
                    selectedTiles.forEach(tile => {
                      const { copyright } = tile.content.gltf.asset;
                    });
                    return selectedTiles;
                  }
                },
              }),
            ]
          });
          map.current.addControl(overlay);
        });
        return () => {
            map.current.remove();
            map.current=null;
        };      
    }, [props.origin]); // This will run this hook every time this ref is updated
    return <div className="map h-screen"  ref={mapContainer}  />;
};
export default Maplibre3;

今回拡張した旅行ガイドでは、以下の右ペインにある目的地リスト内のPhoto viewボタンをクリックすることで、Google photo 3D tilesのダイアログが表示されます。
test4.png
また、ブラウザと、自PCのNVIDIA GPUを関連付けすることで、ぐりぐり動かしてもスムーズに目的地を俯瞰することができます。
test3.gif
しかし、まだまだこれでは現実世界をシミュレーションという意味では弱いですね。。。。これではあくまで鳥になってたどり着いただけです。。。

Google Street Static View APIの実装

ということで結局はGoogle Street view頼みということで、Google Street view static APIを使ってみました。

与えている引数は本プログラム固有の変数となりますが、ポイントとしてはheadingを四方に振って、fovを120(最大値)にして現場のイメージを(静止画ですが)全方位に写真で確認しているところになります。

Google Street view static
<div class="grid grid-cols-2 gap-4">
    <div><img src={'https://maps.googleapis.com/maps/api/streetview?size='+streetView_width+'x'+streetView_height+'&location='+streetViewGps.geometry.coordinates[0][0][1]+','+streetViewGps.geometry.coordinates[0][0][0]+'&heading=0&pitch=20&fov=120&source=outdoor&key='+google_apikey} alt="photo" width={streetView_width} height={streetView_height}></img></div>
    <div><img src={'https://maps.googleapis.com/maps/api/streetview?size='+streetView_width+'x'+streetView_height+'&location='+streetViewGps.geometry.coordinates[0][0][1]+','+streetViewGps.geometry.coordinates[0][0][0]+'&heading=90&pitch=20&fov=120&source=outdoor&key='+google_apikey} alt="photo" width={streetView_width} height={streetView_height}></img></div>
    <div><img src={'https://maps.googleapis.com/maps/api/streetview?size='+streetView_width+'x'+streetView_height+'&location='+streetViewGps.geometry.coordinates[0][0][1]+','+streetViewGps.geometry.coordinates[0][0][0]+'&heading=180&pitch=20&fov=120&source=outdoor&key='+google_apikey} alt="photo" width={streetView_width} height={streetView_height}></img></div>
    <div><img src={'https://maps.googleapis.com/maps/api/streetview?size='+streetView_width+'x'+streetView_height+'&location='+streetViewGps.geometry.coordinates[0][0][1]+','+streetViewGps.geometry.coordinates[0][0][0]+'&heading=-90&pitch=20&fov=120&source=outdoor&key='+google_apikey} alt="photo" width={streetView_width} height={streetView_height}></img></div>
</div>

今回拡張した旅行ガイドでは、以下の右ペインにある目的地リスト内のStreet viewボタンをクリックすることで、Google Street view のダイアログが表示されます。
test8.png
シドニーオペラハウスの場合は指定の緯度経度ではインドアが表示されました。
test6.png
いずれにしても、これで人間目線で目的地の雰囲気を知ることができました。しかし、これでもまだ現実世界をシミュレーションするという意味では弱いですね。。。個人的には、目的地までの経路がどのような雰囲気かを知りたい。

Google Street View Static APIとHERE Routing API v8の合わせ技

というわけで、目的地までの経路をGoogle Street View Static APIで順次確認できる仕組みを実装しました。
この仕組みを実装するには、まず、HERE Routing API v8の計算結果、すなわち経路情報の緯度、経度群が必要になります。以下の様にH.util.flexiblePolyline.decode()を使用することで経路情報であるpolyline情報をデコードし、waypointListという辞書型の配列に変換する処理を作ります。なお、HERE Routing API v8をHERE Maps API for Javascriptで使用する方法については、以下の記事を参照頂ければと思います。

経路情報(waypointList)作成
result.routes[0].sections.forEach((section) => {
  console.log(section)
  var flexiblePolylineData = H.util.flexiblePolyline.decode(section.polyline)
  var tempPoint = [0,0]
  flexiblePolylineData.polyline.forEach ((point)=>
    {
        if (tempPoint[0] == 0 & tempPoint[1] == 0) {
          tempPoint = point;  
        } else {
          let heading = bearing(tempPoint[0], tempPoint[1], point[0], point[1])
          console.log(heading)
          console.log(point);
          tempPoint = point;
          const waypoint = {"point": point, "heading":heading};
          waypointList.push(waypoint);  
        }
    }
  )
  :
  (以下省略
}

ここでポイントとなってくるのはheading情報(経路の方角)を算出する部分になりますが、以下の様な関数を準備しました。以下のbearing関数は先ほどの処理で、waypointListの辞書型配列を作る際に使用しています。

heading算出
    function toRadians(degrees) {
      return degrees * Math.PI / 180;
    };
    
    function toDegrees(radians) {
      return radians * 180 / Math.PI;
    }

    function bearing(startLat, startLng, destLat, destLng){
      startLat = toRadians(startLat);
      startLng = toRadians(startLng);
      destLat = toRadians(destLat);
      destLng = toRadians(destLng);
      let y_p = Math.sin(destLng - startLng) * Math.cos(destLat);
      let x_p = Math.cos(startLat) * Math.sin(destLat) - Math.sin(startLat) * Math.cos(destLat) * Math.cos(destLng - startLng);
      let brng = Math.atan2(y_p, x_p);
      brng = toDegrees(brng);
      return (brng + 360) % 360;
    }

最後に経路情報(waypointList)の配列インデックスを更新するイベントリスナーを作成し、以下のGoogle Street view static APIの部分に組み込みます。

イベントリスナー
function forwardView() {
    setForwardViewCount(forwardViewCount = forwardViewCount + 1);
}  
表示部分
<img className="map h-full w-full" src={'https://maps.googleapis.com/maps/api/streetview?size='+streetView_width+'x'+streetView_height+'&location='+waypointList[forwardViewCount].point[0]+','+waypointList[forwardViewCount].point[1]+'&heading='+waypointList[forwardViewCount].heading+'&pitch=20&fov=120&source=outdoor&key='+google_apikey} alt="photo" width={streetView_width} height={streetView_height} onClick={() => { forwardView() }}></img>

非常にざっくりとした解説となりましたが、以下のイメージのように、画像をクリックするとアニメーションとまではいきませんが、目的地までの経路のGoogle Street View staticのイメージが更新されていきます。
test12.gif
また、Google Street viewイメージが更新される毎(画像をクリックする毎)に地図上の現在位置も同時に更新されるような仕組みとしました。
test13(1).png
ついに、現場にいないにも関わらず、数クリック程度で現場の雰囲気を確認することができるようになりました。

Open AI(Chat GPT) APIの実装

最後に、去年の流行りものですがOpen AIのAPIを使って目的地のメタ情報を取得します。以下のコードサンプルのmySrch.titleが目的地の名前、locationNameForAppはエリアを指します。(この例ではシドニーになります。)以下のプロンプトの通り、非常に欲張りな質問をしています。

Open AI
(async () => {
  const completion = await openai.chat.completions.create({
    model: "gpt-4-1106-preview",
    messages: [
      { role: "system", content: "非常に優秀な"+locationNameForApp+"のツアーガイドとして回答をしてください。"},
      { role: "user", content: "日本語で回答してください。" },
      { role: "assistant", content: "わかりました。日本語で回答します。" },
      { role: "user", content: locationNameForApp+"に存在する"+mySrch.title+"周辺の治安状況を知りたいです。" },
      { role: "assistant", content: "わかりました。治安状況についてあわせて提供します。" },
      { role: "user", content: locationNameForApp+"に存在する"+mySrch.title+"周辺のグルメ状況を知りたいです。" },
      { role: "assistant", content: "わかりました。周辺のグルメ状況をあわせて提供します。" },
      { role: "user", content: locationNameForApp+"に存在する"+mySrch.title+"はどのようなところか箇条書きで答えてください。また参考となるURLをいくつか教えてください。" }],
  });
})();

以下は目的地(シドニーオペラハウス)をクリックした際に、Open AIより返ってきた回答の一例になります。
test14.png
シドニーオペラハウスに関する非常に当たり障りのないシンプルな説明がされています。次にローカルのレストラン情報です。
test15.png
こちらは非常に驚いたのですが、日本語の情報ソースが少ない海外のレストラン場合でも自然な日本語表現で回答を生成してくれています。(本当に紙版の旅行ガイドが必要がない時代が来るのではと思えてきました。。。)

おわりに 

最後に落ちです、いざ、この旅行ガイドWebアプリを使用して、今回、 (シドニーでなく)メルボルン旅行向け自分の脳内イメージと実際とで答え合わせをしました。 その結果としては、ほぼ地図プラットフォームのパワーによるものですが、脳内イメージと一致するところもありました。しかし、これは全くの盲点だったのですが、メルボルンはブタクサの花粉がヒドイ。。。というわけで花粉症持ちの自分としてはマスクが必要な生活となってしまいました。(Google Pollen APIというものがありますが、それを使用するという手もあります。。。)それ以外にも、町中にホームレスの方々が多いとか、電動スクータ使用者が多いとか現場に行かないとわからないことが多く、まだまだ完全なシミュレーションには程遠いようです。(そもそも旅行前にシミュレーションするのはつまらないというご意見もあるかもしれませんが。)思いつくだけでも、まだまだ改良ポイントはいくつかあるので、引き続き、細々とこの開発を続けたいと思います。改良を重ねて将来的にはデジタルツインの基盤にできればおもしろいなと考えています。ここまで読んでいただきましてありがとうございました。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3