TypeScript + Reactを基本からまとめてみた【3】【Setting up Google Maps】

## 環境の準備

google api keyを取得する

Google Cloud Platformへ行き、コンソールをクリックする。

プロジェクトを作成する。


Create React Appで新しいReactアプリを作成する

```
npx create-react-app <プロジェクト名> --template typescript
```

## 使用しないファイルを削除

```
App.test.tesx
logo.svg
servicsWorker.ts
reportWebVitals.ts
```

## 環境構築

```
$ npm install node-sass
$ npm install --save-dev @types/googlemaps
```


## コンポーネント・ファイル構成

```
  src
   ├── Map
       ├── index.ts
       ├── Map.scss
       └── Map.tsx
   ├── utils
        └── GoogleMapsUtils.ts
   ├── App.css
   ├── App.tsx
   ├── index.tsx
   └── react-app-env.d.ts
├── .env
```

```src/Map/index.ts
export { default } from "./Map";
```

```src/Map/Map.scss
.map-container {
  display: flex;
  justify-content: center;
  padding: 1rem;

  &__map {
    height: 60vh;
    width: 100%;
  }
}
```

```src/Map/Map.tsx
import React, { useEffect, useRef, useState } from 'react';
import './Map.scss';

interface IMap {
  mapType: google.maps.MapTypeId;
  mapTypeControl?: boolean;
  setDistanceInKm: React.Dispatch<React.SetStateAction<number>>;
}

interface IMarker {
  address: string;
  latitude: number;
  longitude: number;
}

type GoogleLatLng = google.maps.LatLng;
type GoogleMap = google.maps.Map;
type GoogleMarker = google.maps.Marker;
type GooglePolyline = google.maps.Polyline;

const Map: React.FC<IMap> = ({
  mapType,
  mapTypeControl = false,
  setDistanceInKm,
}) => {
  const ref = useRef<HTMLDivElement>(null);
  const [map, setMap] = useState<GoogleMap>();
  const [marker, setMarker] = useState<IMarker>();
  const [homeMarker, setHomeMarker] = useState<GoogleMarker>();
  const [googleMarkers, setGoogleMarkers] = useState<GoogleMarker[]>([]);
  const [listenerIdArray, setListenerIdArray] = useState<any[]>([]);
  const [LastLineHook, setLastLineHook] = useState<GooglePolyline>();

  const startMap = (): void => {
    if (!map) {
      defaultMapStart();
    } else {
      const homeLocation = new google.maps.LatLng(65.166013499, 13.3698147);
      setHomeMarker(addHomeMarker(homeLocation));
    }
  };
  useEffect(startMap, [map]);

  const defaultMapStart = (): void => {
    const defaultAddress = new google.maps.LatLng(65.166013499, 13.3698147);
    initMap(4, defaultAddress);
  };

  const initEventListener = (): void => {
    if (map) {
      google.maps.event.addListener(map, 'click', function (e) {
        coordinateToAddress(e.latLng);
      });
    }
  };
  useEffect(initEventListener, [map]);

  const coordinateToAddress = async (coordinate: GoogleLatLng) => {
    const geocoder = new google.maps.Geocoder();
    await geocoder.geocode(
      { location: coordinate },
      function (results, status) {
        if (status === 'OK') {
          setMarker({
            address: results[0].formatted_address,
            latitude: coordinate.lat(),
            longitude: coordinate.lng(),
          });
        }
      }
    );
  };

  useEffect(() => {
    if (marker) {
      addMarker(new google.maps.LatLng(marker.latitude, marker.longitude));
    }
  }, [marker]);

  const addMarker = (location: GoogleLatLng): void => {
    const marker: GoogleMarker = new google.maps.Marker({
      position: location,
      map: map,
      icon: getIconAttributes('#000000'),
    });

    setGoogleMarkers((googleMarkers) => [...googleMarkers, marker]);

    const listenerId = marker.addListener('click', () => {
      const homePos = homeMarker?.getPosition();
      const markerPos = marker.getPosition();
      if (homePos && markerPos) {
        const distanceInMeters =
          google.maps.geometry.spherical.computeDistanceBetween(
            homePos,
            markerPos
          );
        setDistanceInKm(Math.round(distanceInMeters / 1000));

        if (LastLineHook) {
          LastLineHook.setMap(null);
        }

        const line = new google.maps.Polyline({
          path: [
            { lat: homePos.lat(), lng: homePos.lng() },
            { lat: markerPos.lat(), lng: markerPos.lng() },
          ],
          icons: [
            {
              icon: {
                path: google.maps.SymbolPath.FORWARD_OPEN_ARROW,
              },
              offset: '100%',
            },
          ],
          map: map,
        });

        setLastLineHook(line);
      }
    });

    setListenerIdArray((listenerIdArray) => [...listenerIdArray, listenerId]);
  };

  useEffect(() => {
    listenerIdArray.forEach((listenerId) => {
      google.maps.event.removeListener(listenerId);
    });

    setListenerIdArray([]);
    setGoogleMarkers([]);
    googleMarkers.forEach((googleMarker) => {
      const markerPosition = googleMarker.getPosition();
      if (markerPosition) {
        addMarker(markerPosition);
      }
    });
  }, [LastLineHook]);

  const addHomeMarker = (location: GoogleLatLng): GoogleMarker => {
    const homeMarkerConst: GoogleMarker = new google.maps.Marker({
      position: location,
      map: map,
      icon: {
        url: window.location.origin + '/assets/images/homeAddressMarker.png',
      },
    });

    homeMarkerConst.addListener('click', () => {
      map?.panTo(location);
      map?.setZoom(6);
    });

    return homeMarkerConst;
  };

  const getIconAttributes = (iconColor: string) => {
    return {
      path: 'M11.0639 15.3003L26.3642 2.47559e-05L41.6646 15.3003L26.3638 51.3639L11.0639 15.3003 M22,17.5a4.5,4.5 0 1,0 9,0a4.5,4.5 0 1,0 -9,0Z',
      fillColor: iconColor,
      fillOpacity: 0.8,
      strokeColor: 'pink',
      strokeWeight: 2,
      anchor: new google.maps.Point(30, 50),
    };
  };

  const initMap = (zoomLevel: number, address: GoogleLatLng): void => {
    if (ref.current) {
      setMap(
        new google.maps.Map(ref.current, {
          zoom: zoomLevel,
          center: address,
          mapTypeControl: mapTypeControl,
          streetViewControl: false,
          rotateControl: false,
          scaleControl: true,
          fullscreenControl: false,
          panControl: false,
          zoomControl: true,
          gestureHandling: 'cooperative',
          mapTypeId: mapType,
          draggableCursor: 'pointer',
        })
      );
    }
  };

  return (
    <div className='map-container'>
      <div ref={ref} className='map-container__map'></div>
    </div>
  );
};

export default Map;
```
```src/utils/GoogleMapsUtils.ts
export const loadMapApi = () => {
  const mapsURL = `https://maps.googleapis.com/maps/api/js?key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}&libraries=geometry,places&language=no&region=NO&v=quarterly`;
  const scripts = document.getElementsByTagName('script');
  // 既存のスクリプトタグを調べ、見つかったらgoogle mapのapiタグを返します。
  for (let i = 0; i < scripts.length; i++) {
    if (scripts[i].src.indexOf(mapsURL) === 0) {
      return scripts[i];
    }
  }

  const googleMapScript = document.createElement('script');
  googleMapScript.src = mapsURL;
  googleMapScript.async = true;
  googleMapScript.defer = true;
  window.document.body.appendChild(googleMapScript);

  return googleMapScript;
};
```
```src/App.css
.distance-info {
  margin: 1rem;
  padding: 1rem;
  font-size: 1.5em;
  background: #7fdbff;
  border-radius: 2rem;
}
```
```src/App.tsx
import React, { useEffect, useState } from 'react';
import './App.css';
import Map from './Map/Map';
import { loadMapApi } from './utils/GoogleMapsUtils';

