MapLibreでレイヤーの特定のところだけ色を変える方法を4つ試してみた

# この記事を書こうと思ったわけ
MapLibre GL Jsでレイヤーに日本全国の町丁目ポリゴンや登記所備付地図データなど広範囲のポリゴンを設定した際に、特定の範囲内のポリゴンだけ塗りつぶし色を設定するにはどうしたらいいのだろうか？
と考えていろいろ試してみたので（無理矢理なものもありますが）共有しておこうと思いました。
他の方法もあればコメントいただけると嬉しいです。

# MapLibreでポリゴンレイヤーの塗りつぶし色を設定するには？
まず初めにMapLibreでポリゴンレイヤーの塗りつぶし色を設定する方法のおさらいです。
MapLibre GL Jsでポリゴンを塗りつぶすには、レイヤーのpaintプロパティのfill-colorで設定します。
また、ソースデータの値に応じてポリゴンの塗りつぶし色を変えたい場合は、interpolateやstepを使用して、例えば町丁目の年収階級別世帯数に応じて異なる色に設定するなどが可能です。

```JavaScript
// addLayerで塗りつぶし色を設定する例
map.addLayer({
    "id": "chomoku-polygon-layer",
    "type": "fill",
    "source": "chomoku-tile",
    "paint": {
        "fill-color": "#fd7e00",
        "fill-opacity": 0.3,
    }
});

//ある年収階層の世帯数で町丁目ごとに色分け
map.addLayer({
    "id": "nenshu-layer",
    "type": "fill",
    "source": "chomoku-tile",
    "paint": {
        "fill-color": [
            "interpolate",
            ["linear"],
            ["get", "nenshu"],
            0,
            "#a1b5ff",
            50,
            "#6284ff",
            100,
            "#dd0000",
            1000,
            "#ff0000",
        ]
        "fill-opacity": 0.3,
    },
});
```
interpolateを使用して色塗りした例
![interpolateを使用して色塗りした例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325378/ec6f19e0-ef2e-0184-e743-ca724beddccc.png "interpolateを使用して色塗りした例")

