Leafletで地図以外の画像を取り扱う

適当な画像をLeafletで表示してみます。画像はコレを使用します。


https://www.pakutaso.com/20200556146post-25030.html

## 画像をタイル状に変換
### image-map-tilesのインストール
拡大縮小出来るようにするには、各ズーム倍率でタイル状にする必要があります。手作業ではやってられないので下記ツールを使用します。

https://www.npmjs.com/package/image-map-tiles

古くてGitHubのリンクも切れてる上、現状そのままで動かないのでパッチを当てます。

```bash
mkdir image-map-tiles
cd image-map-tiles
yarn init
echo nodeLinker: node-modules> .yarnrc.yml
```

patch-packageをインストールして、パッチファイルを配置。

```bash
yarn add -D patch-package postinstall-postinstall
```

package.json に追記。

```package.json
  "scripts": {
    "postinstall": "patch-package"
  },
```

patches フォルダを作成し、パッチファイル image-map-tiles+1.0.0.patch を配置。

```patches/image-map-tiles+1.0.0.patch
diff --git a/node_modules/image-map-tiles/index.js b/node_modules/image-map-tiles/index.js
index a18cae3..5d7c417 100644
--- a/node_modules/image-map-tiles/index.js
+++ b/node_modules/image-map-tiles/index.js
@@ -174,7 +174,7 @@ module.exports = function(imagePath, options) {
 			console.log(zoomPath, tileWidth, tileHeight);
 			console.log(metadata.width, metadata.height);
 
-			var image = sharp(buffer);
+			//var image = sharp(buffer);
 
 			var num_columns = Math.ceil(metadata.width / tileWidth);
 			var num_rows = Math.ceil(metadata.height / tileHeight);
@@ -218,6 +218,7 @@ module.exports = function(imagePath, options) {
 					if (err) {
 						console.log(err);
 					} else {
+						var image = sharp(buffer);
 						image
 							.extract({ left: crop.x, top: crop.y, width: options.tileWidth, height: options.tileHeight })
 							.toFile(tilePath + crop.row + '.jpg', function(err){
@@ -254,7 +255,7 @@ module.exports = function(imagePath, options) {
 
 			thumbnail
 			.resize(250, 250)
-			.crop(sharp.gravity.center)
+			//.crop(sharp.gravity.center)
 			.toFile(outputDir + 'thumbnail.jpg', function(err){
 				if (err) {
 					console.log('Thumbnail Error', err);
```

パッチの準備が出来たので、image-map-tiles をインストールする。インストール時に自動的にパッチがあたる。

```bash
yarn add -D image-map-tiles
```

### 画像をタイル状に変換する
入出力用のフォルダ input, output を作成し、inputに変換したい画像を入れておく。
入出力用のフォルダと同じ階層に main.js を作成する。

```main.js
const imageMapTiles = require('image-map-tiles');

let options = {
  outputDir: "./output",
  zoom: 4,
  tileHeight: 256,
  tileWidth: 256,
};

imageMapTiles("./input/AMEMAN1222044.jpg", options);
```

実行すると、outputフォルダに変換された画像が出来る。

```bash
node main.js
```

## Leaflet上で表示する
タイル状になった画像を public フォルダに格納しておきます。
![スクリーンショット 2024-06-23 202230.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622558/efd9f41c-d2ef-3591-8bcd-252dfabdbde3.png)

### マップコンポーネント
スタイルシートが無いと表示されないので作成する。

```customMap.css
/* 地図を画面全体に表示 */
.leaflet-container {
  width: 100vw;
  height: 100vh;
}

/* 通常カーソル */
.leaflet-grab {
  cursor: auto;
}
```

マップコンポートネントを作成。

```customMap.tsx
'use client';

import { default as L } from 'leaflet';
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
import markerIcon from 'leaflet/dist/images/marker-icon.png';
import markerIconShadow from 'leaflet/dist/images/marker-shadow.png';
import 'leaflet/dist/leaflet.css';
import { useEffect, useLayoutEffect, useState } from 'react';
import './customMap.css';

const DefaultIcon = L.icon({
  iconUrl: markerIcon.src,
  iconRetinaUrl: markerIcon2x.src,
  shadowUrl: markerIconShadow.src,
  iconAnchor: [12, 41], // アイコンのオフセット
  popupAnchor: [0, -32], // ポップアップのオフセット
});
L.Marker.prototype.options.icon = DefaultIcon;

export default function CustomMap() {
  const [layoutLoad, setLayoutLoad] = useState(false);

  useLayoutEffect(() => {
    return setLayoutLoad(true); // マップの再初期化を防ぐ
  }, [layoutLoad]);

  useEffect(() => {
    if (!layoutLoad) return; // マップの再初期化を防ぐ

    const map = L.map('map', {
      crs: L.CRS.Simple,
      maxZoom: 4,
    });
    map.setView(L.latLng(-256, 256), 0); // 初期位置

    L.tileLayer('map/{z}/{x}/{y}.jpg', {
      attribution:
        '© <a href="https://www.pakutaso.com/20200556146post-25030.html">ぱくたそ</a>',
    }).addTo(map);

    // マーカー
    L.marker(L.latLng(-512, 512), {
      draggable: true,
    }).addTo(map);
  }, [layoutLoad]);

  return (
    <div className="leaflet-container">
      <div id="map"></div>
    </div>
  );
}
```

### ページ
作成したマップコンポーネントを読み込むわけですが、クライアントサイドでしか動かないので、ダイナミックインポートする。

```page.tsx
'use client';

import dynamic from 'next/dynamic';
import { useMemo } from 'react';

export default function Home() {
  const CustomMap = useMemo(
    () =>
      dynamic(() => import('./CustomMap'), {
        loading: () => <p>map loading...</p>,
        ssr: false,
      }),
    []
  );

  return <CustomMap />;
}
```

## 実行する
実行して http://localhost:3000/custommap にアクセスするとこんな感じに表示されます。

![image-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622558/d01436ee-7995-6fa7-b2be-9ed98e222f75.jpeg)