function App() {
  const [scriptLoaded, setScriptLoaded] = useState(false);
  const [distanceInKm, setDistanceInKm] = useState<number>(-1);

  useEffect(() => {
    const googleMapScript = loadMapApi();
    googleMapScript.addEventListener('load', function () {
      setScriptLoaded(true);
    });
  }, []);

  const renderDistanceSentence = () => {
    return (
      <div className='distance-info'>
        {`Distance between selected marker and home address is ${distanceInKm}km.`}
      </div>
    );
  };

  return (
    <div className='App'>
      {scriptLoaded && (
        <Map
          mapType={google.maps.MapTypeId.ROADMAP}
          mapTypeControl={true}
          setDistanceInKm={setDistanceInKm}
        />
      )}
      {distanceInKm > -1 && renderDistanceSentence()}
    </div>
  );
}

export default App;
```
```src/index.css
body {
  margin: 0;
}
```
```src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
```
```.env
REACT_APP_GOOGLE_MAPS_API_KEY=
```



## 参考サイト

[Setting up Google Maps with React + Typescript (Part 1)](https://www.youtube.com/watch?v=fMsAPI7de0A)
[Creating Markers on Google Maps with React + Typescript (Part 2)](https://www.youtube.com/watch?v=Id2NtV3TcMQ&t=938s)
[Google Maps APIを使ってみよう](https://www.youtube.com/watch?v=KZ5yC9pCm_Q&t=210s)
[【WordPress】Google Maps Platform APIキーの取得方法【注意点も解説】](https://www.youtube.com/watch?v=e6Fv0HMQPyo)