leaflet + leaflet-kmlで表示させるアイコンのサイズをKMLのscaleで指定できるようにする

Leafletを使った地図アプリを作る上で試行錯誤したことをメモとして書いています。その機能あるよなど、なにかあればぜひコメントください。

## 概要
### やりたいこと
- leafletで表示中の地図に、KMLファイルの内容を表示し、表示されたアイコンサイズを変えたい
- issueに１件あるのですが、私はgoogleEarthから出力できるファイルをそのまま使いたかったので、独自に書くことにしました。https://github.com/windycom/leaflet-kml/issues/38

|as-is|to-be|
|--|--|
|![スクリーンショット 2023-11-17 4.10.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73246/6c4b4160-9b2f-d662-0ce5-0520508f4983.png)|![スクリーンショット 2023-11-17 6.27.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73246/5be3c60f-4196-bcd4-d370-b024c5bdd9de.png)|

### 結果
- leaflet-kmlを拡張(力技で仕様を上書き)
    - kmlに書かれたscale情報を取得できるようにする
    - アイコンサイズをscaleにあわせて拡大縮小できるようにする

## 使用するもの
- [Leaflet](https://leafletjs.com/) 
- leafletjsのKML表示プラグイン[leaflet-kml](https://github.com/windycom/leaflet-kml)
- KMLの作成には[Google マイマップ](https://www.google.com/maps/)、[Google Earth]()を利用しています。

## 事前準備

以前の記事で作成していた sample.kml を今回も仕様します。

 [KMLファイルを作成する]("https://qiita.com/mzone/items/1cf080b487636058f110")
  1. kmlファイルを作成する
  2. kml情報をWebページに表示する

## kmlに記載してあるアイコンサイズ(scale)を変える

今回は、kmlファイルの方のアイコンに記載している`scale`情報を編集しておきます。
２つあるマーカのうち、片方だけ`scale`を２にしておきます。

googlemapではアイコンサイズのscaleが1の場合、32pxで表示されます。
scaleはその何倍かを設定するものです。

```xml:sample.kml(一部抜粋)
<IconStyle>
  <color>ff0051e6</color>
  <!--  <scale>1</scale>  ← こうなっていたものを -->
  <scale>2</scale>　<!--  ← こうする  -->
  <Icon>
    <href>https://www.gstatic.com/mapspro/images/stock/503-wht-blank_maps.png</href>
  </Icon>
  <hotSpot x="32" xunits="pixels" y="64" yunits="insetPixels"/>
</IconStyle>
```

その状態でWebページをリロードしてもアイコンサイズに変化はありません。
leaflet-kmlでは、アイコンサイズは32pxに固定されています。

まだサイズに変化はありません。
![スクリーンショット 2023-11-17 4.10.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73246/5af16f12-b163-f136-7f8a-9370bc4735eb.png)


## scaleが適用できるようにする
leaflet-KMLのコードを一部コピーし、機能を拡張します。

### scaleが適用できるようにする
#### アイコンを処理している関数(parseStyle)をコピー

Leaflet-KML Pluginのファイル、KML.jsの中で`parseStyle`を検索。出てきた箇所を含む関数`parseStyle`をごっそりコピー、自分で作った`index.html`の`Leaflet-KML Plugin`を読み込んだ直後に貼り付ける。

貼り付けの際、parseStyle:functionの部分をL.KML.parseStyle=functionに書き換える。
```js: index.htmlの一部分を抜粋
// ↓こうなっていたものを
parseStyle:function function (xml) { ...

// ↓ こうする
L.KML.parseStyle = function function (xml) { ...
```

#### kmlに記載しているscale情報を取得
0. 標準ではscale情報を読み取っていないので、scale情報を取得するようにする
0. scale情報からアイコンサイズを拡大させる

##### 1. 標準ではscale情報を読み取っていないので、scale情報を取得するようにする
```js: index.htmlの一部分を抜粋
// L.KML.parseStyle = function (xml) の中の
// function _parse (xml) { の中

if (key === 'hotSpot')
  {
    for (var j = 0; j < e.attributes.length; j++) {
      options[e.attributes[j].name] = e.attributes[j].nodeValue;
	}
  } else {
    var value = e.childNodes[0].nodeValue;
    if (key === 'color') {
       options.opacity = parseInt(value.substring(0, 2), 16) / 255.0;
       options.color = '#' + value.substring(6, 8) + value.substring(4, 6) + value.substring(2, 4);
    } else if (key === 'width') {
       options.weight = value;
    } else if (key === 'Icon') {
       ioptions = _parse(e);
       if (ioptions.href) { options.href = ioptions.href; }
    } else if (key === 'href') {
       options.href = value;
    }
    // scaleを追加 ここから
      else if (key === 'scale') {
      options.scale = value;
    }
    // scaleを追加 ここまで
  }
```
##### 2. scale情報からアイコンサイズを拡大させる
0. `attributes` に `scale: true,` を追加
0. `if (ioptions.href) {...}`の中に追記

```js: index.htmlの一部分を抜粋 「if (ioptions.href) {...}付近」
// attributesに scale: true を追加
var attributes = {color: true, width: true, Icon: true, href: true, scale: true, hotSpot: true};

if (ioptions.href) {
    // アイコンサイズを決める　仕様を追加　ここから
    let iconSize = 32; //　とりあえず
    if (ioptions.scale) {
      iconSize = Math.floor(iconSize * parseFloat(ioptions.scale));
      if (isNaN(iconSize)) {
        iconSize = 32;
      }
    }
    // アイコンサイズを決める　仕様を追加　ここまで

    style.icon = new L.KMLIcon({
      iconUrl: ioptions.href,
      shadowUrl: null,
      anchorRef: {x: ioptions.x, y: ioptions.y},
      anchorType: {x: ioptions.xunits, y: ioptions.yunits},
      iconSize: [iconSize, iconSize], // 追加：　アイコンサイズを指定する
      iconAnchor: [iconSize / 2, iconSize / 2], //　追加：　アイコンのセンターを座標に合わせる
    });
  }
```

アイコンサイズが変わりました。
![スクリーンショット 2023-11-17 6.27.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73246/275966a1-e4d3-b79d-5e7f-4c31dc5cc3a2.png)

### 完成したindex.htmlの全容
```html: index.html
<!DOCTYPE html>
<html>
<head>
    <title>Leaflet KML Example</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Leaflet CSS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />

    <!-- Leaflet JavaScript -->
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>

    <!-- Leaflet-KML Plugin -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-plugins/3.4.0/layer/vector/KML.js"></script>
    <script>
        L.KML.parseStyle = function (xml) {
		var style = {}, poptions = {}, ioptions = {}, el, id;

		var attributes = {color: true, width: true, scale:true, Icon: true, href: true, hotSpot: true};

		function _parse (xml) {
			var options = {};
			for (var i = 0; i < xml.childNodes.length; i++) {
				var e = xml.childNodes[i];
				var key = e.tagName;
				if (!attributes[key]) { continue; }
				if (key === 'hotSpot')
				{
					for (var j = 0; j < e.attributes.length; j++) {
						options[e.attributes[j].name] = e.attributes[j].nodeValue;
					}
				} else {
					var value = e.childNodes[0].nodeValue;
					if (key === 'color') {
						options.opacity = parseInt(value.substring(0, 2), 16) / 255.0;
						options.color = '#' + value.substring(6, 8) + value.substring(4, 6) + value.substring(2, 4);
					} else if (key === 'width') {
						options.weight = value;
					} else if (key === 'Icon') {
						ioptions = _parse(e);
						if (ioptions.href) { options.href = ioptions.href; }
					} else if (key === 'href') {
						options.href = value;
					}
					  // scaleを追加
					  else if (key === 'scale') {
						options.scale = value;
					}
				}
			}
			return options;
		}

		el = xml.getElementsByTagName('LineStyle');
		if (el && el[0]) { style = _parse(el[0]); }
		el = xml.getElementsByTagName('PolyStyle');
		if (el && el[0]) { poptions = _parse(el[0]); }
		if (poptions.color) { style.fillColor = poptions.color; }
		if (poptions.opacity) { style.fillOpacity = poptions.opacity; }
		el = xml.getElementsByTagName('IconStyle');
		if (el && el[0]) { ioptions = _parse(el[0]); }
		if (ioptions.href) {
    // アイコンサイズを決める　仕様を追加　ここから
    let iconSize = 32; //　とりあえず
    if (ioptions.scale) {
		console.log(ioptions.scale);
      iconSize = Math.floor(iconSize * parseFloat(ioptions.scale));
      if (isNaN(iconSize)) {
        iconSize = 32;
      }
    }
    // アイコンサイズを決める　仕様を追加　ここまで

    style.icon = new L.KMLIcon({
      iconUrl: ioptions.href,
      shadowUrl: null,
      anchorRef: {x: ioptions.x, y: ioptions.y},
      anchorType: {x: ioptions.xunits, y: ioptions.yunits},
      iconSize: [iconSize, iconSize], // 追加：　アイコンサイズを指定する
      iconAnchor: [iconSize / 2, iconSize / 2], //　追加：　アイコンのセンターを座標に合わせる
    });
  }
		
  id = xml.getAttribute('id');
  if (id && style) {
    style.id = id;
  }

  return style;
};

    </script>
</head>
<body>
    <div id="map" style="height: 400px;"></div>
    <script>
        // Create a Leaflet map
        var map = L.map('map').setView([0, 0], 2); // Set the initial map center and zoom level.

        // Add a base layer (e.g., OpenStreetMap)
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
        }).addTo(map);

        // Load and display a KML file on the map
        var kmlURL = 'sample.kml'; // Replace with the path to your KML file
        var kmlLayer = new L.KML(kmlURL, {
            async: true,
        }).addTo(map);
    </script>
</body>
</html>
```