ReactでGISデータを宣言的に可視化する

GISデータをGoogle Mapで可視化するにあたって、Reactと連携しながら使いたいことがあります。 一方で、Google MapのMaps JavaScript APIでオーバーレイは命令的に実行する必要があり、差分表示などもケアしようとするとそれなりのコードを書く必要がでてきます。

@react-google-maps/apiライブラリでは、Rectangle, Circle, HeatmapLayerなどオーバレイ用のコンポーネントが提供されており、これらを使うことで宣言的に可視化を行うことができます。本記事では、具体例を交えながら使い方を紹介します。

可視化例

@react-google-maps/apiライブラリを使うと、下図の様に可視化することができます。

気象庁アメダス 12/21 12時ごろの気温分布の可視化例
気象庁アメダス 12/21 12時ごろの気温分布の可視化例
1

実装フロー

@react-google-maps/apiライブラリによるGISデータ可視化のみを扱うため、以下は割愛します。 - GISデータの読み込み方法 - 状態管理周り

1. Google Mapの初期化

2つ方法があります。フックのuseJsApiLoaderを使うか、LoadScriptコンポーネントを使う方法です。

React 16.8以降にフックを使う場合においては、前者が推奨されています。具体的には、以下の様になります。

import * as React from "react";
import "./Map.css"
import { useJsApiLoader } from "@react-google-maps/api";

interface Props {
  children?: React.ReactNode;
}

const Map: React.FC<Props> = ({ children }) => {
  const { isLoaded } = useJsApiLoader({
    id: 'google-map-visualization',
    googleMapsApiKey: "your-api-key"  // 適宜、Google MapのAPIキーを指定してください
  })

  return <div className="mapWrapper">
    {isLoaded && (
      <GoogleMap
        mapContainerClassName="mapAreaFrame"
        center={{
          lat: 35.7,
          lng: 139.7
      }}
        zoom={5}
        onLoad={(map) => {
          // マップの初期表示が完了したとき(≠Google Mapsのスクリプトを読み込んだとき)に呼ばれる
        }}
        onBoundsChanged={() => {
          // マップの表示範囲が変わったときに呼ばれる。
          // 表示したいデータ数が多い場合、表示範囲でデータをフィルタするとよい。
        }}
      >
        {children}
      </GoogleMap>
    )}
  </div>
};

cssの内容は以下になります。

.mapWrapper {
  width: 80vw;
  height: 80vh;
}

.mapAreaFrame {
  width: 100%;
  height: 100%;
}

ポイントはmapAreaFrameクラスで、GoogleMapコンポーネントが親コンポーネントと同じサイズになるように設定しています。このサイズ設定がないと、Google Mapが表示されません。

2. GISデータをオーバーレイに変更する

当該ライブラリでは、以下を利用することができます2

コンポーネント 役割 対応するMaps JavaScript APIのページ
Circle 円を表示する Circles  |  Maps JavaScript API  |  Google Developers
Rectangle 矩形を表示する Rectangles  |  Maps JavaScript API  |  Google Developers
InfoWindow 吹き出し表示を特定地点に出す Info Windows  |  Maps JavaScript API  |  Google Developers
Marker ピンを表示する Markers  |  Maps JavaScript API  |  Google Developers
HeatmapLayer ヒートマップを表示する※ Heatmap Layer  |  Maps JavaScript API  |  Google Developers
Polyline 連続線を描画する Simple Polylines  |  Maps JavaScript API  |  Google Developers
Polygon ポリゴンを描画する Simple Polygon  |  Maps JavaScript API  |  Google Developers

Google Maps初期化時に、libraries指定で"visualization"を指定しておく必要があります。前述のuseJsApiLoaderでは、librariesに["visualization"]を指定するようにしてください。

これらコンポーネントは、極力Maps JavaScript APIに合わせてプロパティを指定できるようになっています。

今回は地点に対して可視化を行うので、Circleを使ってみます。

