CesiumをReactで使えるライブラリを作った話

つい先日、CesiumをReact上で使えるライブラリ「Resium」v1.0.0を公開しました。

[darwin-education/resium](https://github.com/darwin-education/resium)

![Resium](https://raw.githubusercontent.com/darwin-education/resium/master/docs/resources/resium.gif)

Resium（とCesium）を使うことで、GoogleEarthのような3D地球儀を使ったGISなWebアプリケーションを手軽に開発することができます。

![スクリーンショット](https://raw.githubusercontent.com/darwin-education/resium/master/docs/resources/screenshot.png)

本稿では、Resiumの特徴と、Resiumを使うとどのようなことができるのか説明します。

# Resiumとは

[Cesium](https://cesiumjs.org/)とは、GoogleEarthのように、Web上で3D地球儀を表示させ、その上に様々なデータを表示させることができる、JavaScriptのGISのOSSライブラリです。

[React](https://reactjs.org/)は、もはや言うまでもないですが、今最も有名なWebのUIフレームワークです。

そして[Resium](https://github.com/darwin-education/resium)は、React上でCesiumを簡単に使えるように、Cesiumの各機能をコンポーネントとして抽象化したライブラリです。

React上にCesiumを表示させようとすると、今までは以下のようにせねばならず、実装が面倒でした。

```jsx
class Example extends React.PureComponent {
  divRef = React.createRef();
  viewer = null;
  entity = null;

  render() {
    return (
      <div ref={this.divRef} />
    );
  }

  componentDidMount() {
    this.viewer = new Cesium.Viewer(this.divRef.current);
    this.entity = this.viewer.entities.add({
      name: "Tokyo",
      position: Cesium.Cartesian3.fromDegrees(139.767052, 35.681167, 100)
    });
  }

  componentWillUnmount() {
    this.viewer.destroy();
  }
}
```

Resiumでは、Cesiumの各機能をReactコンポーネントとして閉じ込めたことで、ReactでCesiumアプリケーションをシンプルに記述できるようになり、仮装DOMの差分更新にも対応、コンポーネントの再利用性も抜群に向上します。

```jsx
const Example = () => (
  <Viewer>
    <Entity
      name="Tokyo"
      position={Cesium.Cartesian3.fromDegrees(139.767052, 35.681167, 100)} />
  </Viewer>
);
```

このアイデアは、 [react-leaflet](https://react-leaflet.js.org/)にインスピレーションを得ています。react-leafletも、React上でLeaflet（GoogleMapsのようなWeb2D地図ライブラリ）を使いやすくするために、Leafletの各機能をうまいことコンポーネントに閉じ込め、抽象化しています。

# 特徴1：宣言的に書ける

React自体の特徴とも言えますが、仮想DOMとJSXによる宣言的な記述は、各コンポーネントの細かい状態を管理する苦痛からプログラマを解放してくれます。

```jsx
<Viewer>
  <Entity
    name="Tokyo"
    position={Cesium.Cartesian3.fromDegrees(139.767052, 35.681167, 100)} />
</Viewer>
```

と言うJSXを返すだけで勝手にViewerが出来上がり、その中にEntityが追加されます。

その後同じエンティティの名前や位置を変えるには

```jsx
<Viewer>
  <Entity
    name="Osaka"
    position={Cesium.Cartesian3.fromDegrees(135.3112, 34.4111, 100)} />
</Viewer>
```

と言う感じにプロパティを変えるだけで、シームレスに切り替わり、

```jsx
<Viewer />
```

ViewerからEntityを消すと、自動的にエンティティがViewerから外され、破棄されます。

いちいちaddとかremoveとかdestroyメソッドを呼んだりしなくて済むのは楽です。

# 特徴2：ホットリロードをサポート

CesiumがReactコンポーネント化した結果、Resiumはホットリロード（HMR・Hot Module Replacement）に対応しています。

このおかげで、ソースコードを変えるだけで、ページリロードなしにCesiumを書き換えることができます！（※webpack等を使って開発している場合のみ）

![aa2.gif](https://qiita-image-store.s3.amazonaws.com/0/46132/79b8ac5a-d0dd-c582-72ad-4cf5c929da42.gif)

変更がサクサクブラウザに反映されていきます。開発速度が爆上がりですね！

これがあるので、もはやCesiumを直接使うより、Resiumで開発した方が開発者はハッピーなんではないか、と言うお気持ちになります。

# 特徴3：TypeScriptもサポート

最近のWebフロントエンド界隈では、もはやPureJSよりもTypeScriptの方が勢いがあるのでは、と言う情勢ですね。

Resiumもこの情勢を鑑み、一からTypeScriptで書き直されたため、ビルトインで型定義ファイルを備え、TypeScriptをフルサポートしています。（もちろんPureなJSでも使えます）

エディタでしっかり補完が利き、型チェックしてくれる安心感は素晴らしい。。。

![Screenshot](https://qiita-image-store.s3.amazonaws.com/0/46132/d96bd32b-73aa-1e11-95da-e132054fa85e.png)

ただ、Cesium本家の型定義が古いまま放置されており、ところどころResiumの実装で難儀しました。Resium側で頑張って最新版のCesiumに合わせて細かく型を定義していますが、全部がカバーできているわけではありません。時間ができたらCesium本家の型定義ファイルにもコントリビュートしたいなと思っています。

# 特徴4：Cesium本体にはない便利な機能が使える

Cesiumだけで実現しようとすると面倒なことを、Resiumでは簡単に実装できるようにしています。

## EntityのDescriptionもReactで書ける

EntityDescriptionと言うコンポーネントを使うと、Entityのdescription（クリックした時に表示されるInfoBoxの中身に表示される内容）もReactで書くことができます。ホットリロードも対応。

```jsx
<Viewer>
  <Entity
    name="Tokyo"
    position={Cartesian3.fromDegrees(139.767052, 35.681167, 100)}
    point={{ pixelSize: 10 }}
  >
    <EntityDescription>
      <h1>Hello World</h1>
      <p>Here is description.</p>
    </EntityDescription>
  </Entity>
</Viewer>
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/46132/fe0195da-81f4-95cc-01c4-2df66f3046d2.png)

内部的には、`react-dom/server.browser` の `renderToStaticMarkup` を内部で使ってdescriptionのHTMLをレンダリングしています。

## Entityにイベントを設定できる

例えば、特定のEntityがクリックされた時のイベントを実装するには、Cesiumだけでは非常に面倒でした。

```js
const viewer = Cesium.Viewer("container");
const entity = viewer.entities.add({ /* ... */ });
const screenSpaceEventHandler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);

screenSpaceEventHandler.setInputAction(
  e => {
    const picked = viewer.scene.pick(e.position);
    if (picked && picked.id === entity) {
      // entity is clicked
    }
  },
  Cesium.ScreenSpaceEventType.LEFT_CLICK
);

```

Resiumではたったこれだけです。

```jsx
<Viewer>
  <Entity
    ...
    onClick={(e, entity) => {
      // entity is clicked
    }}
  />
</Viewer>
```

Resium内部で統一的にイベントを管理しており、パフォーマンスに考慮した実装になっています。こうしたイベント管理をResiumが隠蔽しているので、開発者はイベントの詳細な実装について気にする必要がありません。

onClickだけでなく、 onMouseMoveやonTocuhMoveなど、Cesiumで使えるイベント全てと、onMouseEnter、onMouseLeaveといった独自イベントまで利用可能です。

これらのイベントは、Entityだけでなく、PointPrimitiveやLabelといったプリミティブでも利用可能です。

（てかこのくらいCesiumでもできるようにするべきでは…とか思ったり思わなかったり）

# まとめ

Resiumを使うと、メンテナンス性の高いCesiumアプリケーションを爆速で構築することができます。正直、このためだけにReactを導入しても良いくらいなのでは…とも思います。

Resiumを開発していたら、なんと、Cesiumの中の人からオファーがかかり、Cesium本家のブログにもResiumについて紹介した記事を寄稿させていただいております。

[Integrating Cesium with React | cesium.com](https://cesium.com/blog/2018/03/05/integrating-cesium-and-react/)

（v1リリース前までは「cesium-react」と言う名前だったのですが、中身の機能は大きく変わっていません）

Cesium公式にもしっかり認められている（？）Resiumをじゃんじゃん使っていただき、Cesiumを使ったアプリケーションをカジュアルに開発していただければと思います。

# おまけ

今回、ResiumのようなReactコンポーネントのライブラリを開発する上で、いくつか工夫を行なっています。これらについてはまたの機会に解説できればと思います。

* 共通のロジックを持つ複数コンポーネントの定義の仕方
  * Reactコンポーネントクラスを継承する方式から、HOC（High-Order Component）方式へ
* TravisCI上で、rollupで型定義ごとビルドして、自動で`npm publish`
* コンポーネントのソースコードをパースしてAST（抽象構文木）を生成、interface定義とコメントからAPIドキュメントを自動生成