タイトルの通り、React + Google Maps JavaScript APIでDeck.glを使ってみた時の知見を書いていきます。
Deck.glの導入については、以下の記事をご覧ください。
GeoJsonLayer
冒頭のDeck.gl導入記事では IconLayer
を使いましたが、大量のデータを扱う場合は、 geojson
形式なことも多いと思います。
Deck.glには geojson
を扱うための GeoJsonLayer
が提供されているのですが、、まぁこのコンポーネントを扱うための情報がネット上に少ないです。
とりあえずは公式のドキュメントを見て頑張っていきました。
GeoJsonLayerでマーカーを扱う
マーカーはいわゆる IconLayer
を使うことになります。
pointType
に icon
を指定することで、IconLayer
っぽいプロパティを GeoJsonLayer
でも使えるようになります。( IconLayer
と GeoJsonLayer
でプロパティ名が微妙にことなるので「っぽい」という表現を使いました。)
具体的には下記のようになります。
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をいじくり回して、ちょうど良いところを探してください。
ただ、あまり大きくしすぎると、マーカーが大量になったときにマーカーが黒くなってしまうことがありますので、注意です。
複数レイヤーの描画
実際には、 GeoJsonLayer
や IconLayer
など、複数のレイヤーを組み合わせて使うと思います。複数のレイヤーを使うときにも色々とノウハウがあります。
描画方法
まずは、サンプルコードです。
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
に複数のレイヤーを指定してやればよいです。
レイヤーの更新
ここがドキュメント探すの大変でした。
このメソッドを使います。
マーカーをランダムに動かすロジックを実装しています。
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
は更新対象のレイヤーだけではなく、全てのレイヤーを渡してやる必要があります。(ちょっと違和感ですけど、、、)
ここでもそう言ってます。
終わりに
Deck.glは描画は早いですけど、クセがありますね、、、