cartotile-plain-designのコードを読み解く

# はじめに
　ベクトルタイルの勉強を進めていますが、以下のURLに記載されているコードを使用することで、ベクトルタイルを用いてポップアップを表示をすることが可能です。自分の頭の整理を含めて、こちらのコードの解説を記載します。
 
https://github.com/UN-Geospatial/cartotile-plain-design

地図は[こちら](https://un-geospatial.github.io/cartotile-plain-design/index.html)から見られます。


# コード
使用されているコードは以下の通りです。長いですが、そのまま貼り付けます。

```html:index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link rel="stylesheet" type="text/css" href="https://UN-Geospatial.github.io/cartotile-plain-design/mapbox-gl.css"/>
<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>
<script src="https://UN-Geospatial.github.io/cartotile-plain-design/mapbox-gl.js"></script>
</head>
<body>
<style>
.mapboxgl-popup { max-width: 400px }
</style>
<div id="map"></div>
<script>
map = new mapboxgl.Map({
  container: 'map', 
  attributionControl: true, hash: true, renderWorldCopies: false, maxZoom: 3, zoom: 0.8,
  style: {
    version: 8,
    sources: {
      v: {
        type: 'vector',
        tiles: ['https://UN-Geospatial.github.io/cartotile-plain-design/data/cartotile_v01/{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>',
        maxzoom: 2,
        minzoom: 0
      }
    },
    glyphs: 'https://UN-Geospatial.github.io/cartotile-plain-design/font/{fontstack}/{range}.pbf',
    transition: {
      duration: 0,
      delay: 0
    },
    layers: [
      {
        id: 'background',
        type: 'background',
        layout: {'visibility':'visible'},
        paint: {
          'background-color': ['rgb', 255, 255, 255]
        }
      },
      {
        id: 'bnda',
        type: 'fill',
        source: 'v',
        'source-layer': 'bnda',
        maxzoom: 4,
        minzoom: 0,
        filter: [
        'none',
        ['==', 'ISO3CD', 'ATA']
        ],
        paint: {
          'fill-color': ['rgb', 237, 237, 237]
        }
      },
      {
        id: 'bndl_solid',
        type: 'line',
        source: 'v',
        'source-layer': 'bndl',
        maxzoom: 4,
        minzoom: 0,
        filter: [
        'any',
        ['==', 'BDYTYP', 1],
        ['==', 'BDYTYP', 0],
        ['==', '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,
        minzoom: 0,
        filter: [
        'all',
        ['==', 'BDYTYP', 3]
        ],
        paint: {
          'line-color': ['rgb', 77, 77, 77],
          'line-dasharray': [3,2],
          'line-width': 0.8
        }
      },
      {
        id: 'bndl_dotted',
        type: 'line',
        source: 'v',
        'source-layer': 'bndl',
        maxzoom: 4,
        minzoom: 0,
        filter: [
        'all',
        ['==', 'BDYTYP', 4]
        ],
        paint: {
          'line-color': ['rgb', 77, 77, 77],
          'line-dasharray': [1,2],
          'line-width': 0.8
        }
      }
    ]
  }
})
map.on('load', ()=> {
  map.addControl(new mapboxgl.NavigationControl())
});

var popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false
});

map.on('mousemove', 'bnda', function(e) {
map.getCanvas().style.cursor = 'pointer';

if(e.features[0].properties.STSCOD == 1 ){
  var html = "<p class='stscod1'>" + e.features[0].properties.MAPLAB.toUpperCase() + "</p>";

popup
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map)
} else if (e.features[0].properties.STSCOD == 2){
  var html = "<p class='stscod2'>" + e.features[0].properties.MAPLAB.toUpperCase() + "</p>";

popup
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map)
} else if (e.features[0].properties.STSCOD == 3){

if(e.features[0].properties.MAPLAB == "Falkland Islands (Malvinas) ***"){

  var html = "<p class='stscod3'>" + e.features[0].properties.MAPLAB + "</p>";
} else {
  var html = "<p class='stscod3'>" + e.features[0].properties.MAPLAB + "</p>";
}

popup
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map)
} else if (e.features[0].properties.STSCOD == 4){
  var html = "<p class='stscod4'>" + e.features[0].properties.MAPLAB + "</p>";

popup
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map)
} else if (e.features[0].properties.STSCOD == 5){
  var html = "<p class='stscod5'>" + e.features[0].properties.MAPLAB + "</p>";

popup
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map)
} else if (e.features[0].properties.STSCOD == 6){
  var html = "<p class='stscod6'>" + e.features[0].properties.MAPLAB + "</p>";

popup
.setLngLat(e.lngLat)
.setHTML(html)
.addTo(map)
} else if (e.features[0].properties.STSCOD == 99){

if(e.features[0].properties.MAPLAB == "Jammu and Kashmir **"){
  var html = "<p class='stscod6'>" + e.features[0].properties.MAPLAB + "​</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>
```

# 解説
HTMLの基本的な部分は飛ばします。

フォントの指定
```html
p.stscod1 { font-style:normal; font-size: 9pt; font-family:"arial", "Roboto"; }
```
スタイルの部分でこのような記載があります。
pタグにクラス名stscod1が付与された要素を対象としています

font-style: normal
フォントのスタイルを通常の文字スタイルとして指定します。

font-size: 9pt
フォントのサイズを9ポイントとして指定します。


font-family: "arial", "Roboto"
フォントの種類を指定します。この指定では、“arial”が優先され、“arial”が利用できない場合には”Roboto”が使われます。


```html
<script src="https://UN-Geospatial.github.io/cartotile-plain-design/mapbox-gl.js"></script>
```
mapboxのコードを読み込み、使用できるようにしています。maplibreではない点に注意します。


```html
<style>
.mapboxgl-popup { max-width: 400px }
</style>
```
max-width: 400px; は、ポップアップの最大幅を400ピクセルに制限するスタイルです。しかし、これを100pxなどに変更してもなにも変わりませんでした。Mapbox GL JSでポップアップのサイズを自動的に調整する機能が含まれており、それが優先されているのかもしれません。

```html
map = new mapboxgl.Map({
        container: "map",
        attributionControl: true,
        hash: true,
        renderWorldCopies: false,
        maxZoom: 3,
        zoom: 0.8,
```
◯attributionControl: true
trueだと、地図の下部に「アトリビューション（著作権表示）」が表示されます。反対にfalseだと非表示となります。

◯hash: true
trueだとURL部分にz, x, yの値が表示されます。

◯renderWorldCopies: false
地図を無限に繰り返し表示するかどうかを設定します。false に設定されている場合、地図が左右にスクロールされても、地球は一度だけ表示され、地図が繰り返されることはありません。

◯maxZoom: 3
表示される最大のズームレベルは３としています。

◯zoom: 0.8
デフォルトのズームレベルを0.8としています。実際に試してみると、なぜか0.97などと表示されましたが、これは地図が表示される画面の大きさによるもので、画面を小さくすると無事に0.8と表示されます。


```html
sources: {
            v: {
              type: "vector",
              tiles: [
                "https://UN-Geospatial.github.io/cartotile-plain-design/data/cartotile_v01/{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>',
              maxzoom: 2,
              minzoom: 0,
            },
          },
```

◯v:
ベクトルタイルのSourceを作る時に指定した「Source ID」を記載します。Maputnikで作成する場合は、自身で適当な値を指定します。

◯tiles: ["https://UN-Geospatial.github.io/cartotile-plain-design/data/cartotile_v01/{z}/{x}/{y}.pbf"
ベクトルタイルが格納されているURLを指定します。

◯attribution:
アトリビューション（著作権表示）」を表示しています。

◯maxzoom: 2
データとして利用するベクトルタイルはズームレベルが２までということです。例えば、ズームレベルが３となった場合でもズームレベル２のデータが表示されます。

◯minzoom: 0
データとして利用するベクトルタイルはズームレベルが０からということです。


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

glyphsは、地図上で使用されるフォントのグリフ（文字や記号の形状）を指定するためのURLを設定しています。{fontstack} と {range} はプレースホルダーで、実際に使用されるフォントスタックや文字範囲に応じて置き換えられます。これにより、地図上のテキスト（地名やラベルなど）が適切にレンダリングされます。
glyphsについてあまり分かっていませんが、調べたところによると以下の通りです。

◯フォントスタック (font stack)
複数のフォントをリスト形式で指定する仕組みです。Mapboxでは、テキストを表示する際に、まず最初のフォントが利用されますが、そのフォントで必要な文字が見つからない場合、次のフォントが使用されます。このように、複数のフォントを順にチェックして、適切なフォントを選択することが可能です。

◯レンジ (range)
文字コード（Unicode）の範囲を指定するもので、特定の範囲の文字を効率的にロードするために使用されます。例えば、ASCII文字（基本ラテン文字）は「0-255」という範囲に収められています。Mapboxは、このレンジを指定することで、必要な文字のglyphだけをロードし、無駄なデータのロードを避けることができます。


```html
transition: {
　　duration: 0,
　　delay: 0,
},
```

transition は、地図スタイルの変更時に使用されるアニメーション効果を制御する設定です。

◯duration: 0
アニメーションの継続時間を指定します。0に設定されているため、アニメーションが一瞬で完了します（実質的にはアニメーションなしの設定）。

◯delay: 0
アニメーションが開始されるまでの遅延時間を指定します。0に設定されているため、遅延なくすぐにアニメーションが開始されます。

## layersの設定

### バックグラウンドの設定
```html
{
  id: "background",
  type: "background",
  layout: { visibility: "visible" },
  paint: {
    "background-color": ["rgb", 255, 255, 255],
  },
},
```
id:　ユニークなIDを指定
type: backgroundを指定
layout: { visibility: "visible" }　visibleとして、背景を表示。
"background-color": 白色を指定


### bndaレイヤーの設定
```html
{
  id: "bnda",
  type: "fill",
  source: "v",
  "source-layer": "bnda",
  maxzoom: 4,
  minzoom: 0,
  filter: ["none", ["==", "ISO3CD", "ATA"]],
  paint: {
    "fill-color": ["rgb", 237, 237, 237],
  },
},
```

type: "fill"　として、ポリゴンを指定

source: "v"として、sourcesで使用したデータソース名称を指定

"source-layer": "bnda"として、ベクトルタイルのソースの"bnda"レイヤを指定

maxzoom: 4として、レイヤに対する最大ズームレベルを４と指定。４以上のズームレベルでは、このレイヤは描画されない。sourcesのmaxzoomが２であるので、それ以上のズームレベルではzoomlevel２のデータが描画（オーバーズーミング）されていると思います。

minzoom: 0として、レイヤに対する最小ズームレベルを０と指定。この数値以下のズームレベルでは、このレイヤは描画されない

filter: ["none", ["==", "ISO3CD", "ATA"]]として、IS03CDがATAではない地物のみが対象となります。元データであるshpファイルをQGISで見てみたところ、ATAは南極大陸でした。つまりは、南極大陸以外が描画されているということです。


### bndlレイヤーの設定
```html
{
  id: "bndl_solid",
  type: "line",
  source: "v",
  "source-layer": "bndl",
  maxzoom: 4,
  minzoom: 0,
  filter: [
    "any",
    ["==", "BDYTYP", 1],
    ["==", "BDYTYP", 0],
    ["==", "BDYTYP", 2],
  ],
  paint: {
    "line-color": ["rgb", 77, 77, 77],
    "line-width": 0.8,
  },
},
```
type: "line"　として、ラインを指定

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

line-width": 0.8として、ラインの太さを0.8ピクセルとして指定


```html
{
  id: "bndl_dashed",
  type: "line",
  source: "v",
  "source-layer": "bndl",
  maxzoom: 4,
  minzoom: 0,
  filter: ["all", ["==", "BDYTYP", 3]],
  paint: {
    "line-color": ["rgb", 77, 77, 77],
    "line-dasharray": [3, 2],
    "line-width": 0.8,
  },
},
```
filter: ["all", ["==", "BDYTYP", 3]]として、BDYTYPが 3 である地物のみが対象であることを指定します。3は合意されていない国境線だと思われます。
"line-dasharray": [3, 2]として、線が3ピクセル描かれ、2ピクセルの間隔が空く、という繰り返しのパターンが適用されます。


```html
{
  id: "bndl_dotted",
  type: "line",
  source: "v",
  "source-layer": "bndl",
  maxzoom: 4,
  minzoom: 0,
  filter: ["all", ["==", "BDYTYP", 4]],
  paint: {
    "line-color": ["rgb", 77, 77, 77],
    "line-dasharray": [1, 2],
    "line-width": 0.8,
  },
},
```
filter: ["all", ["==", "BDYTYP", 4]]として、BDYTYPが 4 である地物のみが対象であることを指定します。4はカシミール地方のデータが１つのみ存在します。"line-dasharray": [1, 2]として、線が1ピクセル描かれ、2ピクセルの間隔が空く、という繰り返しのパターンが適用されます。

### ナビゲーションコントロールの設定
```html
map.on("load", () => {
  map.addControl(new mapboxgl.NavigationControl());
});
```
地図オブジェクト（map）が読み込まれたときに、地図のズームイン/ズームアウトや回転を行うためのボタンであるナビゲーションコントロールを作成する。これは画面上の右上に配置される。

### ポップアップインスタンスの作成
```html
var popup = new mapboxgl.Popup({
  closeButton: false,
  closeOnClick: false,
});
```
closeButton: falseで、ポップアップウィンドウの右上に表示される✗ボタンを非表示とする。
closeOnClick: falseで、地図上の他の場所をクリックしたときにポップアップが閉じない設定にする。


### マウスがbndaレイヤに移動した時の設定
```html
map.on("mousemove", "bnda", function (e) {
  map.getCanvas().style.cursor = "pointer";
```
"mousemove"イベントは、ユーザーが地図の"bnda"レイヤ上でマウスを移動させたときに発生します。eはイベントオブジェクトで、イベントに関する情報（例えば、マウスの位置など）が含まれています。
map.getCanvas()で地図のキャンバス要素を取得します。キャンバス要素は、地図のビジュアルをレンダリングするHTML要素（通常は<canvas>タグ）です。
.style.cursor = "pointer"でキャンバス要素のCSSスタイルのcursorプロパティを設定しています。"pointer"に設定すると、マウスカーソルが指さしアイコン（通常は手の形のアイコン）に変わります。これは、リンクやボタンの上にマウスがあるときのデフォルトのカーソルです。
指差しアイコンと手の形のアイコンは似ていますが、よく見ると若干形が異なっているのが分かります。

eの中身を調べるために、
console.log(e);
console.log(e.features[0]);
としてカーソルをbndaレイヤ上に持っていきコンソールを見てみました。
そうすると、console.log(e)とした時に、eオブジェクトにはfeaturesプロパティがありませんでした。しかし、console.log(e.features[0])の場合には、情報が表示されました。
これは、カーソルをレイヤ上に持っていった瞬間には、まだeオブジェクトにfeatures（地物）の情報が格納されていないことが原因と考えられます。


```html
if (e.features[0].properties.STSCOD == 1) {
  var html =
    "<p class='stscod1'>" +
    e.features[0].properties.MAPLAB.toUpperCase() +
    "</p>";

  popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
}
```
e.features[0].properties.STSCOD(Status Codeの略？)が１である時に、以下を実行します。QGISでステータスコードが1の国を見てみると、国境問題がなさそうな国が該当していました。
変数htmlに「<p class='stscod1'> e.features[0].properties.MAPLAB.toUpperCase()</p>」
を代入しています。
クラスを最初の方にも出てきた'stscod1'としています。
e.features[0].properties.MAPLABは国名（ex. Peru)などを示しています。
toUpperCase()メソッドで、国名を大文字にしています。

popup.setLngLat(e.lngLat): ポップアップの位置をe.lngLatで指定します。e.lngLatは、マウスイベントが発生した位置の緯度と経度です。つまりマウスのある位置でポップアップが表示されます。
.setHTML(html): 作成したHTMLをポップアップに設定します。
.addTo(map): ポップアップを地図に追加して表示します。

```html
else if (e.features[0].properties.STSCOD == 2) {
  var html =
    "<p class='stscod2'>" +
    e.features[0].properties.MAPLAB.toUpperCase() +
    "</p>";

  popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
} 
```
先ほどと同様に、e.features[0].properties.STSCODが２である時に実行する関数を指定します。
QGISでステータスコードが２の地域を見てみると、ヨルダン川西岸とガザ地区の２つのポリゴンが該当しました。

```html
else if (e.features[0].properties.STSCOD == 3) {
  if (
    e.features[0].properties.MAPLAB == "Falkland Islands (Malvinas) ***"
  ) {
    var html =
      "<p class='stscod3'>" + e.features[0].properties.MAPLAB + "</p>";
  } else {
    var html =
      "<p class='stscod3'>" + e.features[0].properties.MAPLAB + "</p>";
  }

  popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
} 
```
e.features[0].properties.STSCODが３である時に実行する関数を指定します。
条件分岐が関数内で行われていますが、同じコードが記載されているため、条件分岐する意味がないコードとなっています。コード記載ミスかもしれません。
QGISでステータスコードが3の地域を見てみると、ケイマン諸島やバーミューダなどのある国の海外領土が該当しているようです。

この後も、e.features[0].properties.STSCODが4, 5, 6である時の場合が記載されています。
QGISでステータスコードが4の地域を見てみると、キュラソー島などのある国の海外領土が該当しているようでしたが、3との違いは不明です。
ステータスコードが５は台湾のみが該当していました。
6は該当ありません。

```html
else if (e.features[0].properties.STSCOD == 99) {
  if (e.features[0].properties.MAPLAB == "Jammu and Kashmir **") {
    var html =
      "<p class='stscod6'>" + e.features[0].properties.MAPLAB + "​</p>";

    popup.setLngLat(e.lngLat).setHTML(html).addTo(map);
  } else {
    popup.remove();
  }
}
```
e.features[0].properties.STSCODが99である時に実行する関数を指定します。
QGISでステータスコードが99の地域を見てみると、カシミール地方やエジプトとスーダンの国境付近の地域などの国境が定まっていない地域が該当しています。

e.features[0].properties.MAPLABの値が "Jammu and Kashmir **" である時は、上記で記載したようなポップアップを表示します。
それ以外の場合（アルナーチャル・プラデーシュ州など）は、popup.remove()、つまりポップアップ表示をしない設定となっています。ポップアップを表示させないことで、無用な懸念を惹起させないよう配慮しているのでしょう。

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

マウスがレイヤー "bnda" から離れたときに、カーソルのスタイルをデフォルトに戻します。しかし、デフォルトのスタイルも指定していたスタイルと同等なので、実際には変化はありません。
popup.remove()で表示されているポップアップを削除します。

:::note info
※ベクトルタイルの属性について
console.log(e.features[0])として、カーソルをbndaレイヤに合わせると、MAPLABが国名を表す属性であるということが分かります。実際の元データ（shpなど）がどこにあるのかは不明ですが、元データではMAPLABという属性があるのだと思います。
:::


# まとめ
cartotile-plain-designのコードの解説を行いました。どのようにポップアップの表現をコードに落とし込んでいるのか、国境問題がある地域での表現方法などが分かりました。

# Reference
https://qiita.com/T-ubu/items/17cb65754a54aa5ffb21

https://qiita.com/T-ubu/items/45f48f1990bcb5b011bf

https://github.com/ubukawa/cartotile