[React.js]REACT-MAP-GL入門

![REACT-MAP-GL](https://cloud.githubusercontent.com/assets/583385/13063531/e1571cf6-d3fd-11e5-8691-d08d1b481f4a.png)

## はじめに
REACT-MAP-GLはuber製のMapboxGL JSをラップしたReactのライブラリ群です。

Mapboxはデザインのカスタマイズが可能で、[利用料金](https://www.mapbox.com/pricing/)も50,000PV/月まで無償です。
ライセンスなどもGoogleマップに比べると緩く、例えば管理画面内などクローズドな環境でも無償で使うことができます。
（Googleマップは無償ではNGなはず）

REACT-MAP-GLを使えばMapboxのAPI群をReactで簡単に利用することができます。

今回は公式サンプルを利用して主な機能を解説してみます。

## 事前準備
[公式ドキュメント](https://uber.github.io/react-map-gl/#/Documentation/getting-started/get-started)のままですが、以下でインストールします。

```
npm install --save react-map-gl
```

基本的にwebpack2が推奨されておりサンプルもwebpack環境が用意されています。

またMapboxのAccessTokenも必要となります。
Mapboxのアカウント作成後、[こちら](https://www.mapbox.com/studio/account/tokens/)で確認することができます。

## サンプルを動かす手順
まずプロジェクト一式をダウンロードします。

```
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

``` Javascript: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

``` Javascript: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

``` Javascript: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`はプロットするデータとなります。
　
　

``` Javascript:app.js（初期設定など）
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のプロパティを管理しています。[公式ドキュメント](https://uber.github.io/react-map-gl/#Documentation/api-reference/static-map)
プロパティは基本的にInteractiveMap・StaticMap同じものが用意されています。
`settings`は`ControlPanel`で利用する属性値となります。

`componentDidMount`のリサイズイベントで常時最大サイズとなるようにしています。
　
　

``` Javascript:app.js（render部分）
  _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で利用するマップを指定します。
標準のマップが[こちら](https://www.mapbox.com/api-documentation/#styles)にあるので他のマップに変えてみるのも面白いと思います。

`onViewportChange`では中心座標やズームなどviewportの変更が発生した場合のコールバック先を設定します。
このサンプルではパラメータで変更後の値を受け取り、そのままstateにセットします。

Marker部分について、まずはCSSを読み込みます。
（app.css内に記述しても良さそうですが柔軟に変更しやすいといった意図かと）
プロットするデータ単位で`Marker`コンポーネントを作成し、`latitude`と`longitude`にプロットする位置を指定します。
またプロット位置の調整が`offsetLeft`や`offsetTop`にピクセル数をセットすることで可能です。（アイコン画像の中央表示など）

`ControlPanel`について`containerComponent`に上位のpropsの値を渡しています。
このサンプルでは`containerComponent`の用意はなく`ControlPanel`内の`defaultContainer`を利用します。
Reduxの考え方として[表示と処理を分離してComponentは用意すべき](http://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-components)で、これに倣った形となります。
　
　
### control-panel.js

``` Javascript: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内を[こちら](https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css)の内容に差し替えると綺麗に見えます）
詳細なプロパティは[公式ドキュメント](https://uber.github.io/react-map-gl/#/Documentation/api-reference/popup)を確認してください。

### Navigation Control

サンプルではcontrol-panelを用意していましたがMapbox標準のNavigation Controlが利用できます。
サンプル内で`NavigationControl`をインポートし`ControlPanel`を以下に差替ると表示されます。

```
<div style={{position: 'absolute', right: 0}}>
  <NavigationControl onViewportChange={this._onViewportChange} />
</div>
```

詳細なプロパティは[公式ドキュメント](https://uber.github.io/react-map-gl/#/Documentation/api-reference/navigation-control)を確認してください。

### 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](https://github.com/vicapow/react-map-gl-heatmap-overlay)で冒頭のイメージのようなOverlays が利用できます。

## おわりに
Mapboxの地図が簡単に利用できるのはとても魅力的に感じました。
uberはreact-map-gl以外にも美しいVisualizationライブラリを沢山用意しています。
近いうちに他のライブラリも触ってみようかと。