Obsidianのデイリーノートに今日の天気を埋め込みたい（その3）〜場所選択機能付き〜


## はじめに

この記事は以下の記事の続編です：

- [Obsidianのデイリーノートに今日の天気を埋め込みたい](https://qiita.com/alumite14/items/fe05a85c2ef4f4ea608b)
- [Obsidianのデイリーノートに今日の天気を埋め込みたい（その2）](https://qiita.com/alumite14/items/b8f16d2ef56c7e005658)

前回の記事では、デイリーノート作成時に天気情報を自動取得する仕組みを構築しましたが、**場所を選択できなかった**という課題がありました。これでは異なる場所で使用している複数端末連携時や、ノートPCを外部に持ち出した際に、実行時の場所の天気を選べずに不便でした。

今回の記事では、以下の画像のようにあらかじめコードに埋め込んだ住所を実行時に提案・選択することで、**複数の住所での天気情報を取得できるデイリーノート**を提案します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665424/664c3128-7224-4881-b559-ffbd55fd3237.png)

▼

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665424/cd42061f-cbfb-4cc4-b767-25da8280eefc.png)

## 必要な環境

- Obsidian
- Templater プラグイン
- OpenWeatherMap API キー（取得方法は[前回記事](https://qiita.com/alumite14/items/b8f16d2ef56c7e005658)を参照）

## 実装方法

### 1. デイリーノートテンプレートの作成

すでに作ってある`dailynote_temp.md`の内容を以下のコードに置き換えてください。
（またdailynoteを作っていないなら、[頑張らない最低限のObsidian環境構築 - Qiita](https://qiita.com/alumite14/items/d3ca4f25bfbf08c3b5e2)などの内容を参考にして作ってください）

```md title:dailynote_temp.md
---
aliases:
  - "<% moment(tp.file.title).format('MMDD') %>"
  - "<% moment(tp.file.title).format('YYYYMMDD') %>"
tags:
  - "daily"
  - "<% moment(tp.file.title).format('YYYY') %>"
  - "<% moment(tp.file.title).format('YYYY-MM') %>"
---
#### << [[<% moment(tp.file.title).add(-1, 'd').format('YYYY-MM-DD') %>|Yesterday]] | [[<% moment(tp.file.title).add(-1, 'y').format('YYYY-MM-DD') %>|Last year]] | [[<% moment(tp.file.title).add(1, 'd').format('YYYY-MM-DD') %>|Tomorrow]] >>

今年の残り日数：<% moment(`${moment(tp.file.title).endOf('year')}`).diff(tp.file.title, "days") %>日

<%*
const formatTime = (dateTimeStr) => {
    const date = new Date(dateTimeStr);
    return date.toLocaleTimeString("ja-JP", {
        hour: "2-digit",
        minute: "2-digit",
        hour12: false,
    });
};

const padJapanese = (str, length) => {
    const padding = "　".repeat(Math.max(0, length - str.length));
    return str + padding;
};

const getJapaneseDescription = (description) => {
    const descMap = {
        "clear sky": "快晴",
        "few clouds": "晴れ",
        "scattered clouds": "晴れ時々曇り",
        "broken clouds": "所により曇り",
        "overcast clouds": "曇り",
        "light rain": "小雨",
        "shower rain": "霧雨",
        "moderate rain": "雨",
        thunderstorm: "雷雨",
        "heavy rain": "大雨",
        snow: "雪",
        "light snow": "小雪",
        mist: "霧",
        "thunderstorm with light rain": "弱い雨を伴う雷雨",
        "thunderstorm with rain": "雨を伴う雷雨",
        "thunderstorm with heavy rain": "強い雨を伴う雷雨",
        "light thunderstorm": "弱い雷雨",
        "heavy thunderstorm": "強い雷雨",
        "ragged thunderstorm": "断続的な雷雨",
        "thunderstorm with light drizzle": "弱い霧雨を伴う雷雨",
        "thunderstorm with drizzle": "霧雨を伴う雷雨",
        "thunderstorm with heavy drizzle": "強い霧雨を伴う雷雨",
        "light intensity drizzle": "弱い霧雨",
        drizzle: "霧雨",
        "heavy intensity drizzle": "強い霧雨",
        "light intensity drizzle rain": "弱い霧雨混じりの雨",
        "drizzle rain": "霧雨混じりの雨",
        "heavy intensity drizzle rain": "強い霧雨混じりの雨",
        "shower rain and drizzle": "霧雨を伴うにわか雨",
        "heavy shower rain and drizzle": "強い霧雨を伴うにわか雨",
        "shower drizzle": "にわか霧雨",
        rain: "雨",
        "heavy intensity rain": "激しい雨",
        "very heavy rain": "非常に強い雨",
        "extreme rain": "猛烈な雨",
        "freezing rain": "凍雨",
        "light intensity shower rain": "弱いにわか雨",
        "heavy intensity shower rain": "強いにわか雨",
        "ragged shower rain": "断続的なにわか雨",
        "heavy snow": "大雪",
        sleet: "みぞれ",
        "light shower sleet": "弱いにわかみぞれ",
        "shower sleet": "にわかみぞれ",
        "light rain and snow": "弱い雨雪",
        "rain and snow": "雨雪",
        "light shower snow": "弱いにわか雪",
        "shower snow": "にわか雪",
        "heavy shower snow": "強いにわか雪",
        smoke: "煙",
        haze: "もや",
        "sand/dust whirls": "砂塵旋風",
        fog: "濃霧",
        sand: "砂",
        dust: "粉塵",
        "volcanic ash": "火山灰",
        squalls: "スコール",
        tornado: "竜巻",
    };
    return descMap[description] || description;
};

const convertTimestamp = (timestamp) => {
    const date = new Date(timestamp * 1000);
    const hours = date.getHours().toString().padStart(2, "0");
    const minutes = date.getMinutes().toString().padStart(2, "0");
    return `${hours}:${minutes}`;
};

// formatWeatherData 関数に selectedCityName 引数を追加
const formatWeatherData = (weatherData, selectedCityName) => {
    // weatherData.city.name ではなく selectedCityName を使用
    const city = selectedCityName;
    const { list } = weatherData;

    const sunrise = convertTimestamp(weatherData.city.sunrise); // 日の出時刻
    const sunset = convertTimestamp(weatherData.city.sunset); // 日の入時刻

    const formattedData = list.slice(0, 8).map((period) => {
        const {
            dt_txt,
            weather,
            main: { temp, pressure, humidity },
            pop,
            rain,
            wind: { speed, deg },
        } = period;

        const time = formatTime(dt_txt);
        const iconCode = weather[0].icon;
        const iconImageUrl = `![icon|16](https://openweathermap.org/img/wn/${iconCode}@2x.png)`; // 画像URLを生成
        const description = getJapaneseDescription(weather[0].description);
        const paddedDesc = padJapanese(description, 6);
        const temperature = `${Math.round(temp - 273.15)}°C`;
        const rainChance = `${Math.round(pop * 100)}`;
        const precipitation = rain ? `${rain["3h"].toFixed(1)}` : "0.0";
        const windSpeed = `${Math.round(speed * 3.6)}`;
        //let compassSector = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "N"];
        let compassSector = [
            "⬆️",
            "↗️",
            "↗️",
            "↗️",
            "➡️",
            "↘️",
            "↘️",
            "↘️",
            "⬇️",
            "↙️",
            "↙️",
            "↙️",
            "⬅️",
            "↖️",
            "↖️",
            "↖️",
            "⬆️",
        ];
        const windDirection = compassSector[(deg / 22.5).toFixed(0)];

        return `${time} | ${iconImageUrl} ${paddedDesc} | ${temperature.padStart(
            4
        )} | ${rainChance.padStart(3)}% | ${precipitation.padStart(
            4
        )}mm | ${pressure}hPa | ${windDirection} ${windSpeed.padStart(
            2
        )}km/h | ${humidity.toString().padStart(3)}%`;
    });

    return `${city}の天気 | 🌅${sunrise} / 🌇${sunset}\n${formattedData.join(
        "\n"
    )}`;
};