Circleでは、以下のパラメータを指定します。

  • center: 中心地点
  • radius: 半径[m]
  • onClick: クリックしたときのイベントハンドラ。options.clickableがtrueでないと反応しない
  • options: 描画オプション。ここで塗りつぶし色(fillColor)などを指定する。

コードで書くと以下の様になります。

import * as React from "react";
import {Circle, CircleProps} from "@react-google-maps/api";

type TemperatureByPoint = {
  id: string;  // 観測点のID
  temperatureCelsius: number;  // 観測点の気温[℃]
  placeName: string;  // 観測点の名前
  latitude: number;  // 観測点の緯度[°]
  longitude: number;  // 観測点の経度[°]
};

interface Props {
  temperatures: TemperatureByPoint[];
  minValue: number;    // temperatures中の最低気温
  maxValue: number;    // temperatures中の最高気温
}

const colorTable = ["#0000ff", ..., "#ff0000"];  // 256色分のカラーテーブル

const CirclesComponent = React.memo<Props>(({ temperatures, minValue, maxValue }) => {
  const circles = temperatures.map((t): CircleProps & {key: string }=> ({
    key: t.id,
    center: {
      lat: t.latitude,
      lng: t.longitude,
    },
    radius: 15000,  // 暫定で半径1.5kmとし、日本全域で見たときに見やすいようにしている
    onClick: () => {
      // 本来はInfoWindow等と組み合わせて表示するとよい
      alert(`${t.id}: ${t.placeName}`);
    },
    options: {
      clickable: true,
      fillColor: colorTable[Math.round((t.temperatureCelsius - minValue) / (maxValue - minValue) * 255)],
      fillOpacity: 0.9,
      strokeWeight: 0.1
    }
  }));
  return <>
    {circles.map((circle) => (
      <Circle
        key={circle.key}
        onClick={circle.onClick}
        center={circle.center}
        radius={circle.radius}
        options={circle.options}
      />
    ))};
  </>;
});

後は本コンポーネントに適宜データを渡すとOKです!

パフォーマンスの改善

マップ表示範囲内のデータのみ表示する

WebアプリとしてのGISデータ可視化を行う場合、データ量が多い場合描画負荷が顕著に現れます。利用者の端末のスペックに大きく依存するのですが、数百件オーダを超えてくると操作レスポンスが遅くなってきます。

そのため、マップの表示領域が変わるタイミングでマップの表示領域を取得し、表示範囲外のデータは可視化対象から外すことが推奨されます。@react-google-maps/apiにおいては、GoogleMapコンポーネントにonBoundsChangedプロパティが提供されています。あらかじめonLoadパラメータでmapオブジェクトを保持しておきつつ、このイベントが呼ばれたタイミングでmap.getBounds()を呼ぶと表示範囲を取得できます。後はフィルタ処理を行い、state等に反映すると再描画で反映されます。

実装にあたって遭遇したこと

マップ領域のDOMが表示されない

以下2つの原因があります。

  1. GoogleMapコンポーネントを格納する親コンポーネントの縦幅・横幅が決められておらず、縦横ともに0pxになっている
  2. GoogleMapコンポーネントが親コンポーネントのサイズに追従しておらず、縦横ともに0pxになっている

よくあるケースは2.の方で、GoogleMapコンポーネントのmapContainerClassNameプロパティで{ width: 100%; height: 100% }を入れておくと良いです。親コンポーネントのサイズに追従することで、マップが表示されます。

オーバレイが表示されない

以下のケースで起こりえます。

  1. Google Mapの初期化が完了するまでに、オーバレイするコンポーネントレンダリングが実行されている
  2. optionsにおいて、色の値や設定値に誤りがある

1.については、LoadScriptを使っている場合に発生することがあります。useJsApiLoaderでisLoaded === trueとなった場合のみGoogleMapコンポーネントおよびオーバレイをレンダリングすることで改善されます。


以上になります。皆様ぜひ良いお年を!