React で deck.gl を Google Maps JavaScript API と組み合わせて扱うときの知見(ノウハウ編)

React

タイトルの通り、React + Google Maps JavaScript APIでDeck.glを使ってみた時の知見を書いていきます。

Deck.glの導入については、以下の記事をご覧ください。

React で deck.gl を Google Maps JavaScript API と組み合わせて扱うときの知見(導入編)
タイトルの通り、React + Google Maps JavaScript APIでDeck.glを使ってみた時の知見を書いていきます。Deck.gl とは公式HPはこちらです。おそらくこのページに興味を持つ方は、なかなかマニアッ...

GeoJsonLayer

冒頭のDeck.gl導入記事では IconLayer を使いましたが、大量のデータを扱う場合は、 geojson 形式なことも多いと思います。
Deck.glには geojson を扱うための GeoJsonLayer が提供されているのですが、、まぁこのコンポーネントを扱うための情報がネット上に少ないです。
とりあえずは公式のドキュメントを見て頑張っていきました。

deck.gl | GeoJsonLayer
WebGL-powered visualization framework for large-scale datasets

GeoJsonLayerでマーカーを扱う

マーカーはいわゆる IconLayer を使うことになります。
pointTypeicon を指定することで、IconLayer っぽいプロパティを GeoJsonLayer でも使えるようになります。( IconLayerGeoJsonLayer でプロパティ名が微妙にことなるので「っぽい」という表現を使いました。)

具体的には下記のようになります。

import { GoogleMapsOverlay } from "@deck.gl/google-maps";
import { useGoogleMap } from "@react-google-maps/api";
import { GeoJsonLayer } from "deck.gl";
import { useEffect } from "react";

let overlay: GoogleMapsOverlay;

function DeckOverlay() {
    const map = useGoogleMap();

    const ICON_MAPPING = {
        marker: {x: 0, y: 0, width: 128, height: 128, mask: true}
    };

    const geojson = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                  "type": "Point",
                  "coordinates": [139.692101, 35.689634]
                },
                "properties": {
                  "name": "東京都庁"
                }
            },
            {
              "type": "Feature",
              "geometry": {
                "type": "Point",
                "coordinates": [139.6941689, 35.6902021]
              },
              "properties": {
                "name": "京王プラザホテル"
              }
          }
        ]
    };

    const geojsonLayer = new GeoJsonLayer({
      id: 'geojson_layer',
      data: geojson.features,
      pickable: true,
      pointType: 'icon',
      iconAtlas: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png',
      iconMapping: ICON_MAPPING,
      getIcon: () => 'marker',
      getIconSize: () => 5,
      iconSizeScale: 8,
      onClick: info => {
          console.log(info);
      },
      onHover: info => {
          if (!map) return;
          map.setOptions({ draggableCursor: info.object ? 'pointer' : 'grab' });

          console.log(info);
      },
  });

    useEffect(() => {
        overlay = new GoogleMapsOverlay({
            id: 'deck_gl_google_map_overlay',
            layers: [geojsonLayer],
        });
        overlay.setMap(map);
        return () => {
            overlay.finalize();
        };
    }, []);

    return null; 
};

export default DeckOverlay;

マーカーをsvgにする

getIcon の部分で色々と指定してやればよいです。

import { renderToString } from 'react-dom/server';
import { GoogleMapsOverlay } from "@deck.gl/google-maps";
import { useGoogleMap } from "@react-google-maps/api";
import { GeoJsonLayer } from "deck.gl";
import { useEffect } from "react";

let overlay: GoogleMapsOverlay;