list = {
    "東京 新宿": "160-0022",
    "大阪 梅田": "530-0001",
    "京都 京都駅": "600-8216",
    "仙台 青葉区": "980-0803",
    "福岡 博多": "812-0012",
    "名古屋 栄": "460-0008",
    "横浜 みなとみらい": "220-0012",
};
k = Object.keys(list);
x = await tp.system.suggester(k, k);
zipCode = list[x]; // x は選択されたキー（例: "東京 新宿"）

const appid = // 取得した自分のapiを入力
const response = await fetch(
    `https://api.openweathermap.org/data/2.5/forecast?zip=${zipCode},JP&appid=${appid}`
);

const weatherData = await response.json();
// formatWeatherData を呼び出す際に、選択されたキー x を渡す
const result = formatWeatherData(weatherData, x);
-%>
<% result %>

-----------------------------------

## Diary：<% moment(tp.file.title).format("YYYY-MM-DD (ddd)") %>

-

## Memo

-

```

以下の部分を編集して、お好みの住所を追加・変更できます：

```javascript
list = {
    "東京 新宿": "160-0022",
    "大阪 梅田": "530-0001",
    "京都 京都駅": "600-8216",
    "仙台 青葉区": "980-0803",
    "福岡 博多": "812-0012",
    "名古屋 栄": "460-0008",
    "横浜 みなとみらい": "220-0012",
    // ここに新しい住所を追加
    "広島 中区": "730-0000",
    "沖縄 那覇": "900-0000",
};
```

:::note warn
住所によっては実行時エラーが出る場合があります。その場合は実行できる近い住所を指定してください。
:::

また、任意のタイミング・箇所で天気情報を取得・インサートできるように、天気情報機能だけを抽出したTemplaterファイルも作ります。以下の内容を`template/templater`内にmd形式で保存してください。

<details><summary>天気情報機能抽出Templaterファイル（ほぼ同じ内容なので格納）</summary>

```md title:weather_insert.md.md
<%*
const formatTime = (dateTimeStr) => {
    const date = new Date(dateTimeStr);
    return date.toLocaleTimeString("ja-JP", {
        hour: "2-digit",
        minute: "2-digit",
        hour12: false,
    });
};

