Google Maps JavaScript APIと天気APIでクリックした地点の天気を表示してみる


![スクリーンショット 2022-12-06 9.00.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2321549/c9528095-261e-1862-dd1b-25a6912492f2.png)

# 概要
「急に予定が空いたからどこかに出かけようかな〜！
　でも、お出かけ先の今の天気はどうかな？」

なんてシチュエーション、あると思います。
そんな時に役立つ（？）アプリを一から作ってみましょう！

Googleの「Maps JavaScript API」と無料天気APIを提供するOpen-Meteoの「Weather Forecast API」を組み合わせて、地図上のクリックした地点の現在の天気を取得してみる記事です。

※「Maps JavaScript API」の使用にはユーザー登録が必須で、従量課金制になります。
ただ、このAPIに関しては2022/12/6現在、毎月$200の無料利用枠があります。
この記事の内容を試してみる程度であればその枠内で収まると思いますが、ご認識の上お試しください。詳細は[こちら](https://mapsplatform.google.com/intl/ja/pricing/)から確認できます。


# 使用するもの
- React
- Google 「Maps JavaScript API」
- Open-Meteo 「Weather Forecast API」


## 使い方
完成したアプリの機能は至ってシンプルです。
地図のどこかの地点をクリックすると、クリックした地点の現在の天気[^1]を表示します。

実際の動きとしては、
①地図上をクリックするとピンが立ち、、、
![スクリーンショット 2022-12-06 9.09.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2321549/d088000d-a4ce-f8f4-5984-03d72cf60493.png)

②画面右側にその地点の現在の天気[^1]、気温、風向きと風速が表示されます。
<img width="300" alt="スクリーンショット 2022-12-06 9.09.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2321549/c3660c00-4ce3-dc33-1fc7-8c1cdad64fca.png">

動くとこんな感じになります。
![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2321549/c01d8502-957d-5815-970c-854c1e894689.gif)



シンプルですね！


[^1]: 「Weather Forecast API」で取得できる現在の天気は毎時0分に更新されます。
　例）9:55にリクエストすると9:00のデータが返却されます。

## 作り方
以下の流れで実装していきます。
1. [セットアップ](#セットアップ)
1. [API Keyの用意](#api-keyの用意)
1. [地図表示を実装](#地図表示を実装)
1. [天気表示を実装](#天気表示を実装)

元のソースは[こちら](https://github.com/reo-morisawa/map-click-weather)にあります。
とりあえず動かしてみたい方はcloneして実行してみてください。

※cloneして試す場合でもAPI Keyはご自身で用意いただく必要があります。


### セットアップ
まず環境を整えていきます。
`create-react-app`コマンドを実行します。

```
npx create-react-app my-map-weather-app
```

続いて「Maps JavaScript API」のReact向けnpmパッケージをインストール。

```
npm install @googlemaps/react-wrapper
```

### API Keyの用意

Googleの「Maps JavaScript API」を使用しますので、ユーザー登録、API Keyの取得が必要になります。
ユーザー登録していない場合は、[Google Cloud Platformのページ](https://cloud.google.com/free?utm_source=google&utm_medium=cpc&utm_campaign=japac-JP-all-ja-dr-bkws-all-all-trial-e-dr-1009882&utm_content=text-ad-none-none-DEV_c-CRE_540744517917-ADGP_Hybrid%20%7C%20BKWS%20-%20EXA%20%7C%20Txt%20~%20GCP%20~%20General_google-KWID_43700036901554420-kwd-523627957116&userloc_1009280-network_g&utm_term=KW_google%20%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0&gclid=Cj0KCQiAyracBhDoARIsACGFcS7Q5Ik92KzWXbSXgt_jPpUqm3hdtla000qjD49kZlkIdibOZO-dQMMaAnlAEALw_wcB&gclsrc=aw.ds)からユーザー登録します。
画面右上の「無料で利用開始」から登録できます。
※登録の際、クレジットカード番号も必要になります。
![スクリーンショット 2022-12-06 13.18.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2321549/f3f14d5e-816e-c85f-af38-49569c2cb5c3.png)

登録が完了したら、API Keyを発行します。発行の仕方についてはこちらの記事がわかりやすかったです。


[API Keyを発行する手順 for Google Cloud API](https://zenn.dev/tmitsuoka0423/articles/get-gcp-api-key)

なお、今回は「Maps JavaScript API」を使うので、「Maps JavaScript API」を選択してAPI Keyを発行してください。


API Keyを発行できたら、ソースコードのルート直下に`.env.local`を作り、以下のように書きます。
API Keyは`[発行したAPI Key]`の部分にコピペしてください。

```.env.local
REACT_APP_MAP_API_KEY=[発行したAPI Key]
```


### 地図表示を実装
環境が整ったので実装に入っていきます。
地図表示を実装します。

地図表示には「Maps JavaScript API」のReact向けnpmパッケージを使用します。
（[公式ドキュメント](https://developers.google.com/maps/documentation/javascript/react-map)）


まず、`src/`配下に`components/`ディレクトリを作成し、その配下に`Map.js`を作ります。
こちらで地図本体の実装を行います。
地図を生成し、`onClick`などのイベントも登録していきます。

```react:src/components/Map.js
import { useState, useEffect, useRef, Children, isValidElement, cloneElement } from 'react';

export const Map = ({ center, zoom, onClick, children }) => {
  const ref = useRef(null);
  const [ map, setMap ] = useState();

  useEffect(() => {
    if (ref.current && !map) {
      setMap(new window.google.maps.Map(ref.current, { center, zoom }));
    }
  }, [ ref, map ]);

  useEffect(() => {
    if (map) {
      window.google.maps.event.clearListeners(map, 'click')

      if (onClick) {
        map.addListener('click', onClick);
      }
    }
  }, [ map, onClick ]);

  return <>
    <div className='map' ref={ref} />
    {Children.map(children, (child) => {
        if (isValidElement(child)) {
          // childコンポーネントにmap propsを渡す
          return cloneElement(child, { map });
        }
      })}
  </>;
}
```

ちなみに以下の部分では、childコンポーネントに`map`変数を`props`として渡しています。

```react
{Children.map(children, (child) => {
  if (isValidElement(child)) {
    // childコンポーネントにmap propsを渡す
    return cloneElement(child, { map });
  }
})}
```

同じく`components/`配下に`Marker.js`を作ります。
こちらでは地図クリック時に表示する赤いピンを実装します。

```react:src/components/Marker.js
import { useState, useEffect } from 'react';

export const Marker = (options) => {
  const [ marker, setMarker ] = useState(null);

  useEffect(() => {
    if (!marker) {
      setMarker(new window.google.maps.Marker());
    }

    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [ marker ]);

  useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [ marker, options ]);

  return null;
};
```

App.jsを編集していきます。既に生成されているのですが、丸ごと以下に書き換えてください。

```react:src/App.js
import './App.css';
import { Wrapper } from "@googlemaps/react-wrapper";
import { useState } from 'react';
import { Map } from './components/Map';
import { Marker } from './components/Marker';

// デフォルトで表示する座標（東京駅）
const POS_TOKYO_STATION = { lat: 35.681, lng: 139.767 };

const App = () => {
  const [ markerPosition, setMarkerPosition ] = useState(null);

  const onClick = async (e) => {
    const pos = { lat: e.latLng.lat(), lng: e.latLng.lng() };
    setMarkerPosition(pos);
  }

  return (
    <Wrapper apiKey={process.env.REACT_APP_MAP_API_KEY}>
      <Map center={POS_TOKYO_STATION} zoom={14} onClick={onClick}>
        <Marker position={markerPosition} />
      </Map>
    </Wrapper>
  );
}

export default App;
```

以下の`Wrapper`に[API Keyの用意](#api-keyの用意)で設定したAPIキーが入ってきます。
また、`Map`の中に`Marker`があるので、`Marker`は`Map`のchildコンポーネントになります。
つまり`Map`にて`map`変数を`props`として渡している先は`Marker`になります。

```react
<Wrapper apiKey={process.env.REACT_APP_MAP_API_KEY}>
  <Map center={POS_TOKYO_STATION} zoom={14} onClick={onClick}>
    <Marker position={markerPosition} />
  </Map>
</Wrapper>
```

cssです。既にデフォルト画面用のcssが書き込まれていますが必要ないので全て消し、以下に書き換えます。

```css:src/App.css
.map {
  width: 100vw;
  height: 100vh;
}
```

これで一旦地図機能の実装は完了です。
以下コマンドを実行してみましょう。
```
npm start
```

画面いっぱいに地図が表示され、地点をクリックすると赤いピンが立てばOKです。

### 天気表示を実装
ここからいよいよ天気の表示部を実装します。

天気APIには、Open-Meteoの「Weather Forecast API」を使用します。
このAPIは無料で、API Keyも必要ないというサービス精神の塊みたいなAPIで、誰でもアクセスできます。
[公式ドキュメント](https://open-meteo.com/en/docs#latitude=35.69&longitude=139.69&hourly=precipitation&daily=temperature_2m_max,temperature_2m_min&current_weather=true&timezone=Asia%2FTokyo)で取得したいパラメータや条件を選ぶと、選んだ条件やパラメータに応じたリクエストURLが赤枠部分に表示されますので、色々試してみると面白いです！
![スクリーンショット 2022-12-06 15.06.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2321549/ae5dd91b-7e94-3800-ec32-1e2974251c8e.png)

では実装していきます。
App.jsに追記していきます。

```react:src/App.js
import ...
/* ↓ここを追加↓ */
import { WeatherDisplay } from './components/WeatherDisplay';

...

const App = () => {
  const [ markerPosition, setMarkerPosition ] = useState(null);
  /* ↓ここを追加↓ */
  const [ currentWeather, setCurrentWeather ] = useState(null);

  /* ↓ここを追加↓ */
  const getWeather = async ({ lat, lng }) => {
    const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current_weather=true&timezone=Asia%2FTokyo`);
    return response ? response.json() : null;
  }
  /* ↑ここを追加↑ */

  const onClick = async (e) => {
    const pos = { lat: e.latLng.lat(), lng: e.latLng.lng() };
    setMarkerPosition(pos);
    /* ↓ここを追加↓ */
    const weather = await getWeather({ lat: pos.lat, lng: pos.lng });
    setCurrentWeather(weather.current_weather);
    /* ↑ここを追加↑ */
  }

  return (
    <Wrapper apiKey={process.env.REACT_APP_MAP_API_KEY}>
      <Map center={POS_TOKYO_STATION} zoom={14} onClick={onClick}>
        <Marker position={markerPosition} />
      </Map>
      {/* ↓ここを追加↓ */}
      <WeatherDisplay currentWeather={currentWeather} />
    </Wrapper>
  );
}

export default App;

```

以下の関数で「Weather Forecast API」にリクエストしています。
引数の`lat`, `lng`をパラメータに設定することで指定した位置の天気情報を取得できます。
クリックした位置情報を引数`lat`, `lng`として関数に渡しているので、クリック位置の天気が取得できる、という仕組みになっています。
さらに`current_weather=true`を指定することで、現在の天気情報を取得内容に含ませています。

```react
const getWeather = async ({ lat, lng }) => {
  const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current_weather=true&timezone=Asia%2FTokyo`);
  return response ? response.json() : null;
}
```

これでリクエスト実行・取得は実装できました。
あとは取得データを表示できるコンポーネントを作ります。
`components`配下に`WeatherDisplay.js`を作ります。
こちらで天気表示機能を実装します。

```react:src/components/WeatherDisplay.js
import { useState, useEffect } from 'react';

const WEATHER_DATA = [
  { codes: [ 0 ], img: 'sunny', text: '快晴' },
  { codes: [ 1, 2 ], img: 'sunny', text: '晴れ' },
  { codes: [ 3, 45, 48 ], img: 'cloudy', text: 'くもり' },
  { codes: [ 51, 53, 55, 56, 57 ], img: 'drizzle', text: '霧雨' },
  { codes: [ 61, 63, 66, 80 ], img: 'rainy', text: '雨' },
  { codes: [ 65, 67, 81, 82 ], img: 'heavy_rainy', text: '大雨' },
  { codes: [ 71, 73, 77, 85 ], img: 'snowy', text: '雪' },
  { codes: [ 75, 86 ], img: 'heavy_snowy', text: '大雪' },
  { codes: [ 95, 96, 99 ], img: 'thunder', text: '雷' },
];

export const WeatherDisplay = ({ currentWeather }) => {
  const [ open, setOpen ] = useState(true);

  useEffect(() => {
    if (currentWeather) {
      setOpen(true);
    }
  }, [ currentWeather ])

  if (!currentWeather) {
    return <div className='weather' />;
  }

  const onClickToggle = () => {
    setOpen(!open);
  }

  const formatTime = (time) => {
    const date = new Date(time);
    return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${('00' + date.getMinutes()).slice(-2)}`;
  }

  const getWeatherByCode = (code) => {
    return WEATHER_DATA.find(data => data.codes.includes(code));
  }

  const formattedTime = formatTime(currentWeather.time);
  const weather = getWeatherByCode(currentWeather.weathercode);

  return <div className={`weather ${open && 'weather--open'}`}>
    <div className='weather__toggle' onClick={onClickToggle} />
    <div className='weather__time_wrap'><div className='weather__time'>{formattedTime}</div>クリック地点の天気</div>
    {weather && (
      <div className='weather__icon_wrap'>
        <img src={`./img/${weather.img}.png`} className='weather__icon' />
        <span>{weather.text}</span>
      </div>
    )}
    <ul className='weather__list'>
      <li className='weather__item'>
        <span className='weather__item_name'>気温</span>
        <span className='weather__item_content'>{currentWeather.temperature}&#8451;</span>
      </li>
      <li className='weather__item'>
        <span className='weather__item_name'>風速</span>
        <span className='weather__item_content'>
          <div className='weather__arrow_wrap'>
            <img src='./img/arrow.png' className='weather__arrow' style={{ transform: `rotate(${currentWeather.winddirection}deg)` }} />
          </div>
          {currentWeather.windspeed}km/h
        </span>
      </li>
    </ul>
  </div>;
}
```

`WEATHER_DATA`変数は天気出しわけ用のデータです。
codesにはWMOの天候情報のコードが入っています。APIドキュメントに[天候情報のコードの説明](https://open-meteo.com/en/docs#latitude=35.69&longitude=139.69&hourly=precipitation&daily=temperature_2m_max,temperature_2m_min&current_weather=true&timezone=Asia%2FTokyo:~:text=Weather%20variable%20documentation)があったので、それを参考にして天気をグルーピングしました。
解釈間違っているかもしれないですが、ご容赦ください、、！

```react
const WEATHER_DATA = [
  { codes: [ 0 ], img: 'sunny', text: '快晴' },
  { codes: [ 1, 2 ], img: 'sunny', text: '晴れ' },
  { codes: [ 3, 45, 48 ], img: 'cloudy', text: 'くもり' },
  { codes: [ 51, 53, 55, 56, 57 ], img: 'drizzle', text: '霧雨' },
  { codes: [ 61, 63, 66, 80 ], img: 'rainy', text: '雨' },
  { codes: [ 65, 67, 81, 82 ], img: 'heavy_rainy', text: '大雨' },
  { codes: [ 71, 73, 77, 85 ], img: 'snowy', text: '雪' },
  { codes: [ 75, 86 ], img: 'heavy_snowy', text: '大雪' },
  { codes: [ 95, 96, 99 ], img: 'thunder', text: '雷' },
];
```

続いて天気の画像を`public/img`に追加します。
以下の赤枠のところに表示するものですね。
画像はなんでも良いです。好みに合わせて実装してください。
画像ファイル名は`WEATHER_DATA`変数中の`img`プロパティの名前と対応する形で実装ください。
<img width="300" alt="スクリーンショット 2022-12-06 9.09.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2321549/9c071b35-f2b6-aa9f-7ff4-ce2832d65d84.png">


最後に、見た目を整えるためcssを編集します。

```css:src/App.css
ul {
  padding: 0;
  margin: 0;
}

li {
  list-style: none;
}

.map {
  width: 100vw;
  height: 100vh;
}

.weather {
  position: fixed;
  top: 0;
  right: 0;
  width: 200px;
  height: 100vh;
  padding: 56px 16px 16px;
  background-color: rgba(255, 255, 255, 0.9);
  border-right: solid 1px #ddd;
  transform: translateX(100%);
  transition: transform 0.3s;
}

.weather.weather--open {
  transform: translateX(0);
}

.weather__toggle {
  position: absolute;
  top: 16px;
  left: -48px;
  display: flex;
  width: 48px;
  height: 48px;
  background-color: rgba(255, 255, 255, 0.9);
  transition: left 0.3s;
}


.weather__toggle::after {
  position: absolute;
  top: 33%;
  left: 40%;
  content: '';
  width: 16px;
  height: 16px;
  border-top: solid 2px #3399ff;
  border-right: solid 2px #3399ff;
  transform: rotate(-135deg);

}

.weather.weather--open .weather__toggle {
  left: 8px;
  background-color: transparent;
}

.weather.weather--open .weather__toggle::after {
  transform: rotate(45deg);
  left: 23%;
}

.weather__time_wrap {
  padding: 8px;
  margin-top: 16px;
  font-size: 1rem;
  font-weight: bold;
  color: #fff;
  background-color: #3399ff;
  border-radius: 50px;
  text-align: center;
}

.weather__time {
  font-size: 1.5rem;
}

.weather__icon_wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 8px;
  color: #666;
}

.weather__icon {
  width: 150px;
  height: 150px;
}

.weather__arrow {
  width: 12px;
  height: 30px;
  transform-origin: center center;
}

.weather__list {
  padding: 32px 16px;
}

.weather__item {
  font-size: 2rem;
  color: #666;
}

.weather__item:not(:first-of-type) {
  margin-top: 16px;
}

.weather__item_name {
  font-size: 1rem;
  margin-right: 16px;
}

.weather__item_content {
  display: flex;
}

.weather__arrow_wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
}
```

これで実装完了となります！
以下コマンドを実行してみましょう。
```
npm start
```

地図上でどこかの地点をクリックすると、右からニュッと天気情報が出てくると思います！
ブラボー！お疲れ様でした！

## あとがき

便利なAPIが世の中には溢れていて、それらを組み合わせると色々面白いことができるなと改めて実感できました。
今回は非常にニッチなアプリを作りましたが、「Maps JavaScript API」は特にアイデア次第でまだ世にない便利なサービスを作れそうな予感がしました！


## 参考資料
- [API Keyを発行する手順 for Google Cloud API](https://zenn.dev/tmitsuoka0423/articles/get-gcp-api-key)
- [React アプリケーションに Map と Marker を追加する  |  Maps JavaScript API  |  Google Developers](https://developers.google.com/maps/documentation/javascript/react-map)
- [@googlemaps/react-wrapper - npm](https://www.npmjs.com/package/@googlemaps/react-wrapper)
- [APIキーもログインも不要！完全無料で使える天気予報API「Open-Meteo」を使ってみた！ - paiza開発日誌](https://paiza.hatenablog.com/entry/2021/11/04/130000)
- [Weather Forecast API | Open-Meteo.com](https://open-meteo.com/en/docs#latitude=35.69&longitude=139.69&hourly=precipitation&daily=temperature_2m_max,temperature_2m_min&current_weather=true&timezone=Asia%2FTokyo)
- [気象関係コード表](https://www.jodc.go.jp/data_format/weather-code_j.html)