# やりたいこと
上の方法ではレイヤーのすべてのポリゴンに塗りつぶし色が設定されます。やりたいことは、レイヤーの特定の場所だけポリゴン色を変更することです。
ある地点を中心に半径1km円の範囲内のポリゴンや[mapbox-gl-draw](https://github.com/mapbox/mapbox-gl-draw "mapbox-gl-draw")を使って地図に描画した範囲内のポリゴンのみ色を変えたいのです。
MapLibreのバージョンによってはmapbox-gl-drawはうまく動かない場合があるので半径 n km円内のポリゴン色を変更する場合を想定して以下を進めます。

先に書いておきますが…方法1と方法2は無理やり感があるため、あまり参考にならないと思います。今回のケースでは方法3のmatchや方法4のinを使う方がベターだと思います。

### やりたいことのイメージ
特定地点（クリックした場所など）から半径 n km円の部分のポリゴンだけ色を変えたい
![change-layer-fill-color.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2325378/dd3cfbed-d18e-2ec4-0a40-127fd45f8b6d.gif)

今回試したベクトルタイルはプロパティにgeocodeを持った町丁目ポリゴンです。マップマーケティングの町丁目ポリゴンデータを使ってtippecanoeで作成しました。
また、MapLibre GL Jsの他に[Turf.js](https://turfjs.org/)を使用しています。以下のサンプルコード中のturf.point()などはTurf.jsの関数です。

# 方法1：setFeatureStateを使用する
[setFeatureState](https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.Map/#setfeaturestate)でやってみたらできたのでとりあえず載せておきます。
setFeatureStateでできたというよりはcaseの真偽判定にsetFeatureStateを使って色を変更したという具合です。

setFeatureStateを使用するとレイヤーのfeatureに対して特定の状態を持たせることができます。
[ホバー時にポリゴン色を変更する方法](https://maplibre.org/maplibre-gl-js/docs/examples/hover-styles/ "Create a hover effect")として使用例が紹介されています。

```JavaScript
const map = new maplibregl.Map({
    container: "map",
    style: {
        version: 8,
        sources: {
            "chomoku-tile": {
                "type": "vector",
                "tiles": ["http://127.0.0.1:3000/tiles/{z}/{x}/{y}.pbf"],
                "promoteId": "Geocode"
            }
        },
        layers: [
            {
                "id": "chomoku-polygon-layer",
                "type": "fill",
                "source": "chomoku-tile",
                "source-layer": "chomoku",
                "paint": {
                    "fill-color": [
                        "case",
                        ["boolean", ["feature-state", "selected"], false],
                        "#0072fd",
                        "#fd7e00"
                    ],
                    "fill-opacity": 0.3,
                },
            }
        ],
    },
    center: [139.66681193192352, 35.6722295797362],
    zoom: 11
});

map.on("load", function () {
    map.on("click", "chomoku-polygon-layer", (e) => {
        //クリック地点から半径2km円を作成
        const center = turf.point([e.lngLat.lng, e.lngLat.lat]);
        const radius = 2;
        const circle = turf.circle(center, radius);
        
        //円と描画されているfeatureで共有部分の有無を判定
        map.queryRenderedFeatures().forEach(f => {
            if (turf.booleanIntersects(f, circle)) {
                map.setFeatureState(
                    { source: "chomoku-tile", sourceLayer: "chomoku", id: f.properties.Geocode },
                    { selected: true }
                );
            } else {
                map.setFeatureState(
                    { source: "chomoku-tile", sourceLayer: "chomoku", id: f.properties.Geocode },
                    { selected: false }
                );
            }
        });
    });
});
```
:::note info
setFeatureStateではフィーチャーのidを指定します。ベクトルタイルにidを持っていなくてもプロパティで代用できる場合はsourceの[promoteId](https://maplibre.org/maplibre-style-spec/sources/#vector-promoteId)でプロパティの項目をidの代わりに指定することができます。
ポリゴンには一意のジオコードをプロパティに持たせてあるのでジオコードをpromoteIdに指定しました。
:::


# 方法2：stopsを使用する
[stops](https://maplibre.org/maplibre-style-spec/deprecations/#function-stops "MapLibre stops function")を試してみました。
stopsでもやりたいことはできはしましたが、stopsは非推奨となっているようなので使用しないほうがいいでしょう。
また、stopsという単語からどういうことができるのかがイメージしづらい印象を持ちました。
```JavaScript
const map = new maplibregl.Map({
    container: "map",
    style: {
        version: 8,
        sources: {
            "chomoku-tile": {
                "type": "vector",
                "tiles": ["http://127.0.0.1:3000/tiles/{z}/{x}/{y}.pbf"],
                "promoteId": "Geocode"
            }
        },
        layers: [
            {
                "id": "chomoku-polygon-layer",
                "type": "fill",
                "source": "chomoku-tile",
                "source-layer": "chomoku",
                "paint": {
                    "fill-color": "#000000",
                    "fill-opacity": 0.3,
                },
            }
        ],
    },
    center: [139.66681193192352, 35.6722295797362], 
    zoom: 11 
});

map.on("load", () => {
    map.on("click", "chomoku-polygon-layer", (e) => {
        const stops =  [];
        //クリック地点から半径2km円を作成
        const center = turf.point([e.lngLat.lng, e.lngLat.lat]);
        const radius = 2;
        const circle = turf.circle(center, radius);
        
        //円と描画されているfeatureで共有部分の有無を判定
        map.queryRenderedFeatures().forEach(f => {
            if (turf.booleanIntersects(f, circle)) {
                if (stops.some(elm => elm.includes(f.properties.Geocode))) { return; } //重複排除
                //対象のジオコードとカラーをセット
                stops.push([
                    f.properties.Geocode,
                    "#0072fd"
                ])
            }
        });

        map.setPaintProperty("chomoku-polygon-layer", "fill-color", { "property": "Geocode", "type": "categorical", "stops": stops } );                    
    });
});
```

# 方法3：matchを使用する
[match](https://maplibre.org/maplibre-style-spec/expressions/#match)も試してみました。
今回やりたいことは「対象のジオコードに合致するところの色を変えたい」です。matchという言葉はやりたいこととも合っていて、stopsよりわかりやすくなったと思います。
```JavaScript
const map = new maplibregl.Map({
    container: "map",
    style: {
        version: 8,
        sources: {
            "chomoku-tile": {
                "type": "vector",
                "tiles": ["http://127.0.0.1:3000/tiles/{z}/{x}/{y}.pbf"],
                "promoteId": "Geocode"
            }
        },
        layers: [
            {
                "id": "chomoku-polygon-layer",
                "type": "fill",
                "source": "chomoku-tile",
                "source-layer": "chomoku",
                "paint": {
                    "fill-color": "#fd7e00",
                    "fill-opacity": 0.3,
                },
            }
        ],
    },
    center: [139.66681193192352, 35.6722295797362], 
    zoom: 11
});

map.on("load", () => {
    map.on("click", "chomoku-polygon-layer", (e) => {
        //クリック地点から半径2km円を作成
        const center = turf.point([e.lngLat.lng, e.lngLat.lat]);
        const radius = 2;
        const circle = turf.circle(center, radius);
        
        //円と描画されているfeatureで共有部分の有無を判定
        const intersectedGeocodes = [];
        map.queryRenderedFeatures().forEach(f => {
            if (turf.booleanIntersects(f, circle)) {
                intersectedGeocodes.push(f.properties.Geocode);
            }
        });

        const match = [
            "match", 
            ["get", "Geocode"],
            intersectedGeocodes, //重複がある場合はArray.from(new Set(intersectedGeocodes))などで重複を排除して指定
            "#0072fd",
            "#fd7e00"
        ]; 
        
        map.setPaintProperty("chomoku-polygon-layer", "fill-color", match);
    });
});
```

# 方法4：filterとinを使用する
最後にExpressionsの [in](https://maplibre.org/maplibre-style-spec/expressions/#in) を使用してレイヤーのfilerにセットする方法です。
[setFilter()](https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.Map/#setfilter)で既存のレイヤーを操作して特定範囲内のポリゴンだけ表示させることができます。
addLayerでfilterをセットしたレイヤーを追加することもできます。このコード例では2回目以降のaddLayerの前に追加済みレイヤーの削除が必要になり、addLayerを使う場合はコード量が増えます。

```JavaScript
const map = new maplibregl.Map({
    container: "map",
    style: {
        version: 8,
        sources: {
            "chomoku-tile": {
                "type": "vector",
                "tiles": ["http://127.0.0.1:3000/tiles/{z}/{x}/{y}.pbf"],
                "promoteId": "Geocode"
            }
        },
        layers: [
            {
                "id": "chomoku-polygon-layer",
                "type": "fill",
                "source": "chomoku-tile",
                "source-layer": "chomoku",
                "paint": {
                    "fill-color": "#fd7e00",
                    "fill-opacity": 0.3,
                },
            },
            {
                "id": "intersected-layer",
                "type": "fill",
                "source": "chomoku-tile",
                "layout": {
                    "visibility": "none"
                },
                "paint": {
                    "fill-color": "#0072fd",
                    "fill-opacity": 0.6,
                },
                "source-layer": "chomoku",
            }
        ],
    },
    center: [139.66681193192352, 35.6722295797362],
    zoom: 11
});

map.on("load", () => {
    map.on("click", "chomoku-polygon-layer", (e) => {
        //クリック地点から半径2km円を作成
        const center = turf.point([e.lngLat.lng, e.lngLat.lat]);
        const radius = 2;
        const circle = turf.circle(center, radius);
        
        //円と描画されているfeatureで共有部分の有無を判定
        const intersectedGeocodes = [];
        map.queryRenderedFeatures().forEach(f => {
            if (turf.booleanIntersects(f, circle)) {
                intersectedGeocodes.push(f.properties.Geocode);
            }
        });
        
        if (intersectedGeocodes.length) {
            //setFilterを使って既存のレイヤーを変更する場合
            //宣言時にvisibilityをnoneにしているので変更
            map.setLayoutProperty("intersected-layer", 'visibility', 'visible');
            map.setFilter("intersected-layer", ["in", ["get", "Geocode"], ["literal", intersectedGeocodes]]);
            
            /*
            //addLayerを使用する場合（宣言部分のintersected-layerは不要）
            //追加済みレイヤーを削除
            if (map.getLayer("intersected-layer")) {
                map.removeLayer("intersected-layer");
            }
            
            //円と共有部分があったポリゴンレイヤー追加
            map.addLayer({
                "id": "intersected-layer",
                "type": "fill",
                "source": "chomoku-tile",
                "paint": {
                    "fill-color": "#0072fd",
                    "fill-opacity": 0.6,
                },
                "source-layer": "chomoku",
                "filter": ["in", ["get", "Geocode"], ["literal", intersectedGeocodes]]
            });
            */
        }
    });
});

```

# まとめ
今回はポリゴンの色塗りをいろいろと試してみましたが、MapLibreでレイヤーの表現を操作するにはMapLibre Style Specの[Expressions](https://maplibre.org/maplibre-style-spec/expressions/)の理解が重要だとわかりました。
試した方法を振り返ってみても、結局はどのExpressionを使うかということです。上記の他のExpressionでも同様のことができると思います。
どのExpressionがどのようなケースに適しているのかを知る必要があると思いました。


Expressionsについて@T-ubuさんが書かれれているとてもいい記事がありましたのでリンクさせていただきます。
上記いろいろ試した後に見つけたので、試す前にこちらの記事を読んでいればあまり悩まずにできていたのではと思います。

https://qiita.com/T-ubu/items/961176fb92fb66a927e0

また、前日の@humohumoさんのアドベントカレンダー記事もExpressionsについて取り上げられていて、とても参考になります。

https://qiita.com/humohumo/items/6460188ab7e8214a89bd

### おまけ
サンプルコードではベクトルタイルと円の共有部分の判定にTurf.jsのbooleanIntersectsを使用しました。
MapLibreのexpressionには[within](https://maplibre.org/maplibre-style-spec/expressions/#within)というものがあり円内に含まれているか判定することができます。今度withinも試してみようと思います。

追記
withinで評価可能なfeatureはPointとLineStringのみのようです。指定ポリゴン内に含まれるポイントは取得可能ですが本記事のような指定ポリゴン内に含まれるポリゴンは取得できませんでした。