const padJapanese = (str, length) => {
    const padding = "　".repeat(Math.max(0, length - str.length));
    return str + padding;
};

const getJapaneseDescription = (description) => {
    const descMap = {
        "clear sky": "快晴",
        "few clouds": "晴れ",
        "scattered clouds": "晴れ時々曇り",
        "broken clouds": "所により曇り",
        "overcast clouds": "曇り",
        "light rain": "小雨",
        "shower rain": "霧雨",
        "moderate rain": "雨",
        thunderstorm: "雷雨",
        "heavy rain": "大雨",
        snow: "雪",
        "light snow": "小雪",
        mist: "霧",
        "thunderstorm with light rain": "弱い雨を伴う雷雨",
        "thunderstorm with rain": "雨を伴う雷雨",
        "thunderstorm with heavy rain": "強い雨を伴う雷雨",
        "light thunderstorm": "弱い雷雨",
        "heavy thunderstorm": "強い雷雨",
        "ragged thunderstorm": "断続的な雷雨",
        "thunderstorm with light drizzle": "弱い霧雨を伴う雷雨",
        "thunderstorm with drizzle": "霧雨を伴う雷雨",
        "thunderstorm with heavy drizzle": "強い霧雨を伴う雷雨",
        "light intensity drizzle": "弱い霧雨",
        drizzle: "霧雨",
        "heavy intensity drizzle": "強い霧雨",
        "light intensity drizzle rain": "弱い霧雨混じりの雨",
        "drizzle rain": "霧雨混じりの雨",
        "heavy intensity drizzle rain": "強い霧雨混じりの雨",
        "shower rain and drizzle": "霧雨を伴うにわか雨",
        "heavy shower rain and drizzle": "強い霧雨を伴うにわか雨",
        "shower drizzle": "にわか霧雨",
        rain: "雨",
        "heavy intensity rain": "激しい雨",
        "very heavy rain": "非常に強い雨",
        "extreme rain": "猛烈な雨",
        "freezing rain": "凍雨",
        "light intensity shower rain": "弱いにわか雨",
        "heavy intensity shower rain": "強いにわか雨",
        "ragged shower rain": "断続的なにわか雨",
        "heavy snow": "大雪",
        sleet: "みぞれ",
        "light shower sleet": "弱いにわかみぞれ",
        "shower sleet": "にわかみぞれ",
        "light rain and snow": "弱い雨雪",
        "rain and snow": "雨雪",
        "light shower snow": "弱いにわか雪",
        "shower snow": "にわか雪",
        "heavy shower snow": "強いにわか雪",
        smoke: "煙",
        haze: "もや",
        "sand/dust whirls": "砂塵旋風",
        fog: "濃霧",
        sand: "砂",
        dust: "粉塵",
        "volcanic ash": "火山灰",
        squalls: "スコール",
        tornado: "竜巻",
    };
    return descMap[description] || description;
};

const convertTimestamp = (timestamp) => {
    const date = new Date(timestamp * 1000);
    const hours = date.getHours().toString().padStart(2, "0");
    const minutes = date.getMinutes().toString().padStart(2, "0");
    return `${hours}:${minutes}`;
};

