Vue 3 + Leafletで画像上に図形を描く



この記事では、Vue 3とLeafletを使って地図や画像上にクリック位置に応じてマーカーやポリゴンを追加する方法を紹介します。マーカーのドラッグによるポリゴンの更新や、頂点の削除機能なども備えています。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3884367/480faad2-4627-d978-77b2-321bc801c1db.gif">



## 準備：環境構築

まず、Leafletをインストールします。

```bash
npm install leaflet
```

次に、Leafletのスタイルを読み込みます。Vueのメインファイル（`main.js`）やこのコンポーネントに以下を追加してください。

```javascript
import 'leaflet/dist/leaflet.css'
```

---

## テンプレート部分

テンプレートには、地図を表示する領域とボタンのコンテナを設定します。

```html
<template>
  <div>
    <!-- 地図表示エリア -->
    <div id="map" class="leaflet-container"></div>

    <!-- ボタンコンテナ -->
    <div class="button-container">
      <!-- 最後に追加したマーカーを削除するボタン -->
      <button @click="removeLastMarker">
        <span>一個前に戻る</span>
      </button>

      <!-- すべてのマーカーとポリゴンを削除するボタン -->
      <button @click="removeAllMarkers">
        <span>クリア</span>
      </button>
    </div>
  </div>
</template>
```

### 詳細解説

1. **`<div id="map" class="leaflet-container">`**  
   Leafletの地図を表示するためのコンテナです。この`id`を使って、Leafletの`L.map`が地図を描画します。

2. **ボタンの設定**  
   - `@click="removeLastMarker"`：Vueのクリックイベントリスナーで、このボタンをクリックすると最後のマーカーを削除する`removeLastMarker`関数が実行されます。
   - `@click="removeAllMarkers"`：こちらのボタンはすべてのマーカーやポリゴンを削除するための`removeAllMarkers`関数が呼び出されます。

---

## スクリプト部分

次に、Leafletの地図オブジェクトやポリゴン、マーカーの管理を行うスクリプト部分について詳しく見ていきます。

### インポートと初期設定

```javascript
<script setup>
import L from 'leaflet' // Leafletライブラリをインポート
import { onMounted, ref } from 'vue' // Vue 3のonMountedとrefをインポート

// 表示する画像のURLを定義
const src = ref(new URL('@/assets/room_living_normal.png', import.meta.url).href)

const map = ref(null) // Leafletの地図オブジェクトを格納
const markers = ref([]) // マーカーのリスト
const vertices = ref([]) // ポリゴンの頂点座標リスト
const polygon = ref(null) // ポリゴンオブジェクト
```

- **`src`**：地図上に表示する画像のパスです。Vueの`@/assets`を使って相対パスを指定しています。
- **`map`、`markers`、`vertices`、`polygon`**：Leafletのオブジェクトやマーカー・ポリゴンの位置を管理するための`ref`です。

---

### カスタムアイコン設定

次に、ポリゴンの頂点に表示されるカスタムアイコンを設定します。

```javascript
const vertexIcon = L.divIcon({
  className: '', // カスタムCSSクラスは指定しない
  html: '<div style="width: 10px; height: 10px; background-color: var(--green-500, #22C55E); border-radius: 50%;"></div>', // 丸い形のカスタムアイコンを指定
  iconSize: [10, 10], // アイコンサイズ
  iconAnchor: [5, 5], // アイコンの基準位置
})
```

- **`html`**：10x10ピクセルの緑色の丸いカスタムアイコンです。これをポリゴンの頂点表示用に使います。
- **`iconSize`**と**`iconAnchor`**：アイコンのサイズを指定し、`iconAnchor`でアイコンの中心を基準位置として配置します。

---

### 地図初期化関数 `initMap`

地図を初期化し、画像をオーバーレイとして追加します。また、地図にクリックイベントを登録します。

```javascript
const initMap = () => {
  const img = new Image() // 画像オブジェクトを生成

  img.onload = () => { // 画像の読み込みが完了したときに実行
    const width = img.width
    const height = img.height
    const bounds = [[0, 0], [height, width]] // 画像の左上から右下までの座標範囲

    map.value = L.map('map', {
      crs: L.CRS.Simple, // シンプルな座標系を使用
      minZoom: -1, // 最小ズームレベル
      maxZoom: 2, // 最大ズームレベル
      center: [height / 2, width / 2], // 地図の中心を画像の中央に設定
      zoom: 0, // 初期ズームレベル
      zoomControl: true, // ズームコントロールを表示
    })

    L.imageOverlay(src.value, bounds).addTo(map.value) // 画像を地図上にオーバーレイ
    map.value.fitBounds(bounds) // 地図の表示範囲を画像に合わせる

    map.value.on('click', onMapClick) // 地図のクリックイベントを設定
  }

  img.src = src.value // 画像の読み込みを開始
}
```

