Carto Tile地図をMaputnikとMaplibre GL JSを用いて作成する

# はじめに
Carto tileについて情報を更新する可能性もあり、そのため[こちらの記事](https://qiita.com/k96mz/items/ccf2d2628d89cfa824a6
)で作成したベクトルタイルを用いてMaputnikでスタイリングを行い、Maplibre GL JSで地図表示してみます。さらにポップアップの設定もしています。

# Maputnikを用いてスタイリングをする
[Maputnik](https://maplibre.org/maputnik/)を用いてスタイリングしていきます。まずは、作成したベクトルタイルを新規ソースとして追加します。ベクトルタイルはズームレベル４まで作成していますが、最大ズームはCarto tileのページに合わせて２としています。つまり、データとしてはズームレベル４までありますが、使用するデータはズームレベル２までです。ズームレベルが４まで拡大されていたとしても、使用されているデータはズームレベル２のものとなります。

![スクリーンショット 2024-08-22 16.56.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/ee640025-b6b6-2f17-c47f-36eb5711b196.png)

ここからは、[以前に記事にしたcartotile-plain-designのコードのスタイル](https://qiita.com/k96mz/items/49f9976ad6ed9fdd701a)に沿ってスタイル付けをしていきます。ポップアップの表示の解説もこちらの記事を参考にしてください。

## 背景（back ground）
まずは、背景（back ground）のレイヤを作成します。
レイヤ名： bg
色： rgb(255, 255, 255)

## bnda（ポリゴン）
次に、以下のようにしてbnda（ポリゴン）をレイヤとして追加します。

![スクリーンショット 2024-08-22 16.50.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/7c7ea987-9939-7b80-006b-0d86f93f554d.png)

そして、以下の通りスタイルを設定します。
Min Zoom: 0
Max Zoom: 4
Filter: no filter matches
iso3cd == ATA（始めにISO3CDとしていましたが、正常に反映されませんでした。大文字・小文字は分けて読み込まれるようなので注意です。）
Color: rgb(237, 237, 237)

「iso3cd」はISOに基づいて、３文字のアルファベットで表される国名だと思われます。ATAは南極（antarcticの略かも）を表してるので、南極以外を表示しています。

## bndl（ライン）
次にbndl（ライン）を使用してレイヤを設定しますが、一つのソースレイヤbndlから以下の３つのレイヤを作成します。
・bndl_solid
・bndl_dashed
・bndl_dotted

### bndl_solid
ID: bndl_solid
Min Zoom: 0
Max Zoom: 4

Filter: any filter matches
bdytyp == 0
bdytyp == 1
bdytyp == 2

Color: rgb(77, 77, 77)
Width: 0.8

bdytypが、 0 or 1 or 2である地物を対象としています。
bdytypはおそらく、boundary typeの略だと思われます。QGISで調べてみたところ、0が海岸線、1が内陸の国境線でした。2はデータが存在しません。

### bndl_dashed
ID: bndl_dashed
Min Zoom: 0
Max Zoom: 4

Filter: every filter matches
bdytyp == 3

Color: rgb(77, 77, 77)
Width: 0.8
Dasharray: 3, 2

bdytypが3である地物を対象としています。
3は合意されていない国境線だと思われます。
"line-dasharray": [3, 2]として、線が3ピクセル描かれ、2ピクセルの間隔が空く、という繰り返しのパターンが適用されます。アフリカ北東部は以下のように表示されます。

![スクリーンショット 2024-08-23 9.25.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/28d4a032-f155-be0b-4e7c-2c234621109e.png)

### bndl_dotted
ID: bndl_dotted
Min Zoom: 0
Max Zoom: 4

Filter: every filter matches
bdytyp == 4

Color: rgb(77, 77, 77)
Width: 0.8
Dasharray: 1, 2

bdytypが4である地物を対象としています。
4はカシミール地方のデータが１つのみ存在します。"line-dasharray": [1, 2]として、線が1ピクセル描かれ、2ピクセルの間隔が空く、という繰り返しのパターンが適用されます。以下のように、図の中心部分のように表現になります。

![スクリーンショット 2024-08-23 9.28.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/ad5a32bb-8b76-aafe-9be3-f0bb3b61e48e.png)

スタイルファイルを作成したら、Maputnik上でダウンロードします。
作成したスタイルファイルは、[こちら](https://github.com/k96mz/20240818CartoTileStylized/blob/main/CartoTile_style.json)にあります。Maputnikでインポートする場合は、以下のURLを使用できます。
 https\://k96mz.github.io/20240818CartoTileStylized/CartoTile_style.json
 
コードも以下に示します。

```json:CartoTile_style.json
{
  "version": 8,
  "name": "Empty Style",
  "metadata": {"maputnik:renderer": "mlgljs"},
  "sources": {
    "v": {
      "type": "vector",
      "tiles": [
        "https://k96mz.github.io/20240818CartoTileStylized/Data/VTstylized/{z}/{x}/{y}.pbf"
      ],
      "minzoom": 0,
      "maxzoom": 2
    }
  },
  "sprite": "",
  "glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
  "layers": [
    {
      "id": "bg",
      "type": "background",
      "layout": {"visibility": "visible"},
      "paint": {"background-color": "rgb(255, 255, 255)"}
    },
    {
      "id": "bnda",
      "type": "fill",
      "source": "v",
      "source-layer": "bnda",
      "maxzoom": 4,
      "filter": ["none", ["==", "iso3cd", "ATA"]],
      "paint": {"fill-color": "rgb(237, 237, 237)"}
    },
    {
      "id": "bndl_solid",
      "type": "line",
      "source": "v",
      "source-layer": "bndl",
      "maxzoom": 4,
      "filter": [
        "any",
        ["==", "bdytyp", 0],
        ["==", "bdytyp", 1],
        ["==", "bdytyp", 2]
      ],
      "paint": {"line-color": "rgb(77, 77, 77)", "line-width": 0.8}
    },
    {
      "id": "bndl_dashed",
      "type": "line",
      "source": "v",
      "source-layer": "bndl",
      "maxzoom": 4,
      "filter": ["all", ["==", "bdytyp", 3]],
      "paint": {
        "line-color": "rgb(77, 77, 77)",
        "line-width": 0.8,
        "line-dasharray": [3, 2]
      }
    },
    {
      "id": "bndl_dotted",
      "type": "line",
      "source": "v",
      "source-layer": "bndl",
      "maxzoom": 4,
      "filter": ["all", ["==", "bdytyp", 4]],
      "paint": {
        "line-color": "rgb(77, 77, 77)",
        "line-width": 0.8,
        "line-dasharray": [1, 2]
      }
    }
  ],
  "id": "dgzy6oo"
}
```

# スタイルファイルの統合
Maputnikで作成したスタイルファイルをcartotile-plain-designで使用されているものに近づけていきます。スタイルファイルをindex.htmlに統合したものは以下の通りです。index.htmlは[こちらの記事](https://qiita.com/k96mz/items/588a5241327f51cb5f9f)を参考に記載しています。

```html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Display a map</title>
    <!-- <meta
      property="og:description"
      content="Initialize a map in an HTML element with MapLibre GL JS."
    /> -->
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://unpkg.com/maplibre-gl@4.5.0/dist/maplibre-gl.css"
    />
    <script src="https://unpkg.com/maplibre-gl@4.5.0/dist/maplibre-gl.js"></script>
    <style>
      body {
        margin: 0;
        top: 0;
        bottom: 0;
        width: 100%;
      }
      p.stscod1 {
        font-style: normal;
        font-size: 9pt;
        font-family: "arial", "Roboto";
      }
      p.stscod2 {
        font-style: italic;
        font-size: 8.5pt;
        font-family: "arial", "Roboto";
      }
      p.stscod3 {
        font-style: normal;
        font-size: 9pt;
        font-family: "arial", "Roboto";
      }
      p.stscod4 {
        font-style: normal;
        font-size: 8.5pt;
        font-family: "arial", "Roboto";
      }
      p.stscod5 {
        font-style: normal;
        font-size: 8.5pt;
        font-family: "arial", "Roboto";
      }
      p.stscod6 {
        font-style: italic;
        font-size: 8pt;
        font-family: "arial", "Roboto";
      }
      p.stscod99 {
        font-style: normal;
        font-size: 6.5pt;
        font-family: "arial", "Roboto";
      }

      #map {
        position: absolute;
        top: 0;
        bottom: 0;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      const map = new maplibregl.Map({
        container: "map", // container id
        attributionControl: false,
        hash: true,
        renderWorldCopies: false,
        maxZoom: 3,
        zoom: 0.8, // starting zoom
        style: {
          version: 8,
          // name: "Empty Style",
          // metadata: { "maputnik:renderer": "mlgljs" },
          sources: {
            v: {
              type: "vector",
              tiles: [
                "https://k96mz.github.io/20240818CartoTileStylized/Data/VTstylized/{z}/{x}/{y}.pbf",
              ],
              attribution:
                '<table><tr><td style="font-size: 7pt; line-height: 100%">The boundaries and names shown and the designations used on this map do not imply official endorsement or acceptance by the United Nations.​ Final boundary between the Republic of Sudan and the Republic of South Sudan has not yet been determined.​<br>* Non-Self Governing Territories<br>** Dotted line represents approximately the Line of Control in Jammu and Kashmir agreed upon by India and Pakistan. The final status of Jammu and Kashmir has not yet been agreed upon by the parties.​<br>*** A dispute exists between the Governments of Argentina and the United Kingdom of Great Britain and Northern Ireland concerning sovereignty over the Falkland Islands (Malvinas).</td><td  style="font-size: 5pt; color: #009EDB" valign="bottom">Powered by<br><img src="https://unopengis.github.io/watermark/watermark.png" alt="UN OpenGIS logo" width="50" height="50"></td></tr></table>',
              minzoom: 0,
              maxzoom: 2,
            },
          },
          sprite: "",
          glyphs:
            "https://UN-Geospatial.github.io/cartotile-plain-design/font/{fontstack}/{range}.pbf",
          transition: {
            duration: 0,
            delay: 0,
          },
          layers: [
            {
              id: "bg",
              type: "background",
              layout: { visibility: "visible" },
              paint: { "background-color": "rgb(255, 255, 255)" },
            },
            {
              id: "bnda",
              type: "fill",
              source: "v",
              "source-layer": "bnda",
              maxzoom: 4,
              filter: ["none", ["==", "iso3cd", "ATA"]],
              paint: { "fill-color": "rgb(237, 237, 237)" },
            },
            {
              id: "bndl_solid",
              type: "line",
              source: "v",
              "source-layer": "bndl",
              maxzoom: 4,
              filter: [
                "any",
                ["==", "bdytyp", 0],
                ["==", "bdytyp", 1],
                ["==", "bdytyp", 2],
              ],
              paint: { "line-color": "rgb(77, 77, 77)", "line-width": 0.8 },
            },
            {
              id: "bndl_dashed",
              type: "line",
              source: "v",
              "source-layer": "bndl",
              maxzoom: 4,
              filter: ["all", ["==", "bdytyp", 3]],
              paint: {
                "line-color": "rgb(77, 77, 77)",
                "line-width": 0.8,
                "line-dasharray": [3, 2],
              },
            },
            {
              id: "bndl_dotted",
              type: "line",
              source: "v",
              "source-layer": "bndl",
              maxzoom: 4,
              filter: ["all", ["==", "bdytyp", 4]],
              paint: {
                "line-color": "rgb(77, 77, 77)",
                "line-width": 0.8,
                "line-dasharray": [1, 2],
              },
            },
          ],
          // id: "dgzy6oo",
        },
        // maplibreLogo: true,
      });
      map.addControl(new maplibregl.AttributionControl({ compact: false }));

      map.on("load", () => {
        map.addControl(new maplibregl.NavigationControl());
      });

      const popup = new maplibregl.Popup({
        closeButton: false,
        closeOnClick: false,
      });

      map.on("mousemove", "bnda", function (e) {
        map.getCanvas().style.cursor = "pointer";
        // console.log(e);
        // console.log(e.features[0].properties);

        if (e.features[0].properties.stscod == 1) {
          const html =
            "<p class='stscod1'>" +
            e.features[0].properties.nam_en.toUpperCase() +
            "</p>";
          popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
        } else if (e.features[0].properties.stscod == 2) {
          const html =
            "<p class='stscod2'>" +
            e.features[0].properties.nam_en.toUpperCase() +
            "</p>";
          popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
        } else if (e.features[0].properties.stscod == 3) {
          let html;
          if (
            e.features[0].properties.nam_en == "Falkland Islands (Malvinas) ***"
          ) {
            html =
              "<p class='stscod3'>" + e.features[0].properties.nam_en + "</p>";
          } else {
            html =
              "<p class='stscod3'>" + e.features[0].properties.nam_en + "</p>";
          }
          popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
        } else if (e.features[0].properties.stscod == 4) {
          const html =
            "<p class='stscod4'>" + e.features[0].properties.nam_en + "</p>";
          popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
        } else if (e.features[0].properties.stscod == 5) {
          const html =
            "<p class='stscod5'>" + e.features[0].properties.nam_en + "</p>";
          popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
        } else if (e.features[0].properties.stscod == 6) {
          const html =
            "<p class='stscod6'>" + e.features[0].properties.nam_en + "</p>";
          popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
        } else if (e.features[0].properties.stscod == 99) {
          // if (e.features[0].properties.nam_en == "Jammu and Kashmir **") {
          if (e.features[0].properties.nam_en == "Jammu and Kashmir") {
            const html =
              "<p class='stscod6'>" + e.features[0].properties.nam_en + "</p>";
            popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
          } else {
            popup.remove();
          }
        }
      });

      map.on("mouseleave", "bnda", function () {
        map.getCanvas().style.cursor = "";
        popup.remove();
      });
    </script>
  </body>
</html>
```

## コードの解説
スタイルファイルをindex.htmlに統合後、以下を追記。
```
attributionControl: false,
hash: true,
renderWorldCopies: false,
maxZoom: 3,
zoom: 0.8,
```

### Attribution
```
map.addControl(new maplibregl.AttributionControl({ compact: false }));
```
上記のコードを追加しました。AttributionControlのオプションとして、「compact: false」とすることで、地図を操作した時も、アトリビューション（著作権表示）」が常に表示されます。他にやり方があるのかもしれませんが、「attributionControl: false」と上記の組み合わせでうまく表示されます。以下はAttributionControlOptionsの参考ページです。

https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/AttributionControlOptions/


### glyphs
```
glyphs:"https://UN-Geospatial.github.io/cartotile-plain-design/font/{fontstack}/{range}.pbf",
```

glyphsを追加します。フォントは、cartotile-plain-designで使用されているものをそのままコピーして保存したものを使用します。

### transition
```html
transition: {
　　duration: 0,
　　delay: 0,
},
```
transition は、地図スタイルの変更時に使用されるアニメーション効果を制御する設定です。必要なのかどうか分かりませんが、とりあえず記載しておきます。

### ポップアップ制御部分の記載
cartotile-plain-designとは属性の名前等が異なっています。cartotile-plain-designでは、e.features[0].properties.MAPLABは国名を示していますが、こちらでは、e.features[0].properties.nam_enです。
e.features[0].properties.STSCODは、こちらでは小文字でe.features[0].properties.stscodです。
これらを反映させて、ポップアップ部分のコードを記載しました。
また、使用されるクラスのスタイルも記載しました。

"Jammu and Kashmir **"については、こちらでは"Jammu and Kashmir"となっていましたので、コードも修正しています。


# Maplibre GL JSで地図を表示
次にMaplibre GL JSで地図を表示します。
作成した地図は[こちら](https://k96mz.github.io/20240818CartoTileStylized/index.html)で見られます。

![スクリーンショット 2024-08-27 9.46.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/c2ca5fca-b799-3759-e0f1-b62c4a6c229e.png)

:::note info
※ベクトルタイルの投影法
ベクトルタイルはWebメルカトルと呼ばれる投影法によって表示されています。EPSGは3857です。ベクトルタイルを作成する際には、基本的にはtippecanoeを利用しますが、その際のインプットデータはデフォルトではWGS84(EPSG:4326)です。アウトプットデータはWebメルカトル（EPSG:3857）となります。Carto Mapは以下のとおり、WGS84(EPSG:4326)の見た目だったので、tippecanoeやGDALを利用して、WGS84の投影法に変換できるのか調べてみましたが、出来ないようでした。Carto Mapの元データが最初からWebメルカトル（EPSG:3857）であり、それをそのまま素直にベクトルタイルにしたものがCarto Mapではないかと思われます。
※追記
[Dirty Reprojectors](https://developmentseed.org/dirty-reprojectors-app/)というツールを利用するとWebメルカトルではない形で出力出来ます。
:::


![スクリーンショット 2024-08-27 9.53.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/5e049e1c-e347-e322-2729-11a6639caca2.png)

# まとめ
Carto Tile地図をMaputnikとMaplibre GL JSを用いて作成しました。ポップアップ表示の部分はif文を多用しており複雑に見えますが、構造はシンプルです。地図の投影法はなかなか理解するのが難しいですが、記事作成を通して、ベクトルタイルはやはりWebメルカトル（EPSG:3857）として表示されるのだと再認識しました。

# Reference

https://qiita.com/k96mz/items/ccf2d2628d89cfa824a6

https://qiita.com/k96mz/items/49f9976ad6ed9fdd701a

https://qiita.com/k96mz/items/1ac756935b685d20dd7c

https://qiita.com/k96mz/items/588a5241327f51cb5f9f