【React/Leaflet】MarkerClusterGroupを使おうとして苦戦した

ネコニウム研究所

PCを利用したモノづくりに関連する情報や超個人的なナレッジを掲載するブログ

【React/Leaflet】MarkerClusterGroupを使おうとして苦戦した

2023-10-31 | , , ,

React v18&react-leaflet v4&TypeScriptな環境でMarkerClusterGroupを使おうとして苦戦した話。

概要

今回の記事では、React 18&react-leaflet v4&TypeScriptな環境でMarkerClusterGroupを使う手順を掲載する。

仕様書

環境

  • react 18.2.0
  • react-leaflet 4.2.1
  • @changey/react-leaflet-markercluster 4.0.0-rc1
  • typescript 5.2.2

手順書

react v18&react-leaflet v4の環境でreact-leaflet-markerclusterのMarkerClusterGroupを使うとエラーになっちゃう。相性の問題?
この問題を回避したバージョンがあって下記のコマンドでインストールできる。

npm install @changey/react-leaflet-markercluster

JavaScriptな環境ならこれでreact v18&react-leaflet v4な環境でもMarkerClusterGroupが使えるようになる。(はず)

んで、TypeScriptな環境だともうひと手間必要でreact-leaflet-markerclusterの型定義ファイルを用意する必要あり。@changey/react-leaflet-markerclusterにはの型定義ファイルが含まれてないので本家のreact-leaflet-markerclusterから型定義ファイルnode_modules\@type\react-leaflet-markercluster\index.d.tsを拝借する。

プロジェクトのsrcreact-leaflet-markercluster.d.tsを作って、node_modules\@type\react-leaflet-markercluster\index.d.tsの中身を全部コピペする。

これを@changey/react-leaflet-markerclusterから使えるように加筆する。加筆後は下記のような感じ。

declare module '@changey/react-leaflet-markercluster' {

    /// <reference types="leaflet.markercluster" />

    import { DivIcon, Icon, MarkerCluster, Point, PolylineOptions } from "leaflet";
    import { ComponentType } from "react";

    interface MarkerClusterGroupProps {
        /**
         * showCoverageOnHover
         *
         * When you mouse over a cluster it shows the bounds of its markers.
         */
        showCoverageOnHover?: boolean | undefined;

        /**
         * zoomToBoundsOnClick
         *
         * When you click a cluster we zoom to its bounds.
         */
        zoomToBoundsOnClick?: boolean | undefined;

        /**
         * spiderfyOnMaxZoom
         *
         * When you click a cluster at the bottom zoom level we spiderfy it so you can see all of its
         * markers.
         *
         * Note: the spiderfy occurs at the current zoom level if all items within the cluster are
         * still clustered at the maximum zoom level or at zoom specified by disableClusteringAtZoom
         * option.
         */
        spiderfyOnMaxZoom?: boolean | undefined;

        /**
         * removeOutsideVisibleBounds
         *
         * Clusters and markers too far from the viewport are removed from the map for performance.
         */
        removeOutsideVisibleBounds?: boolean | undefined;

        /**
         * animate
         *
         * Smoothly split / merge cluster children when zooming and spiderfying.
         * If L.DomUtil.TRANSITION is false, this option has no effect (no animation is possible).
         */
        animate?: boolean | undefined;

        /**
         * animateAddingMarkers
         *
         * If set to true (and animate option is also true) then adding individual markers
         * to the MarkerClusterGroup after it has been added to the map will add the marker
         * and animate it into the cluster.
         * Defaults to false as this gives better performance when bulk adding markers.
         * addLayers does not support this, only addLayer with individual Markers.
         */
        animateAddingMarkers?: boolean | undefined;

        /**
         * disableClusteringAtZoom
         *
         * If set, at this zoom level and below, markers will not be clustered.
         * This defaults to disabled.
         *
         * Note: you may be interested in disabling spiderfyOnMaxZoom option when using
         * disableClusteringAtZoom.
         */
        disableClusteringAtZoom?: number | undefined;

        /**
         * maxClusterRadius
         *
         * The maximum radius that a cluster will cover from the central marker (in pixels).
         * Default 80.
         * Decreasing will make more, smaller clusters.
         * You can also use a function that accepts the current map zoom
         * and returns the maximum cluster radius in pixels.
         */
        maxClusterRadius?: number | ((zoom: number) => number) | undefined;