- **`L.map`**：地図オブジェクトを初期化し、画像の大きさに合わせた表示範囲を設定します。
- **`L.imageOverlay`**：画像を地図にオーバーレイとして追加します。
- **`fitBounds`**：地図の表示範囲を画像全体にフィットさせます。

---

### 地図クリックイベント `onMapClick`

地図がクリックされると、クリック位置にマーカーを追加し、ポリゴンの頂点を更新します。

```javascript
const onMapClick = e => {
  const latlng = e.latlng // クリック位置の緯度経度を取得

  if (!polygon.value) {
    polygon.value = L.polygon([latlng], { // 新しいポリゴンを作成
      color: 'var(--green-500, #22C55E)', // 枠線の色
      fillColor: 'rgba(34, 197, 94)', // 塗りつぶしの色
      weight: 2, // 枠線の太さ
    }).addTo(map.value)
    vertices.value.push(latlng) // 頂点リストにクリック位置を追加
  } else {
    const latlngs = polygon.value.getLatLngs()[0] // 既存ポリゴンの頂点リストを取得
    latlngs.push(latlng) // 新しいクリック位置を頂点リストに追加
    polygon.value.setLatLngs([latlngs]) // 更新された頂点リストでポリゴンを再描画
    vertices.value.push(latlng) // 頂点リストにもクリック位置を追加
  }

  addMarker(latlng) // クリック位置にマーカーを追加
}
```

- **ポリゴンの作成**：最初のクリック時にポリゴンが作成され、次以降のクリックで頂点が追加されてポリゴンが拡張されます。
- **`polygon.value.getLatLngs()`**：既存のポリゴン頂点リストを取得し、クリック位置を頂点として追加します。

---

### マーカー追加とドラッグ操作

クリック位置にマーカーを追加し、ドラッグによって頂点を更新します。

```javascript
const addMarker = latlng => {
  const marker = L.marker(latlng, { icon: vertexIcon, draggable: true }).addTo(map.value)
  marker.on('drag', onMarkerDrag) // ドラッグイベントを登録
  markers.value.push(marker) // マーカーを

リストに追加
}

const onMarkerDrag = e => {
  const marker = e.target
  const latlng = marker.getLatLng()
  const latlngs = polygon.value.getLatLngs()[0]
  const index = markers.value.indexOf(marker)

  if (index !== -1) {
    latlngs[index] = latlng // ドラッグ位置を頂点リストに反映
    vertices.value[index] = latlng // 頂点リストを更新
    polygon.value.setLatLngs([latlngs]) // ポリゴンを再描画
  }
}
```

- **`L.marker`**：クリック位置にカスタムアイコンのマーカーを作成し、`draggable`を`true`にしてドラッグ可能にします。
- **`onMarkerDrag`**：ドラッグ操作でマーカー位置が変更された際に、ポリゴンの頂点も更新されます。

---

### マーカー削除処理

最後に追加したマーカーや全マーカーを削除する関数を定義します。

```javascript
const removeLastMarker = () => {
  if (markers.value.length > 0) {
    const lastMarker = markers.value.pop() // 最後のマーカーを取得し削除
    map.value.removeLayer(lastMarker) // マーカーを地図から削除
    const latlngs = polygon.value.getLatLngs()[0]
    latlngs.pop() // 最後の頂点も削除
    vertices.value.pop()
    if (latlngs.length > 0) {
      polygon.value.setLatLngs([latlngs]) // ポリゴンを再描画
    } else {
      map.value.removeLayer(polygon.value) // 頂点がなくなればポリゴンも削除
      polygon.value = null
    }
  }
}

const removeAllMarkers = () => {
  if (polygon.value) {
    map.value.removeLayer(polygon.value) // ポリゴンが存在する場合削除
    polygon.value = null
  }
  markers.value.forEach(marker => map.value.removeLayer(marker)) // 全マーカーを削除
  markers.value = [] // マーカーリストをクリア
  vertices.value = [] // 頂点リストをクリア
}
</script>
```

- **`removeLastMarker`**：最後に追加したマーカーとポリゴンの頂点を削除します。
- **`removeAllMarkers`**：すべてのマーカーとポリゴンを削除し、地図をリセットします。

---

## スタイル部分

最後に、地図の見た目やボタンの配置を設定します。

```css
<style scoped>
.leaflet-container {
  width: 50%;
  height: 600px;
}

</style>
```

- **`.leaflet-container`**：地図の表示領域を定義し、地図のサイズや枠線を設定します。

---

以上で、Vue 3とLeafletを使用して、地図上にマーカーやポリゴンを動的に追加・削除する方法が完成です。Leafletの柔軟な機能を使って、さらに多彩なインタラクションを追加することも可能です。

## 参考記事
[Leafletの使い方](https://ktgis.net/service/leafletlearn/index.html#step8)
[Leafletの図形描画](https://blog.dpto.jp/leaflet/leaflet-figure/)