はじめに
REACT-MAP-GLはuber製のMapboxGL JSをラップしたReactのライブラリ群です。
Mapboxはデザインのカスタマイズが可能で、利用料金も50,000PV/月まで無償です。
ライセンスなどもGoogleマップに比べると緩く、例えば管理画面内などクローズドな環境でも無償で使うことができます。
(Googleマップは無償ではNGなはず)
REACT-MAP-GLを使えばMapboxのAPI群をReactで簡単に利用することができます。
今回は公式サンプルを利用して主な機能を解説してみます。
事前準備
公式ドキュメントのままですが、以下でインストールします。
npm install --save react-map-gl
基本的にwebpack2が推奨されておりサンプルもwebpack環境が用意されています。
またMapboxのAccessTokenも必要となります。
Mapboxのアカウント作成後、こちらで確認することができます。
サンプルを動かす手順
まずプロジェクト一式をダウンロードします。
git clone https://github.com/uber/react-map-gl.git
必要パッケージをインストールします。
cd react-map-gl
npm install
続いて動かすサンプル内のパッケージをインストールします。(ここではinteractionを利用)
cd examples/interaction/
npm install
環境変数MapboxAccessTokenに取得したTokenをセットします。
本来はdotenvやcross-envで管理しますが、今回はサンプル内のwebpack.configの先頭行に以下を追記します。
process.env['MapboxAccessToken']='取得したトークン'
これで準備完了となり以下のコマンドでサンプルが起動します。(http://localhost:8080/)
npm run start-local
サンプルの説明
bart-station.json
[
  {"name":"Lafayette (LAFY)","coordinates":[-122.123801,37.893394]},
  {"name":"12th St. Oakland City Center (12TH)","coordinates":[-122.271604,37.803664]},
  {"name":"16th St. Mission (16TH)","coordinates":[-122.419694,37.765062]},
  …
プロットするデータ群です。場所名と位置情報の2構成となっています。
root.js
/* global document */
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
ReactDOM.render(<App/>, document.body.appendChild(document.createElement('div')));
説明するまでもないですが、body内に核となるAppコンポーネントを追加しています。
app.js
/* global window */
import React, {Component} from 'react';
import {render} from 'react-dom';
import MapGL, {Marker} from 'react-map-gl';
import ControlPanel from './control-panel';
import bartStations from './bart-station.json';
const token = process.env.MapboxAccessToken; // eslint-disable-line
import MARKER_STYLE from './marker-style';
react-map-glについて、詳細は後述しますが以下が利用可能となります。
- InteractiveMap (Map Components 動的なマップ default)
- StaticMap (Map Components 静止画マップ)
- Marker (React Controls)
- Popup (React Controls)
- NavigationControl (React Controls)
- CanvasOverlay (Overlays)
- HTMLOverlay (Overlays)
- SVGOverlay (Overlays)
これらがreact-map-glの機能となります。
上記以外にexperimental(EventManager, MapControls, autobind)があります。
ただし実験的なものなので利用は自己責任とのことです。
control-panelはサンプル右上の操作盤コンポーネント、bartStationsはプロットするデータとなります。
 
 
export default class App extends Component {
  state = {
    viewport: {
      latitude: 37.729,
      longitude: -122.36,
      zoom: 11,
      bearing: 0,
      pitch: 50,
      width: 500,
      height: 500
    },
    settings: {
      dragPan: true,
      dragRotate: true,
      scrollZoom: true,
      touchZoomRotate: true,
      doubleClickZoom: true,
      minZoom: 0,
      maxZoom: 20,
      minPitch: 0,
      maxPitch: 85
    }
  }
  componentDidMount() {
    window.addEventListener('resize', this._resize);
    this._resize();
  }
  componentWillUnmount() {
    window.removeEventListener('resize', this._resize);
  }
  _resize = () => {
    this.setState({
      viewport: {
        ...this.state.viewport,
        width: this.props.width || window.innerWidth,
        height: this.props.height || window.innerHeight
      }
    });
  };
stateのviewportはMap Componentsのプロパティを管理しています。公式ドキュメント
プロパティは基本的にInteractiveMap・StaticMap同じものが用意されています。
settingsはControlPanelで利用する属性値となります。
componentDidMountのリサイズイベントで常時最大サイズとなるようにしています。
 
 
  _onViewportChange = viewport => this.setState({viewport});
  _onSettingChange = (name, value) => this.setState({
    settings: {...this.state.settings, [name]: value}
  });
  _renderMarker(station, i) {
    const {name, coordinates} = station;
    return (
      <Marker key={i} longitude={coordinates[0]} latitude={coordinates[1]} >
        <div className="station"><span>{name}</span></div>
      </Marker>
    );
  }
  render() {
    const {viewport, settings} = this.state;
    return (
      <MapGL
        {...viewport}
        {...settings}
        mapStyle="mapbox://styles/mapbox/dark-v9"
        onViewportChange={this._onViewportChange}
        mapboxApiAccessToken={token} >
        <style>{MARKER_STYLE}</style>
        { bartStations.map(this._renderMarker) }
        <ControlPanel containerComponent={this.props.containerComponent}
          settings={settings} onChange={this._onSettingChange} />
      </MapGL>
    );
  }
}
render()内の<MapGL>のディレクティブについてviewportとsettingsは前述したので割愛します。
mapStyleではMapBoxで利用するマップを指定します。
標準のマップがこちらにあるので他のマップに変えてみるのも面白いと思います。
onViewportChangeでは中心座標やズームなどviewportの変更が発生した場合のコールバック先を設定します。
このサンプルではパラメータで変更後の値を受け取り、そのままstateにセットします。
Marker部分について、まずはCSSを読み込みます。
(app.css内に記述しても良さそうですが柔軟に変更しやすいといった意図かと)
プロットするデータ単位でMarkerコンポーネントを作成し、latitudeとlongitudeにプロットする位置を指定します。
またプロット位置の調整がoffsetLeftやoffsetTopにピクセル数をセットすることで可能です。(アイコン画像の中央表示など)
ControlPanelについてcontainerComponentに上位のpropsの値を渡しています。
このサンプルではcontainerComponentの用意はなくControlPanel内のdefaultContainerを利用します。
Reduxの考え方として表示と処理を分離してComponentは用意すべきで、これに倣った形となります。
 
 
control-panel.js
import React, {PureComponent} from 'react';
const camelPattern = /(^|[A-Z])[a-z]*/g;
const defaultContainer =  ({children}) => <div className="control-panel">{children}</div>;
export default class ControlPanel extends PureComponent {
  _formatSettingName(name) {
    return name.match(camelPattern).join(' ');
  }
  _renderCheckbox(name, value) {
    return (
      <div key={name} className="input">
        <label>{this._formatSettingName(name)}</label>
        <input type="checkbox" checked={value}
          onChange={evt => this.props.onChange(name, evt.target.checked)} />
      </div>
    );
  }
  _renderNumericInput(name, value) {
    return (
      <div key={name} className="input">
        <label>{this._formatSettingName(name)}</label>
        <input type="number" value={value}
          onChange={evt => this.props.onChange(name, Number(evt.target.value))} />
      </div>
    );
  }
  _renderSetting(name, value) {
    switch (typeof value) {
    case 'boolean':
      return this._renderCheckbox(name, value);
    case 'number':
      return this._renderNumericInput(name, value);
    default:
      return null;
    }
  }
  render() {
    const Container = this.props.containerComponent || defaultContainer;
    const {settings} = this.props;
    return (
      <Container>
        <h3>Limit Map Interaction</h3>
        <p>Turn interactive features off/on.</p>
        <div className="source-link">
          <a href="https://github.com/uber/react-map-gl/tree/master/examples/interaction" target="_new">View Code ↗</a>
        </div>
        <hr />
        { Object.keys(settings).map(name => this._renderSetting(name, settings[name])) }
      </Container>
    );
  }
}
受け取ったsettingsのキーと値を並べ、値が変わればマップに反映します。
camelPatternはキーを表示する際に利用し、defaultContainerは前述したとおりです。
control-panel自体は動的に変わるものでないためPureComponentを利用します。
render()内でsettingsのキー毎に処理を行います。
_renderSettingで対象データの型から表示要素(チェックボックス/テキストボックス)を識別します。
ラベル名はキャメルケースでスペース区切りで表示します。(css内で大文字にしています)
onChangeで上位からの関数を実行し(AppComponent内でsetState)マップに反映します。
上記以外の主な機能
Popup
サンプルではマーカーを利用していましたがPopupも用意されおり基本的には同じ使い方です。
例えばapp.js内のMarkerコンポーネントを単純にPopupと差し替えれば置き換わります。
(marker-style.js内をこちらの内容に差し替えると綺麗に見えます)
詳細なプロパティは公式ドキュメントを確認してください。
Navigation Control
サンプルではcontrol-panelを用意していましたがMapbox標準のNavigation Controlが利用できます。
サンプル内でNavigationControlをインポートしControlPanelを以下に差替ると表示されます。
<div style={{position: 'absolute', right: 0}}>
  <NavigationControl onViewportChange={this._onViewportChange} />
</div>
詳細なプロパティは公式ドキュメントを確認してください。
Overlays(SVGOverlay, HTMLOverlay, CanvasOverlay)
マップ上にデータを重ね合わせて表現することができます。
サンプル内でSVGOverlayをインポート、以下メソッドを用意し、<MapGL>内に<SVGOverlay redraw={this._redraw} />を追加します。
  _redraw({project}) {
    const [cx, cy] = project([-122.36, 37.729]);
    return <circle cx={cx} cy={cy} r={4} fill="blue" />;
  }
これでサンプルでSVGOverlayが追加され青い点が表示されることが確認できます。
この機能を利用したreact-map-gl-heatmap-overlayで冒頭のイメージのようなOverlays が利用できます。
おわりに
Mapboxの地図が簡単に利用できるのはとても魅力的に感じました。
uberはreact-map-gl以外にも美しいVisualizationライブラリを沢山用意しています。
近いうちに他のライブラリも触ってみようかと。
