1
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?

地図で指定した場所の週間天気予報をGitHubPagesで表示してみた

Last updated at Posted at 2025-03-29

概要

  • 地図をタップした場所の週間天気予報を表示する、スマートフォン向けのシンプルなアプリです
    • 地図データ提供: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

プルダウンの場合の週間天気予報表示

https://qiita.com/kubo4ka/items/1dcc35bf350a9e600e3c

以上

1
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
1
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?