ReactでMapboxを使ってみる

こんにちは！実は、はじめての技術記事執筆です...！ 

今回は、Mapbox Advent Calendar 2024 として、Mapbox を触ってみたレポートを書きます💫

## Mapbox とは

地図や位置情報に関連するサービスやアプリケーションを開発するためのプラットフォームを提供する企業および技術スタックの名称です。
技術としては、OpenStreetMap を内部的に利用しています。

## 背景

今回 Mapbox という技術に触れるきっかけになったのは、ちょうどプロジェクトで地図を用いたサービスを開発することになったためです。

私は「福岡未踏的人材発掘・育成コンソーシアム」というコミュニティでクリエータとして活動しており、現在は株式会社 Fusic の「自治体のお困りごと可視化マップ」というプロダクトの開発に携わっています。

- [福岡未踏についてはこちら](https://mitou-fukuoka.org/)
- [私が開発しているプロダクト「自治体のお困りごと可視化マップ」](https://mitou-fukuoka.org/works/solve-city-share-map/)

## 概要

今回やったことは以下のとおりです。

- `react-map-gl`を通じて、マップを表示する
- `Geocoding API v5` を利用して、リバース・ジオコーディングしてみる

では、ひとつずつご紹介します１

## 環境

| name                 | version |
| -------------------- | ------- |
| React                | 18      |
| Typescript           | 5       |
| Next.js (App router) | 14.2.5  |
| react-map-gl         | 7.1.7   |
| Chakra UI            | 2.8.2   |

## 実装

### 技術選定

Mapbox を Javascript で利用するためには、Mapbox 公式が提供しているライブラリである[Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/ja/guides/)があります。

ただ、React のコードを書くとなると、Vanilla な Javascript のコードを参照するのは大変なところがあります。

そこで今回は（楽するために）、Mapbox GL JS をオーバーラップしたサードパーティ製のライブラリである`react-gl-js`を利用します。

### 仕様設計

今回、以下のような仕様を満たす画面を実装します。

- SPA のうちの 1 つの画面（＝前後の画面へ遷移するためのボタンがある）
- 地図が表示されており、地図上の場所をタップするよう案内がある
- 地図上の場所をタップすると、地図の下に、タップした場所の住所が表示される

実装にあたり参考にさせていただいた記事は以下です。
ただ、記事の内容だと自分の環境でうまく動作しなかったので、一部修正しています。

[react-map-gl を使ってみる](https://zenn.dev/mapbox_japan/articles/348b8e229aaab5)

### コーディング

#### 準備

事前に以下を行っておきます。

- Mapbox アカウントの作成とアクセストークンの取得
- Next.js プロジェクトの作成とパッケージのインストール

今回、自分は以下のパッケージを利用しています（`package.json`から確認）。

- `yarn add react-map-gl@7.1.7`
- `yarn add @types/mapbox-gl@3.4.0`
- `yarn add mapbox-gl@3.7.0`

#### 地図を表示する

まず、地図を表示します。

```tsx
<Map
  initialViewState={initialViewState}
  mapboxAccessToken={accessToken}
  mapLib={mapboxgl as any}
  mapStyle="mapbox://styles/mapbox/light-v11"
  style={{ height: "100%", width: "100%" }}
></Map>
```

ここで、Props として初期スタイル、アクセストークン、ライブラリ、スタイルを渡しています。

```typescript
import mapboxgl from "mapbox-gl"; // mapbox-gl-js をインポート

const accessToken = Env.mapboxAccessToken; // 環境変数から取得
const initialViewState = {
  latitude: 35.6895,
  longitude: 139.6917,
  zoom: 14,
};
```

すると、東京都庁を中心とする白基調の地図が表示されます。

![1_Basic-map.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825503/bd9aeb89-2bde-8cfa-26e6-2807963302a4.png)

もしここで、お気に入りのスタイルを見つけたければ、[Styles API](https://docs.mapbox.com/api/maps/styles/)を参照してみてください。

#### クリックした場所の住所を取得する

地図の任意の場所をクリックすると、`event: mapboxgl.MapMouseEvent`が発生します。この中に、クリックした場所の緯度経度情報が含まれているので、これを利用して住所を取得します。

緯度経度から住所を取得するためには、リバース・ジオコーディングを行う必要があります。今回は Mapbox の [Geocoding API v5](https://docs.mapbox.com/api/search/geocoding-v5/) を利用します。

まずは、API を叩く処理を実装します。たとえば、以下では、緯度経度を受け取って住所を返すカスタムフックを作成しています。

```typescript
import { useState } from "react";

import { Env } from "@/config/env";

export const useGetAddress = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchAddress = async ({ lat, lon }: { lat: number; lon: number }) => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(
        `https://api.mapbox.com/geocoding/v5/mapbox.places/${lon},${lat}.json?access_token=${Env.mapboxAccessToken}&language=ja`
      );
      const result = await response.json();
      if (result.features && result.features.length > 0) {
        return result.features[0].place_name;
      }
      return "住所が見つかりません";
    } catch (err) {
      setError("住所を取得できませんでした");
      return "住所を取得できませんでした";
    } finally {
      setIsLoading(false);
    }
  };

  return { error, fetchAddress, isLoading };
};
```

以下では、いま実装したカスタムフックを呼び出しています。
また、今回 SPA で実装しているので、Context API を利用して、緯度経度を`formData`としてグローバルに保持しています。

```typescript
const { formData, setFormData } = usePostContext();
const location = formData.location;
const address = formData.address;

const { error, isLoading: loading, fetchAddress } = useGetAddress();
```

地図をクリックした時の処理を担うハンドラーを実装します。

```typescript
const handleMapClick = async (event: mapboxgl.MapMouseEvent) => {
  const { lng, lat } = event.lngLat;
  setFormData({ location: { lat, lng } });
  const address = await fetchAddress({ lat, lon: lng });
  setFormData({ address });
};
```

最後に、ハンドラーの呼びだし、クリック時のマーカー表示、住所表示を実装します。
`Map`コンポーネントに、いま作成したハンドラーを渡しています。
`Marker`コンポーネントを利用して、クリックした場所にマーカーを表示します。

```tsx
<Box
  border="1px solid lightgray"
  borderRadius="md"
  height="400px"
  mt="4"
  width="100%"
>
  <Map
    initialViewState={initialViewState}
    mapboxAccessToken={accessToken}
    mapLib={mapboxgl as any}
    mapStyle="mapbox://styles/mapbox/light-v11"
    style={{ height: '100%', width: '100%' }}
    onClick={handleMapClick}
  >
    {location && (
      <Marker
        color="red"
        latitude={location.lat}
        longitude={location.lng}
      />
    )}
  </Map>
</Box>
<Box mt="2">
  住所: {loading ? '読み込み中...' : address || error || '未選択'}
</Box>
```

すると、以下のように表示されます。

![2_Fetch-address.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825503/8bad98a7-d289-9eaa-2369-1ecf67ff5f01.gif)


画像に示すように、地図をクリックすると、クリックした場所の住所が表示されるようになりました。
住所の表示（リバース・ジオコーディング）も、1 秒以下で表示されるので、ユーザーにとってもストレスなく使えると思います。

### まとめ

今回は、Next.js (Typescript)で Mapbox を使って、地図を表示したり、クリックした場所の住所を取得したりする方法を紹介しました。
個人的には、このように Typescript で Mapbox を使っているようなドキュメントや記事が少なく、型エラーへの対処に苦労しました。
しかしながら、Mapbox は非常に使いやすく、地図を表示するだけでなく、様々な機能を提供しているので、今後もっといろんな機能を触ってみようと思います。

ここまで読んでいただき、ありがとうございました！

## 引用

### 公式

- [Mapbox](https://www.mapbox.jp/)
- [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/ja/guides/)
- [react-map-gl](https://visgl.github.io/react-map-gl/)
- [Mapbox Styles API](https://docs.mapbox.com/api/maps/styles/)
- [Mapbox Geocoding API v5](https://docs.mapbox.com/api/search/geocoding-v5/)

### 記事

- [react-map-gl を使ってみる](https://zenn.dev/mapbox_japan/articles/348b8e229aaab5)
- [私が開発しているプロダクト「自治体のお困りごと可視化マップ」](https://mitou-fukuoka.org/works/solve-city-share-map/)