function DeckOverlay() {
    const map = useGoogleMap();

    const geojson = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                  "type": "Point",
                  "coordinates": [139.692101, 35.689634]
                },
                "properties": {
                  "name": "東京都庁"
                }
            },
            {
              "type": "Feature",
              "geometry": {
                "type": "Point",
                "coordinates": [139.6941689, 35.6902021]
              },
              "properties": {
                "name": "京王プラザホテル"
              }
          }
        ]
    };
    const createSvgIcon = () => {
        const svg = renderToString(
            <svg
                width="80px"
                height="80px"
                viewBox="0 0 24 24"
                xmlns="http://www.w3.org/2000/svg"
                xmlnsXlink="http://www.w3.org/1999/xlink"
            >
                <circle fill="white" cx="12" cy="9" r="4" />
                <path
                    fill="#003452"
                    d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
                ></path>
            </svg>
        );
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
    };

    const geojsonLayer = new GeoJsonLayer({
        id: 'geojson_layer',
        data: geojson.features,
        pickable: true,
        pointType: 'icon',
        getIcon: () => ({
            url: createSvgIcon(),
            width: 80,
            height: 80,
        }),
        getIconSize: () => 3,
        iconSizeScale: 16,
        onClick: info => {
            console.log(info);
        },
        onHover: info => {
            if (!map) return;
            map.setOptions({ draggableCursor: info.object ? 'pointer' : 'grab' });

            console.log(info);
        },
    });

    useEffect(() => {
        overlay = new GoogleMapsOverlay({
            id: 'deck_gl_google_map_overlay',
            layers: [geojsonLayer],
        });
        overlay.setMap(map);
        return () => {
            overlay.finalize();
        };
    }, []);

    return null; 
};

export default DeckOverlay;

マーカーの大きさの微調整は、svgのwidthとheight、getIcon内で指定するwidthとheight、getIconSizeとiconSizeScaleをいじくり回して、ちょうど良いところを探してください。
ただ、あまり大きくしすぎると、マーカーが大量になったときにマーカーが黒くなってしまうことがありますので、注意です。

IconLayer with auto packing atlas stops drawing icons, generateMipmap crashes · Issue #3875 · visgl/deck.gl
Description We are using IconLayer to represent live data with auto packing atlas (we generate a few hundred different icons depending on the data we receive). ...

複数レイヤーの描画

実際には、 GeoJsonLayerIconLayer など、複数のレイヤーを組み合わせて使うと思います。複数のレイヤーを使うときにも色々とノウハウがあります。

描画方法

まずは、サンプルコードです。

import { renderToString } from 'react-dom/server';
import { GoogleMapsOverlay } from "@deck.gl/google-maps";
import { useGoogleMap } from "@react-google-maps/api";
import { GeoJsonLayer } from "deck.gl";
import { useEffect } from "react";

let overlay: GoogleMapsOverlay;

function DeckOverlay() {
    const map = useGoogleMap();

    const geojson1 = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [139.692101, 35.689634]
                },
                "properties": {
                    "name": "東京都庁"
                }
            },
        ]
    };

    const geojson2 = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [139.6941689, 35.6902021]
                },
                "properties": {
                    "name": "京王プラザホテル"
                }
            },
        ]
    };    

    const createSvgIcon = () => {
        const svg = renderToString(
            <svg
                width="80px"
                height="80px"
                viewBox="0 0 24 24"
                xmlns="http://www.w3.org/2000/svg"
                xmlnsXlink="http://www.w3.org/1999/xlink"
            >
                <circle fill="white" cx="12" cy="9" r="4" />
                <path
                    fill="#003452"
                    d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
                ></path>
            </svg>
        );
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
    };

    const makeGeoJsonLayer = (id: string, data: unknown) => {
        return new GeoJsonLayer({
            id,
            data ,
            pickable: true,
            pointType: 'icon',
            getIcon: () => ({
                url: createSvgIcon(),
                width: 80,
                height: 80,
            }),
            getIconPixelOffset: () => [0, -28],
            getIconSize: () => 3,
            iconSizeScale: 16,
            onClick: info => {
                console.log(info);
            },
            onHover: info => {
                if (!map) return;
                map.setOptions({ draggableCursor: info.object ? 'pointer' : 'grab' });

                console.log(info);
            },
        });  
    }

    const geojsonLayer1 = makeGeoJsonLayer('layer1', geojson1);
    const geojsonLayer2 = makeGeoJsonLayer('layer2', geojson2);

    useEffect(() => {
        overlay = new GoogleMapsOverlay({
            id: 'deck_gl_google_map_overlay',
            layers: [geojsonLayer1, geojsonLayer2],
        });
        overlay.setMap(map);
        return () => {
            overlay.finalize();
        };
    }, []);

    return null; 
};

export default DeckOverlay;

GoogleMapsOverlay をインスタンス化するときに、 layers に複数のレイヤーを指定してやればよいです。

