LeafletプラグインをReact+TypeScriptで使えるように拡張

# はじめに

leaflet の基本的な機能は、[react-leaflet](https://github.com/PaulLeCam/react-leaflet) を用いることで容易に React で扱うことができる。しかし、少しでも複雑なことを行うには react-leaflet だけでは不十分であり、leaflet プラグインを React+TypeScript で扱えるように拡張することが必要となる。本記事ではマーカーの回転を容易にできるようにする [leaflet-marker-rotation](https://github.com/abdulk1/Leaflet.RotatedMarker) を React に対応できるよう拡張する。なお、このプラグインでは、すでに型定義ファイル leaflet-marker-rotation/src/index.d.ts が存在しているため、TypeScript 対応は自身で行う必要がない。また react-leaflet はインストールされている前提で話を進める。
[公式ドキュメント Core API](https://react-leaflet.js.org/docs/core-api/) を参考にした。

# leaflet-marker-rotation のインストール

まず元となるプラグイン leaflet-marker-rotation をインストールする。

```bash
$ npm install leaflet-marker-rotation
```

leaflet-marker-rotation/src/index.d.ts を見ると、leaflet の Marker に対して rotationAngle と rotationOrigin の2つのオプションを拡張して作成されていることがわかる。（今回はプラグインを React で使用できることが目的なので、leaflet-marker-rotation/src/rotatedMarker.js のような具体的な中身の理解はしない。）

# react-leaflet を参考にコンポーネント内容の確認

急に拡張するといっても難しいので、まずは react-leaflet でどのように拡張されているのかを確認して参考にする。今回は Marker に関する拡張なので、react-leaflet 内で定義されている Marker コンポーネントの中身がどのようになっているかを見る。

```javascript:react-leaflet/esm/Marker.js
import { createLayerComponent } from '@react-leaflet/core';
import { Marker as LeafletMarker } from 'leaflet';
export const Marker = createLayerComponent(function createMarker({
  position,
  ...options
}, ctx) {
  const instance = new LeafletMarker(position, options);
  return {
    instance,
    context: { ...ctx,
      overlayContainer: instance
    }
  };
}, function updateMarker(marker, props, prevProps) {
  if (props.position !== prevProps.position) {
    marker.setLatLng(props.position);
  }

  if (props.icon != null && props.icon !== prevProps.icon) {
    marker.setIcon(props.icon);
  }

  if (props.zIndexOffset != null && props.zIndexOffset !== prevProps.zIndexOffset) {
    marker.setZIndexOffset(props.zIndexOffset);
  }

  if (props.opacity != null && props.opacity !== prevProps.opacity) {
    marker.setOpacity(props.opacity);
  }

  if (marker.dragging != null && props.draggable !== prevProps.draggable) {
    if (props.draggable === true) {
      marker.dragging.enable();
    } else {
      marker.dragging.disable();
    }
  }
});
```

まず上記をなんとなく理解する。コンポーネント作成に重要な役割を果たしている [createLayerComponent に関する公式ドキュメント](https://react-leaflet.js.org/docs/core-api/#createlayercomponent)には以下のように書かれている。

![createLayerComponent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/97cc9309-5528-ba74-1acc-196064c047e0.png)

Arguments にはインスタンスを作成して返す関数 __createElement__、インスタンスの内容を更新する関数 __updateElement__ の2つを使用している。（Type parameters と Returns は一旦置いておく。）
この構成をもとに先ほどの react-leaflet/esm/Marker.js を見ていくと、以下のような構成になっていることがわかる。

- __createMarker__ : Marker インスタンスを作成し、そのインスタンスを返す。（context とかは不明）
- __updateMarker__ : position, icon, zIndexOffset, opacity, draggable が変更されたときに更新する。

上記を理解した上で、マーカーの回転ができるコンポーネント RotatableMarker を作成する。

# コンポーネントの作成

作成したコンポーネントのコードを以下に示す。（全型のエラーを除去しきれなかったため一部 `any` を使用している。）

```typescript:RotatableMarker.tsx
import { createLayerComponent, LeafletContextInterface } from "@react-leaflet/core";
import { LatLngLiteral, MarkerOptions } from "leaflet";
import { RotatedMarker } from "leaflet-marker-rotation";

interface RotatedMarkerOptions extends MarkerOptions {
  /*
   * Rotation angle, in degrees, clockwise. Defaults to 0.
   */
  rotationAngle?: number;
  /*
   * Rotation angle, in degrees, clockwise. Defaults to 'bottom center'
   */
  rotationOrigin?: string;
}

interface RotatedMarkerProps {
  latlng: LatLngLiteral;
  options?: RotatedMarkerOptions;
}

function createRotatableMarker(props: any, context: LeafletContextInterface) {
  const instance = new RotatedMarker(props.latlng, props.options);

  return {
    instance,
    context: { ...context, overlayContainer: instance },
  };
}

function updateRotatableMarker(marker:any, props: RotatedMarkerProps, prevProps: RotatedMarkerProps) {
  if (props.latlng !== prevProps.latlng) {
    marker.setLatLng(props.latlng);
  }

  // add angle
  if (props.options?.rotationAngle !== prevProps.options?.rotationAngle) {
    marker.setRotationAngle(props.options?.rotationAngle || 0);
  }

  // add origin
  if (props.options?.rotationOrigin !== prevProps.options?.rotationOrigin) {
    marker.setRotationOrigin(props.options?.rotationOrigin || 'bottom center');
  }

  if (props.options?.icon != null && props.options?.icon !== prevProps.options?.icon) {
    marker.setIcon(props.options?.icon);
  }

  if (
    props.options?.zIndexOffset != null &&
    props.options?.zIndexOffset !== prevProps.options?.zIndexOffset
  ) {
    marker.setZIndexOffset(props.options?.zIndexOffset);
  }

  if (props.options?.opacity != null && props.options?.opacity !== prevProps.options?.opacity) {
    marker.setOpacity(props.options?.opacity);
  }

  if (marker.dragging != null && props.options?.draggable !== prevProps.options?.draggable) {
    if (props.options?.draggable === true) {
      marker.dragging.enable();
    } else {
      marker.dragging.disable();
    }
  }
}

const RotatableMarker = createLayerComponent<any, any>(createRotatableMarker, updateRotatableMarker);

export default RotatableMarker;
```

__createRotatableMarker__ にて、leaflet-marker-rotation プラグインから読み込んだ RotatedMarker を使ってインスタンスを作成、__updateRotatableMarker__ にて追加された rotationAngel と rotationOrigin の2つを更新するように修正した。200度回転して表示させた結果が以下。

![RotatableMarker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/9ee46909-b949-147d-0a58-388c678c4670.png)

# おわりに

leaflet プラグインの拡張ができた。ただ完全理解はできていない、かつ型定義も一部しきれていないので修正していきたい。わかる人がいればコメントください。