概要
- 地図をタップした場所の週間天気予報を表示する、スマートフォン向けのシンプルなアプリです
- 地図データ提供:OpenStreetMap
- Reverse Geocoding:Nominatim
- 天気データ提供:Open-Meteo
- ホスティング:GitHubPages
デモ
ソース
主な機能
-
特徴
- スマホの縦持ちに特化したデザイン
- 地図上の任意の場所を指定 → 住所情報取得
- Open-Meteo API による週間天気(天気・最低気温・最高気温)
- UTF-8絵文字で天気をわかりやすく表示
- ダークテーマ + 紫系配色で目に優しいデザイン
- HTML / CSS / JavaScript構成(サーバ不要)
- 各ボタンは3秒間のインターバルを設けており、APIへの負担を軽減
-
使い方
- 地図の任意の場所をタップします。
- 「場所設定」ボタンを押し、タップした場所の住所を確認します。
- 「予報取得」ボタンを押し、選択した場所の最新の天気予報を取得します。
-
技術仕様
- HTML / CSS / JavaScriptを用いたフロントエンドのみの構成
- GitHub Pagesでホスト
- 地図表示はLeafletを利用。Webページに地図を表示するための軽量で使いやすいJavaScriptライブラリ
ソースについて
htmlについて
🔹0. index.html 全体構成の概要
HTMLは次のような構造になっています
<!DOCTYPE html>
<html lang="ja">
<head> ... </head>
<body>
<h1>...</h1>
<div id="map">...</div>
<div id="message">...</div>
<div id="info">...</div>
<div id="forecast">...</div>
<footer>...</footer>
<script src="..."></script>
</body>
</html>
🔹1. !DOCTYPE html と html lang="ja"
<!DOCTYPE html>
<html lang="ja">
- HTML5 の文書宣言
- lang="ja" によって、文書の言語を「日本語」と指定(アクセシビリティや検索にも影響)
🔹2. head セクション
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>週間天気予報</title>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
</head>
- meta charset: 日本語を含む文字を正しく表示するための設定(UTF-8)
- viewport: スマホでの表示最適化(レスポンシブ対応)
- title: タブや検索結果に表示されるページタイトル
- style.css: 自作のスタイルシート
- Leaflet のCSS: 地図描画ライブラリLeafletの外部スタイル
🔹3. body セクション(画面の見える部分)(1) タイトル表示
<h1>地図ベース週間天気予報</h1>
- ページの見出し。フォントサイズや色はCSSで制御
🔹4. body セクション (2) 地図表示エリア
<div id="map"></div>
- 地図(Leaflet.jsで描画される)を表示するためのコンテナ
- サイズはCSSで設定(高さ300px、幅100%)
🔹5. body セクション (3) 操作案内メッセージ
<div id="message">操作:地図タップ → 場所設定 → 予報取得</div>
- 初期表示における操作ガイド
- 操作が進むとJSで内容が書き換えられたり非表示になったりします
🔹6. body セクション (4) 情報パネル(緯度経度、住所、ボタン)
<div id="info">
<button id="setLocation">場所設定</button>
<p id="status"></p>
<p id="coords">緯度・経度: -</p>
<p id="address">住所: -</p>
<button id="fetchWeather" disabled>予報取得</button>
<p id="fetchedTime"></p>
</div>
- setLocationボタン 地図で指定した地点から住所を取得するトリガー
- status 現在の状態(例:住所取得中、予報取得完了など)
- coords タップされた緯度・経度を表示
- address 取得した住所を表示
- fetchWeatherボタン 天気予報取得ボタン。初期は無効(disabled)
- fetchedTime 予報を取得した日時を表示
🔹7. body セクション (5) 予報表示エリア
<div id="forecast"></div>
- JavaScriptから天気情報(絵文字と気温など)が挿入される場所
- 初期は非表示にしておき、予報取得後に表示する
🔹8. body セクション (6) フッター(利用APIのクレジット)
<footer>
<div>地図データ提供:<a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a></div>
<div>Reverse Geocoding:<a href="https://nominatim.openstreetmap.org/ui/about.html" target="_blank">Nominatim</a></div>
<div>天気データ提供:<a href="https://open-meteo.com/" target="_blank">Open-Meteo</a></div><br>
このページの説明は以下の記事に記載します。<br>
<a href="https://qiita.com/kubo4ka/items/9b7fbeca3e28599d8bc0"
target="_blank">https://qiita.com/kubo4ka/items/9b7fbeca3e28599d8bc0</a>
</footer>
- 利用しているデータ/API元の明示(クレジット義務あり)
- a タグで外部リンク。target="_blank" で新しいタブに開く
🔹9. body セクション (7) スクリプト読み込み
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="script.js"></script>
- Leafletライブラリ(JS本体)
- 自作の JavaScript(script.js)でアプリのロジックを制御
jsについて
🔹0. 全体構造の見通し
// 初期変数・要素取得
// 関数:経度補正・日付整形・気温整形・天気絵文字変換
// 地図の初期化とタップ処理
// 「場所設定」ボタン処理
// 「予報取得」ボタン処理
🔹1. 初期変数の定義・DOM要素取得
let selectedLat = null;
let selectedLng = null;
let marker = null;
const coordsEl = document.getElementById("coords");
const addressEl = document.getElementById("address");
const statusEl = document.getElementById("status");
const forecastEl = document.getElementById("forecast");
const fetchWeatherBtn = document.getElementById("fetchWeather");
const setLocationBtn = document.getElementById("setLocation");
const messageEl = document.getElementById("message");
const fetchedTimeEl = document.getElementById("fetchedTime");
// 仮マーカー(グレーっぽい)
const tempMarkerIcon = L.icon({
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png",
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png",
shadowSize: [41, 41],
className: "temp-marker" // カスタムスタイル用
});
// 正式マーカー(通常アイコン)
const officialMarkerIcon = L.icon({
iconUrl: "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-violet.png",
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png",
shadowSize: [41, 41],
});
let lastSetTime = 0;
let lastFetchTime = 0;
- ユーザーが地図をタップしたときの 緯度・経度 を保持
- HTMLから取得するDOM要素を変数に保持しておき、後で動的に値を書き換える
- 仮設定アイコン、正式設定アイコンの画像を読み込み
- API呼び出しの連打防止(3秒間隔)のため、lastSetTime と lastFetchTime を使う
🔹2. 補助関数群
(a) 経度を -180〜180 に補正
function normalizeLongitude(lon) {
return ((lon + 180) % 360 + 360) % 360 - 180;
}
- 例えば 249° → -111° に変換
- Leafletは180を超えた経度を返せるが、Open-Meteo や Nominatim API では -180~180 の範囲外はエラーとなるため
(b) 日付文字列を "MM/DD (曜)" に整形
function getFormattedDate(dateStr) {
const date = new Date(dateStr);
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const weekday = date.toLocaleDateString("ja-JP", { weekday: "short" });
return `${month}/${day} (${weekday})`;
}
- スマホのスペース考慮して年は省略
- 例:"2025-03-30" → "03/30 (日)"
- padStart() を使ってゼロ詰め
(c) 気温を固定長表示 " - 2.3°" の形に整形
function formatTemp(temp) {
const sign = temp < 0 ? "-" : " ";
const abs = Math.abs(temp);
const intPart = Math.floor(abs);
const decPart = Math.round((abs - intPart) * 10);
const tens = intPart >= 10 ? Math.floor(intPart / 10) : " ";
const ones = intPart % 10;
return `${sign} ${tens}${ones}.${decPart}°`;
}
- スペース+桁揃えの整形
- 例:-2.3 → " - 2.3°"、10.7 → " 10.7°"
(d) 天気コードを絵文字に変換
function weatherEmoji(code) {
if (code === 0) return '☀️'; // 快晴
if (code === 1) return '🌤️'; // ほぼ晴れ
if (code === 2) return '⛅'; // 薄曇り
if (code === 3) return '☁️'; // 曇り
if (code === 45 || code === 48) return '🌫️'; // 霧
if (code === 51 || code === 53 || code === 55) return '🌦️'; // 霧雨
if (code === 56 || code === 57) return '🌧️❄️'; // 凍結霧雨
if (code === 61 || code === 63) return '🌧️'; // 弱〜中程度の雨
if (code === 65) return '🌧️🌧️'; // 強い雨
if (code === 66 || code === 67) return '🌧️❄️'; // 凍結雨
if (code === 71 || code === 73) return '🌨️'; // 弱〜中程度の雪
if (code === 75) return '❄️❄️'; // 強い雪
if (code === 77) return '🌨️⛄'; // 雪あられ
if (code === 80) return '🚿'; // 弱いにわか雨
if (code === 81) return '🌧️🚿'; // 中程度のにわか雨
if (code === 82) return '🌧️🌧️🚿'; // 激しいにわか雨
if (code === 85) return '🌨️🚿'; // 弱いにわか雪
if (code === 86) return '🌨️❄️❄️'; // 強いにわか雪
if (code === 95) return '⛈️'; // 雷雨
if (code === 96) return '⛈️🧊'; // 雷雨(弱い雹)
if (code === 99) return '⛈️🧊🧊'; // 雷雨(強い雹)
return '❔'; // 未定義コードのフォールバック
}
- Open-Meteo の weathercode を WMO の定義に準拠して分類、それっぽい絵文字を割り当て
- 定義のないコードは "❔" で代替
https://www.nodc.noaa.gov/archive/arc0021/0002199/1.1/data/0-data/HTML/WMO-CODE/WMO4677.HTM
🔹3. 地図の初期化とタップイベント
const map = L.map("map").setView([35.6895, 139.6917], 5);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
}).addTo(map);
- 地図を #map 要素に描画(Leaflet)
- 東京を中心にズームレベル5で初期表示
map.on("click", function (e) {
selectedLat = e.latlng.lat;
selectedLng = normalizeLongitude(e.latlng.lng);
coordsEl.textContent = `緯度・経度: ${selectedLat.toFixed(4)}, ${selectedLng.toFixed(4)}`;
addressEl.textContent = "住所: -";
statusEl.innerHTML = "地図がタップされました。<br>「場所設定」ボタンを押してください。";
fetchWeatherBtn.disabled = true;
forecastEl.innerHTML = "";
fetchedTimeEl.textContent = "";
forecastEl.classList.add("hidden"); // 予報非表示に戻す
forecastEl.classList.add("hidden");
if (marker) map.removeLayer(marker);
marker = L.marker([selectedLat, selectedLng], { icon: tempMarkerIcon }).addTo(map);
});
- 地図をタップしたら座標を取得・画面に表示
- 状態やボタンの有効化を管理
- 予報エリアをリセットして非表示化
🔹4. 場所設定ボタン処理(Nominatimによる住所取得)
setLocationBtn.addEventListener("click", async () => {
const now = Date.now();
if (now - lastSetTime < 3000) return;
lastSetTime = now;
if (!selectedLat || !selectedLng) {
statusEl.innerHTML = "まず地図をタップしてください。";
return;
}
statusEl.innerHTML = "住所取得中...";
if (marker) map.removeLayer(marker);
marker = L.marker([selectedLat, selectedLng], { icon: officialMarkerIcon }).addTo(map);
- 連打防止(3秒)
- マーカー更新(既にある場合は削除して再描画)
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${selectedLat}&lon=${selectedLng}&format=json`
);
const data = await res.json();
addressEl.textContent = `住所: ${data.display_name || "不明"}`;
statusEl.innerHTML = "場所が設定されました。<br>「予報取得」ボタンを押してください。";
fetchWeatherBtn.disabled = false;
messageEl.textContent = ""; // 案内文消去
} catch (err) {
statusEl.innerHTML = "住所の取得に失敗しました。";
}
});
- Reverse Geocoding API 呼び出し
- 住所の表示、状態メッセージ、ボタン有効化を行う
🔹5. 予報取得ボタン処理(Open-Meteo API 呼び出し)
fetchWeatherBtn.addEventListener("click", async () => {
const now = Date.now();
if (now - lastFetchTime < 3000) return;
lastFetchTime = now;
if (!selectedLat || !selectedLng) {
statusEl.innerHTML = "まず場所を設定してください。";
return;
}
statusEl.innerHTML = "予報取得中...";
- 緯度経度が設定されていなければ処理中断
- 予報API呼び出し準備
try {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${selectedLat}&longitude=${selectedLng}&daily=weathercode,temperature_2m_max,temperature_2m_min&timezone=Asia%2FTokyo`;
const res = await fetch(url);
const data = await res.json();
const daily = data.daily;
const output = daily.time.map((date, i) => {
const emoji = weatherEmoji(daily.weathercode[i]);
const dateStr = getFormattedDate(date);
const min = formatTemp(daily.temperature_2m_min[i]);
const max = formatTemp(daily.temperature_2m_max[i]);
return `
<div class="forecast-day">
<span>${dateStr}</span>
<span>${emoji}</span>
<span>${min}</span>
<span>/</span>
<span>${max}</span>
</div>
`;
}).join("");
forecastEl.innerHTML = output;
forecastEl.classList.remove("hidden"); // 表示
statusEl.innerHTML = "予報取得完了";
fetchedTimeEl.textContent = `予報取得日時: ${new Date().toLocaleString("ja-JP")}`;
} catch (err) {
statusEl.innerHTML = "予報の取得に失敗しました。";
}
});
- Open-Meteo APIを呼び出してデータを取得
- 日別のHTMLブロックを .forecast-day として生成・表示
- 予報エリア表示、更新日時の記録、状態更新
cssについて
🔹0. 全体構成の概要
CSSは主に以下の構成になっています
- 基本スタイル(body・h1など)
- レイアウト要素(地図・予報エリア・フッターなど)
- ボタンデザイン(通常・ホバー・無効)
- 予報表示(gridレイアウト)
- 案内表示(メッセージ・ステータス)
- ユーティリティ(.hidden など)
🔹1. 基本スタイル
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #121212;
color: #ffffff;
text-align: center;
}
h1 {
margin: 1rem;
font-size: 1.5rem;
color: #888;
}
- body 全体の背景色はダークテーマ(#121212)
- フォントはシンプルな sans-serif(スマホでも読みやすい)
- テキストは中央寄せ
- h1 でタイトルの余白・サイズを指定
🔹2. 地図表示と案内メッセージ
#map {
height: 300px;
width: 100%;
}
#message {
padding: 0.8rem;
margin: 1rem auto;
width: fit-content;
color: #9050ea;
font-weight: bold;
font-size: 0.95rem;
}
#status {
color: #9050ea;
padding: 0.5rem;
font-weight: bold;
width: fit-content;
margin: 0.5rem auto;
font-size: 0.9rem;
}
- #map:Leafletが描画する地図領域(縦300px)
- #message・#status:
- 紫系テキストで操作ナビゲーションを目立たせる
- fit-content を使って中央寄せ+自然な横幅調整
🔹3. ボタンデザイン(紫ベース+ホバー・無効対応)
button {
padding: 0.6rem 1.2rem;
font-size: 1rem;
background-color: #6200ea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
margin: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
transition: 0.3s;
}
button:hover {
background-color: #9575cd;
}
button:disabled {
background-color: #555555;
cursor: not-allowed;
}
- 通常:柔らかい紫色(#6200ea)と丸みのある角
- ホバー:少し明るい紫(#9575cd)に変化
- 無効状態(disabled):灰色で明確に見た目を変える+カーソル変更
🔹4. 予報表示スタイル(.forecast-day)
#forecast {
background-color: #1e1e1e;
border: 1px solid #444;
border-radius: 8px;
margin: 1rem auto;
padding: 1rem;
max-width: 90%;
}
- 背景をやや明るめのダークグレー(視認性を保ちつつダークテーマ)
- 角丸、余白、最大幅などを調整してコンパクトに中央寄せ
.forecast-day {
display: grid;
grid-template-columns: 20% 20% 25% 10% 10%;
gap: 1ch;
align-items: center;
padding: 0.4rem 0;
border-bottom: 1px solid #333;
font-size: 0.9rem;
font-family: 'Courier New', monospace;
font-feature-settings: "tnum";
}
- display: grid で5列構成(日付・絵文字・最低・/・最高)
- 各列の幅を%で調整
- フォントは等幅(数字の桁をきれいに揃える)
- font-feature-settings: "tnum" で数字の揃えをさらに補強
.forecast-day span {
white-space: nowrap; /* 改行防止 */
}
.forecast-day span:first-child { justify-self: start; }
.forecast-day span:nth-child(2) { justify-self: center; }
.forecast-day span:nth-child(3) { justify-self: end; }
.forecast-day span:nth-child(4) { justify-self: center; }
.forecast-day span:last-child { justify-self: start; }
- spanごとの位置揃え(左寄せ/中央/右寄せ)を細かく制御
🔹5. フッターとリンク
footer {
font-size: 0.7rem;
color: #888;
margin: 2rem 0 1rem 0;
line-height: 1.6;
}
footer a {
color: #bb86fc;
text-decoration: none;
}
- データ提供元を明記
- リンクカラーもダークテーマに合わせた落ち着いた色
- line-heightで行間を調整し読みやすさを確保
フロー図
アプリについて
天気予報シリーズ第三弾。
別にシリーズにしようと思った訳ではなく、
先ず一つ作ってみたら、普通に気温がほしくなり、
二つ目で気温は付けたけど、ハードコードした経度緯度が気になり、
三つ目になりました。
今回は一応思い残す点はなく完成とできた感じです。
予報の列を揃えられた点も、前回の諦めを対応できた感じです。
ナビゲーションの場所とか、
ステータスとか、もっと画面の構成考えてからやれば良かったとは思いますが、
今回はこれで完成としました。
似たようなことを試していますので、良ければご確認ください。
気象庁のjsonを利用した天気予報表示
https://qiita.com/kubo4ka/items/9b7fbeca3e28599d8bc0
プルダウンの場合の週間天気予報表示
以上