レイヤーの更新

ここがドキュメント探すの大変でした。
このメソッドを使います。

deck.gl | GoogleMapsOverlay
WebGL-powered visualization framework for large-scale datasets

マーカーをランダムに動かすロジックを実装しています。

import { renderToString } from 'react-dom/server';
import { GoogleMapsOverlay } from "@deck.gl/google-maps";
import { useGoogleMap } from "@react-google-maps/api";
import { GeoJsonLayer } from "deck.gl";
import { useEffect, useState } from "react";

let overlay: GoogleMapsOverlay;
let coordinates1 = [139.692101, 35.689634];
let coordinates2 = [139.6941689, 35.6902021];

function DeckOverlay() {
    const map = useGoogleMap();

    const [point1, setPoint1] = useState(coordinates1);
    const [point2, setPoint2] = useState(coordinates2);

    const geojson1 = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": point1
                },
                "properties": {
                    "name": "東京都庁"
                }
            },
        ]
    };

    const geojson2 = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": point2
                },
                "properties": {
                    "name": "京王プラザホテル"
                }
            },
        ]
    };    

    const createSvgIcon = () => {
        const svg = renderToString(
            <svg
                width="80px"
                height="80px"
                viewBox="0 0 24 24"
                xmlns="http://www.w3.org/2000/svg"
                xmlnsXlink="http://www.w3.org/1999/xlink"
            >
                <circle fill="white" cx="12" cy="9" r="4" />
                <path
                    fill="#003452"
                    d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
                ></path>
            </svg>
        );
        return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
    };

    const makeGeoJsonLayer = (id: string, data: unknown) => {
        return new GeoJsonLayer({
            id,
            data ,
            pickable: true,
            pointType: 'icon',
            getIcon: () => ({
                url: createSvgIcon(),
                width: 80,
                height: 80,
            }),
            getIconPixelOffset: () => [0, -28],
            getIconSize: () => 3,
            iconSizeScale: 16,
            onClick: info => {
                console.log(info);
            },
            onHover: info => {
                if (!map) return;
                map.setOptions({ draggableCursor: info.object ? 'pointer' : 'grab' });

                console.log(info);
            },
        });  
    }

    const geojsonLayer1 = makeGeoJsonLayer('layer1', geojson1);
    const geojsonLayer2 = makeGeoJsonLayer('layer2', geojson2);

    const layerUpdate = (layer: string, direction: 0 | 1, range: number) => {
        console.log(layer)
        if (layer === 'layer1') {
            coordinates1[direction] = coordinates1[direction] + range;
        } else {
            coordinates2[direction] = coordinates2[direction] + range;
        }
        if (layer === 'layer1') setPoint1([...coordinates1]); 
        if (layer === 'layer2') setPoint2([...coordinates2]);
    }

    const randomUpdate = () => {
        setInterval(() => {
            const layer = `layer${ Math.random() > 0.5 ? 1 : 2}`;
            const direction = Math.random() > 0.5 ? 0 : 1;
            const range = Math.random() > 0.5 ? 0.001 : -0.001;
            layerUpdate(layer, direction, range);
        }, 1000);
    };

    useEffect(() => {
        overlay = new GoogleMapsOverlay({
            id: 'deck_gl_google_map_overlay',
            layers: [geojsonLayer1, geojsonLayer2],
        });
        overlay.setMap(map);
        randomUpdate();
        return () => {
            overlay.finalize();
        };
    }, []);

    useEffect(() => {
        overlay.setProps({ layers: [geojsonLayer1, geojsonLayer2] });
    }, [geojsonLayer1, geojsonLayer2]);

    return null; 
};

export default DeckOverlay;

こんな感じで動きます。

overlay.setProps に渡す layers は更新対象のレイヤーだけではなく、全てのレイヤーを渡してやる必要があります。(ちょっと違和感ですけど、、、)
ここでもそう言ってます。

How to update layer props in google maps? · Issue #4501 · visgl/deck.gl
I can't find out how to update ScatterplotLayer with google maps. I want to change visible(by visibleVar) and radiusMinPixels, radiusMaxPixels according to ...

終わりに

Deck.glは描画は早いですけど、クセがありますね、、、

タイトルとURLをコピーしました