LoginSignup
9
9

More than 5 years have passed since last update.

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

Posted at

REACT-MAP-GL

はじめに

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

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

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

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

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
      }
    });
  };

stateviewportはMap Componentsのプロパティを管理しています。公式ドキュメント
プロパティは基本的にInteractiveMap・StaticMap同じものが用意されています。
settingsControlPanelで利用する属性値となります。

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

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>のディレクティブについてviewportsettingsは前述したので割愛します。

mapStyleではMapBoxで利用するマップを指定します。
標準のマップがこちらにあるので他のマップに変えてみるのも面白いと思います。

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

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

ControlPanelについてcontainerComponentに上位のpropsの値を渡しています。
このサンプルではcontainerComponentの用意はなくControlPanel内のdefaultContainerを利用します。
Reduxの考え方として表示と処理を分離してComponentは用意すべきで、これに倣った形となります。
 
 

control-panel.js

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ライブラリを沢山用意しています。
近いうちに他のライブラリも触ってみようかと。

9
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9