📍

React(Next.js) と Google Map の Tips

2020/12/05に公開
3

こんにちは!この記事は GameWith アドベントカレンダー5日目の記事です!

https://qiita.com/advent-calendar/2020/gamewith

React(Next.js) で Google Map を表示したり、いろいろ操作をしたのでまとめて行こうと思います

Google Map の表示

https://www.npmjs.com/package/google-map-react

https://github.com/google-map-react/google-map-react

今回利用したのは google-map-react というライブラリです

他にも google-maps-react などといった似たような名前のライブラリなどもありますが、ダウンロード数やメンテナンスの状況などを見て、google-map-react を選びました

表示するだけなら、defaultCenter に表示したい緯度経度を渡せば簡単に表示ができます

import GoogleMapReact from 'google-map-react';

export default function Create() {
  const defaultLatLng = {
    lat: 35.7022589,
    lng: 139.7744733,
  };

  return (
    <div style={{ height: '300px', width: '300px' }}>
      <GoogleMapReact
        bootstrapURLKeys={{ key: process.env.NEXT_PUBLIC_GOOGLE_MAP_KEY }}
        defaultCenter={defaultLatLng}
        defaultZoom={16}
      />
    </div>
  );
}

マーカーの表示

google-map-react ではマーカーを表示する機能はないため、Google Map API を利用しマーカーを表示させます

https://github.com/google-map-react/google-map-react/blob/HEAD/API.md#ongoogleapiloaded-func

まず、Google Map API を利用するために、onGoogleApiLoaded を利用し、map, maps を取得します

https://developers.google.com/maps/documentation/javascript/markers

その後、Marker を利用しマーカーを表示させます

import GoogleMapReact from 'google-map-react';

export default function Create() {
  const defaultLatLng = {
    lat: 35.7022589,
    lng: 139.7744733,
  };

  const handleApiLoaded = ({ map, maps }) => {
    new maps.Marker({
      map,
      position: defaultLatLng,
    });
  };

  return (
    <div style={{ height: '300px', width: '300px' }}>
      <GoogleMapReact
        bootstrapURLKeys={{ key: process.env.NEXT_PUBLIC_GOOGLE_MAP_KEY }}
        defaultCenter={defaultLatLng}
        defaultZoom={16}
        onGoogleApiLoaded={handleApiLoaded}
      />
    </div>
  );
}

地図のクリックと緯度経度の取得

google-map-react にクリックを取れる仕組みがあるので利用します

https://github.com/google-map-react/google-map-react/blob/HEAD/API.md#onclick-func

import GoogleMapReact from 'google-map-react';

export default function Create() {
  const defaultLatLng = {
    lat: 35.7022589,
    lng: 139.7744733,
  };

  const setLatLng = ({ x, y, lat, lng, event }) => {
    console.log(lat);
    console.log(lng);
  };

  return (
    <div style={{ height: '300px', width: '300px' }}>
      <GoogleMapReact
        bootstrapURLKeys={{ key: process.env.NEXT_PUBLIC_GOOGLE_MAP_KEY }}
        defaultCenter={defaultLatLng}
        defaultZoom={16}
        onClick={setLatLng}
      />
    </div>
  );
}

クリックした位置にマーカーを置く

map, maps, marker を保持しておく必要があるので、useState を利用します

クリック時に marker.setMap(null) を実行することで、マーカーが無限に増えていくのを防止します(クリックした箇所全てにマーカーを置きたい場合は実行しないでください)

