17
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NIJIBOXAdvent Calendar 2022

Day 13

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

Last updated at Posted at 2022-12-12

スクリーンショット 2022-12-06 9.00.48.png

概要

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

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

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

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

使用するもの

  • React
  • Google 「Maps JavaScript API」
  • Open-Meteo 「Weather Forecast API」

使い方

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

実際の動きとしては、
①地図上をクリックするとピンが立ち、、、
スクリーンショット 2022-12-06 9.09.08.png

②画面右側にその地点の現在の天気1、気温、風向きと風速が表示されます。
スクリーンショット 2022-12-06 9.09.22.png

動くとこんな感じになります。
Videotogif (1).gif

シンプルですね!

作り方

以下の流れで実装していきます。

  1. セットアップ
  2. API Keyの用意
  3. 地図表示を実装
  4. 天気表示を実装

元のソースはこちらにあります。
とりあえず動かしてみたい方は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のページからユーザー登録します。
画面右上の「無料で利用開始」から登録できます。
※登録の際、クレジットカード番号も必要になります。
スクリーンショット 2022-12-06 13.18.08.png

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

API Keyを発行する手順 for Google Cloud API

なお、今回は「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パッケージを使用します。
公式ドキュメント

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

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として渡しています。

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

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

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を編集していきます。既に生成されているのですが、丸ごと以下に書き換えてください。

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;

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

<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が書き込まれていますが必要ないので全て消し、以下に書き換えます。

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

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

npm start

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

天気表示を実装

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

天気APIには、Open-Meteoの「Weather Forecast API」を使用します。
このAPIは無料で、API Keyも必要ないというサービス精神の塊みたいなAPIで、誰でもアクセスできます。
公式ドキュメントで取得したいパラメータや条件を選ぶと、選んだ条件やパラメータに応じたリクエストURLが赤枠部分に表示されますので、色々試してみると面白いです!
スクリーンショット 2022-12-06 15.06.29.png

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

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を指定することで、現在の天気情報を取得内容に含ませています。

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を作ります。
こちらで天気表示機能を実装します。

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ドキュメントに天候情報のコードの説明があったので、それを参考にして天気をグルーピングしました。
解釈間違っているかもしれないですが、ご容赦ください、、!

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プロパティの名前と対応する形で実装ください。
スクリーンショット 2022-12-06 9.09.16.png

最後に、見た目を整えるため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」は特にアイデア次第でまだ世にない便利なサービスを作れそうな予感がしました!

参考資料

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

17
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?