// formatWeatherData 関数に selectedCityName 引数を追加
const formatWeatherData = (weatherData, selectedCityName) => {
    // weatherData.city.name ではなく selectedCityName を使用
    const city = selectedCityName;
    const { list } = weatherData;

    const sunrise = convertTimestamp(weatherData.city.sunrise); // 日の出時刻
    const sunset = convertTimestamp(weatherData.city.sunset); // 日の入時刻

    const formattedData = list.slice(0, 8).map((period) => {
        const {
            dt_txt,
            weather,
            main: { temp, pressure, humidity },
            pop,
            rain,
            wind: { speed, deg },
        } = period;

        const time = formatTime(dt_txt);
        const iconCode = weather[0].icon;
        const iconImageUrl = `![icon|16](https://openweathermap.org/img/wn/${iconCode}@2x.png)`; // 画像URLを生成
        const description = getJapaneseDescription(weather[0].description);
        const paddedDesc = padJapanese(description, 6);
        const temperature = `${Math.round(temp - 273.15)}°C`;
        const rainChance = `${Math.round(pop * 100)}`;
        const precipitation = rain ? `${rain["3h"].toFixed(1)}` : "0.0";
        const windSpeed = `${Math.round(speed * 3.6)}`;
        //let compassSector = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "N"];
        let compassSector = [
            "⬆️",
            "↗️",
            "↗️",
            "↗️",
            "➡️",
            "↘️",
            "↘️",
            "↘️",
            "⬇️",
            "↙️",
            "↙️",
            "↙️",
            "⬅️",
            "↖️",
            "↖️",
            "↖️",
            "⬆️",
        ];
        const windDirection = compassSector[(deg / 22.5).toFixed(0)];

        return `${time} | ${iconImageUrl} ${paddedDesc} | ${temperature.padStart(
            4
        )} | ${rainChance.padStart(3)}% | ${precipitation.padStart(
            4
        )}mm | ${pressure}hPa | ${windDirection} ${windSpeed.padStart(
            2
        )}km/h | ${humidity.toString().padStart(3)}%`;
    });

    return `${city}の天気 | 🌅${sunrise} / 🌇${sunset}\n${formattedData.join(
        "\n"
    )}`;
};

list = {
    "東京 新宿": "160-0022",
    "大阪 梅田": "530-0001",
    "京都 京都駅": "600-8216",
    "仙台 青葉区": "980-0803",
    "福岡 博多": "812-0012",
    "名古屋 栄": "460-0008",
    "横浜 みなとみらい": "220-0012",
};
k = Object.keys(list);
// tp.system.suggester の結果を x に格納
x = await tp.system.suggester(k, k);
zipCode = list[x]; // x は選択されたキー（例: "津田沼"）

console.log(zipCode); // 郵便番号を確認

const appid = // 取得した自分のapiを入力
const response = await fetch(
    `https://api.openweathermap.org/data/2.5/forecast?zip=${zipCode},JP&appid=${appid}`
);

const weatherData = await response.json();
// formatWeatherData を呼び出す際に、選択されたキー x を渡す
return formatWeatherData(weatherData, x);
-%>


```

</details>

## 使用方法

### デイリーノートでの使用

1. 新しいデイリーノートを作成
2. テンプレート実行時に住所選択ダイアログが表示される
3. 希望の住所を選択
4. 天気情報が自動挿入されたデイリーノートが開く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665424/664c3128-7224-4881-b559-ffbd55fd3237.png)

### 任意タイミングでの使用

1. コマンドパレット（Windowsなら`Ctrl + P`）を開く
2. 「`Templater: Insert template/templater/weather_insert.md`」を選択
3. 住所選択ダイアログが表示される
4. 希望の住所を選択
5. 天気情報が挿入される

## 注意事項

- OpenWeatherMap APIキーは実際のものに置き換えてください
- 郵便番号は日本の形式（7桁）で入力してください
- インターネット接続が必要です
  - オフラインでもデイリーノートを作成したいなら、デイリーノートから天気情報取得コード部分は消して、ネット接続があるときに`weather_insert.md`でインサートするような運用にすると良いです

## まとめ

今回の改善により、以下の機能が実現されました：

1. **場所選択機能**: 実行時に複数の住所から選択可能
2. **柔軟な使用**: デイリーノート作成時以外でも天気取得可能

素敵なObsidianライフを🖖。