map.panTo(latLng) は地図の中心を引数の緯度経度に移動させるために実行しています(オプションです

import { useState } from 'react';
import GoogleMapReact from 'google-map-react';

export default function Create() {
  const [map, setMap] = useState(null);
  const [maps, setMaps] = useState(null);
  const [marker, setMarker] = useState(null);

  const defaultLatLng = {
    lat: 35.7022589,
    lng: 139.7744733,
  };

  // map, maps で受け取ると変数が被るので object で受け取っています
  const handleApiLoaded = (object) => {
    setMap(object.map);
    setMaps(object.maps);
  };

  const setLatLng = ({ x, y, lat, lng, event }) => {
    if (marker) {
      marker.setMap(null);
    }
    const latLng = {
      lat,
      lng,
    };
    setMarker(new maps.Marker({
      map,
      position: latLng,
    }));
    map.panTo(latLng);
  };

  return (
    <div style={{ height: '300px', width: '300px' }}>
      <GoogleMapReact
        bootstrapURLKeys={{ key: process.env.NEXT_PUBLIC_GOOGLE_MAP_KEY }}
        defaultCenter={defaultLatLng}
        defaultZoom={16}
        onClick={setLatLng}
        onGoogleApiLoaded={handleApiLoaded}
      />
    </div>
  );
}

複数マーカーの表示&画面内に収まるように表示

先にスクショを貼ります

このように複数マーカーを表示させ、全マーカーが画面内に収まるように自動的に調整し表示させます

https://developers.google.com/maps/documentation/javascript/reference/map#Map.fitBounds

https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLngBounds

LatLngBounds を利用し、マーカー作成後 bounds.extend(marker.position) を実行し、最後に map.fitBounds(bounds) を利用して画面内に収まるように調整させます

import GoogleMapReact from 'google-map-react';

export default function Create() {
  const defaultLatLng = {
    lat: 35.7022589,
    lng: 139.7744733,
  };

  const items = [
    {
      lat: 36.7022589,
      lng: 140.7744733,
    },
    {
      lat: 37.7022589,
      lng: 141.7744733,
    },
    {
      lat: 38.7022589,
      lng: 142.7744733,
    },
  ];

  const handleApiLoaded = ({ map, maps }) => {
    const bounds = new maps.LatLngBounds();
    items.forEach((item) => {
        const marker = new maps.Marker({
          position: {
            lat: item.lat,
            lng: item.lng,
          },
          map,
        });
        bounds.extend(marker.position);
    });
    map.fitBounds(bounds);
  };

  return (
    <div style={{ height: '300px', width: '300px' }}>
      <GoogleMapReact
        bootstrapURLKeys={{ key: process.env.NEXT_PUBLIC_GOOGLE_MAP_KEY }}
        defaultZoom={16}
        defaultCenter={defaultLatLng}
        onGoogleApiLoaded={handleApiLoaded}
      />
    </div>
  );
}

地名で検索

https://developers.google.com/maps/documentation/javascript/geocoding

Geocoding Service を利用して地名の検索をします

Geocoder を作成後、geocode 関数に address を渡して検索を実行します

results の先頭から緯度経度の情報を取り出して map.setCenter を利用し中心に表示し、マーカーを表示させます

export default function Create() {
  const [map, setMap] = useState(null);
  const [maps, setMaps] = useState(null);
  const [geocoder, setGeocoder] = useState(null);
  const [address, setAddress] = useState(null);
  const [marker, setMarker] = useState(null);
  
  const defaultLatLng = {
    lat: 35.7022589,
    lng: 139.7744733,
  };

  const handleApiLoaded = (obj) => {
    setMap(obj.map);
    setMaps(obj.maps);
    setGeocoder(new obj.maps.Geocoder());
  };

  const search = () => {
    geocoder.geocode({
      address,
    }, (results, status) => {
      if (status === maps.GeocoderStatus.OK) {
        map.setCenter(results[0].geometry.location);
        if (marker) {
          marker.setMap(null);
        }
        setMarker(new maps.Marker({
          map,
          position: results[0].geometry.location,
        }));
        console.log(results[0].geometry.location.lat());
        console.log(results[0].geometry.location.lng());
      }
    });
  };

  return (
    <div>
      <input type="text" onChange={(e) => setAddress(e.target.value)} />
      <button type="button" onClick={search}>Search</button>
    </div>
    <div style={{ height: '300px', width: '300px' }}>
      <GoogleMapReact
        bootstrapURLKeys={{ key: process.env.NEXT_PUBLIC_GOOGLE_MAP_KEY }}
        defaultCenter={defaultLatLng}
        defaultZoom={16}
        onGoogleApiLoaded={handleApiLoaded}
      />
    </div>
  )
}

最後に

地図の操作は思ったより楽しいですね

他にも新しい Tips があれば随時追加していこうと思います!

Discussion

宮水宮水

素晴らしい記事をありがとうございます!!大変参考になりました。

1点だけ、大変些細なことで申し訳ないのですが、複数マーカーの緯度経度が全て一緒になっているので、適当な値に直されたらより良いと思います。ご検討よろしくお願いします。

  const defaultLatLng = {
    lat: 35.7022589,
    lng: 139.7744733,
  };

  const items = [
    {
      lat: 35.7022589,
      lng: 139.7744733,
    },
    {
      lat: 35.7022589,
      lng: 139.7744733,
    },
    {
      lat: 35.7022589,
      lng: 139.7744733,
    },
  ];
とめはちとめはち

React に明るくないので原因がわかっていないですが、こちらのサンプルコードを参考にすると、以下の key の所でエラーが発生してしまいますね。

bootstrapURLKeys={{ key: process.env.NEXT_PUBLIC_GOOGLE_MAP_KEY }}

発生しているエラーは下記のようになります。悩ましい。
Next.js のバージョンの違いで起こるのだろうか…

No overload matches this call.
  Overload 1 of 2, '(props: Props | Readonly<Props>): googleMapReact', gave the following error.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'.
  Overload 2 of 2, '(props: Props, context: any): googleMapReact', gave the following error.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'.ts(2769)