        /**
         * polygonOptions
         *
         * Options to pass when creating the L.Polygon(points, options) to show the bounds of a cluster.
         * Defaults to empty, which lets Leaflet use the default Path options.
         */
        polygonOptions?: PolylineOptions | undefined;

        /**
         * singleMarkerMode
         *
         * If set to true, overrides the icon for all added markers to make them appear as a 1 size cluster.
         * Note: the markers are not replaced by cluster objects, only their icon is replaced.
         * Hence they still react to normal events, and option disableClusteringAtZoom does not restore
         * their previous icon (see #391).
         */
        singleMarkerMode?: boolean | undefined;

        /**
         * spiderLegPolylineOptions
         *
         * Allows you to specify PolylineOptions to style spider legs.
         * By default, they are { weight: 1.5, color: '#222', opacity: 0.5 }.
         */
        spiderLegPolylineOptions?: PolylineOptions | undefined;

        /**
         * spiderfyDistanceMultiplier
         *
         * Increase from 1 to increase the distance away from the center
         * that spiderfied markers are placed.
         * Use if you are using big marker icons (Default: 1).
         */
        spiderfyDistanceMultiplier?: number | undefined;

        /**
         * iconCreateFunction
         *
         * Function used to create the cluster icon.
         */
        iconCreateFunction?: ((cluster: MarkerCluster) => Icon | DivIcon) | undefined;

        /**
         * spiderfyShapePositions
         *
         * Function used to override spiderfy default shape positions.
         */
        spiderfyShapePositions?: ((count: number, centerPt: Point) => Point[]) | undefined;

        /**
         * clusterPane
         *
         * Map pane where the cluster icons will be added.
         * Defaults to L.Marker's default (currently 'markerPane').
         */
        clusterPane?: string | undefined;

        /**
         * chunkedLoading
         *
         * Boolean to split the addLayers processing in to small intervals
         * so that the page does not freeze.
         */
        chunkedLoading?: boolean | undefined;

        /**
         * chunkInterval
         *
         * Time interval (in ms) during which addLayers works before pausing
         * to let the rest of the page process.
         * In particular, this prevents the page from freezing while adding a lot of markers.
         * Defaults to 200ms.
         */
        chunkInterval?: number | undefined;

        /**
         * chunkDelay
         *
         * Time delay (in ms) between consecutive periods of processing for addLayers.
         * Default to 50ms.
         */
        chunkDelay?: number | undefined;

        /**
         * chunkProgress
         *
         * Callback function that is called at the end of each chunkInterval.
         * Typically used to implement a progress indicator.
         * Defaults to null.
         */
        chunkProgress?: ((processed: number, total: number, elapsed: number) => void) | null;

        //子のコンポーネントを使えるようにする
        children: React.ReactNode;

        //Markerと同じような感じでrefを使えるようにする
        ref: (ref: any) => void;
    }

    declare const MarkerClusterGroup: ComponentType<MarkerClusterGroupProps>;
    export default MarkerClusterGroup;
}

@changey/react-leaflet-markerclusterから使えるようにdeclare module '@changey/react-leaflet-markercluster'の中に全体を含めるのと、MarkerClusterGroupMarkerコンポーネントを子にして使うんだけども何故かこのままだとPropsにchildren: React.ReactNodeが含まれておらず子のコンポーネントを使おうとするとエラーになるのでinterface MarkerClusterGroupPropsの中にchildren: React.ReactNode;を加筆する。

MarkerClusterGroupMarkerと同じような感じでuseRefを使いたい場合は、ref: (ref: any) => void;も加筆しておく。

import MarkerClusterGroup from '@changey/react-leaflet-markercluster';

インポートはこんな感じで

<MarkerClusterGroup spiderfyOnMaxZoom={true}>
    <Marker position={[lat, lng]}></Marker>
</MarkerClusterGroup>

レンダーの部分はこんな感じで使えるようになる。レンダーの方は本家のreact-leaflet-markerclusterと同じように使えるらしい。(本家を使ったことがないので、らしい)

まとめ(感想文)

MarkerClusterGroupを使うだけのために半日使ったよね…。