概要
「急に予定が空いたからどこかに出かけようかな〜!
でも、お出かけ先の今の天気はどうかな?」
なんてシチュエーション、あると思います。
そんな時に役立つ(?)アプリを一から作ってみましょう!
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を表示します。
実際の動きとしては、
①地図上をクリックするとピンが立ち、、、
②画面右側にその地点の現在の天気1、気温、風向きと風速が表示されます。
シンプルですね!
作り方
以下の流れで実装していきます。
元のソースはこちらにあります。
とりあえず動かしてみたい方は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のページからユーザー登録します。
画面右上の「無料で利用開始」から登録できます。
※登録の際、クレジットカード番号も必要になります。
登録が完了したら、API Keyを発行します。発行の仕方についてはこちらの記事がわかりやすかったです。
API Keyを発行する手順 for Google Cloud API
なお、今回は「Maps JavaScript API」を使うので、「Maps JavaScript API」を選択してAPI Keyを発行してください。
API Keyを発行できたら、ソースコードのルート直下に.env.local
を作り、以下のように書きます。
API Keyは[発行したAPI Key]
の部分にコピペしてください。
REACT_APP_MAP_API_KEY=[発行したAPI Key]
地図表示を実装
環境が整ったので実装に入っていきます。
地図表示を実装します。
地図表示には「Maps JavaScript API」のReact向けnpmパッケージを使用します。
(公式ドキュメント)
まず、src/
配下にcomponents/
ディレクトリを作成し、その配下にMap.js
を作ります。
こちらで地図本体の実装を行います。
地図を生成し、onClick
などのイベントも登録していきます。
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
を作ります。
こちらでは地図クリック時に表示する赤いピンを実装します。
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を編集していきます。既に生成されているのですが、丸ごと以下に書き換えてください。
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キーが入ってきます。
また、Map
の中にMarker
があるので、Marker
はMap
の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が書き込まれていますが必要ないので全て消し、以下に書き換えます。
.map {
width: 100vw;
height: 100vh;
}
これで一旦地図機能の実装は完了です。
以下コマンドを実行してみましょう。
npm start
画面いっぱいに地図が表示され、地点をクリックすると赤いピンが立てばOKです。
天気表示を実装
ここからいよいよ天気の表示部を実装します。
天気APIには、Open-Meteoの「Weather Forecast API」を使用します。
このAPIは無料で、API Keyも必要ないというサービス精神の塊みたいなAPIで、誰でもアクセスできます。
公式ドキュメントで取得したいパラメータや条件を選ぶと、選んだ条件やパラメータに応じたリクエストURLが赤枠部分に表示されますので、色々試してみると面白いです!
では実装していきます。
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}¤t_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}¤t_weather=true&timezone=Asia%2FTokyo`);
return response ? response.json() : null;
}
これでリクエスト実行・取得は実装できました。
あとは取得データを表示できるコンポーネントを作ります。
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}℃</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
プロパティの名前と対応する形で実装ください。
最後に、見た目を整えるため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
- React アプリケーションに Map と Marker を追加する | Maps JavaScript API | Google Developers
- @googlemaps/react-wrapper - npm
- APIキーもログインも不要!完全無料で使える天気予報API「Open-Meteo」を使ってみた! - paiza開発日誌
- Weather Forecast API | Open-Meteo.com
- 気象関係コード表