【7日目】discord.jsで地震情報を地図で表示する

# はじめに
こんにちは、なりかくんと申します。
5日目に、地震情報をEmbed化して表示しました。今回は、地震情報を地図にして表示したいと思います。
前回の記事をまだお読みでない方はぜひそちらからお読みください。

https://qiita.com/narikakun/items/c1b3e872ab453350ccd7

# 地図で表示する方法
まず、地震情報を地図で表示する方法ですがウェブページをスクリーンショットしてその画像を張り付けたいと思います。
Node.jsだけでも、d3.js等を使えば作れるのですが過去に作ったことがありますが画像生成の速度が圧倒的にスクリーンショットのほうが早いのでこちらを採用します。
ウェブページでは、Leaflet.jsのライブラリを使って地図を生成します。

今回は、私が開発している[nTool.onlineの地震情報マップ](https://ntool.online/weather/earthquake)を使います。以下の画像は過去の地震データですが、このように表示できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/c329c9ef-d70b-1d97-47a2-34fd36279924.png)

# スクリーンショットにpuppeteerを使う
今回、Node.jsでスクリーンショットを撮るのにpuppeteerを使います。インストールも簡単ですし、数行で使うことが出来るので非常におすすめなライブラリです。

インストールはいつも通りnpmで行えます。
```shell
npm install puppeteer
```

スクリーンショットを撮るコードは以下のようになります。
```js
const browser = await puppeteer.launch();

const page = await browser.newPage();
await page.goto('https://ntool.online/weather/earthquake?fullscreen=true');
await page.screenshot({ path: 'screenshot.png' });

await browser.close();
```
しかしこのままでは、完全に読み込まれていない状態で撮影されてしまうので完全に読み込まれてからかつ1秒待ってから撮影するようにします。
`page.goto()`に`{ waitUntil: 'networkidle0' }`を追加してあげることで完全に読み込まれるのを待つようにできます。
また、`new Promise(r => setTimeout(r, 1000));`を使うことで1秒（1000ミリ秒）待つことが出来ます。

これらを合わせると先ほどのコードがこのようになります。
```js
const browser = await puppeteer.launch();

const page = await browser.newPage();
await page.goto('https://ntool.online/weather/earthquake?fullscreen=true', { waitUntil: 'networkidle0' });
new Promise(r => setTimeout(r, 1000));
await page.screenshot({ path: 'screenshot.png' });

await browser.close();
```

あとこのまま撮影するとウィンドウサイズが微妙に小さいので少し大きくします。
`puppeteer.launch()`を
```js
puppeteer.launch({
    defaultViewport: {
        width: 960,
        height: 540
    }
});
```
にすると、960×540の解像度のウィンドウで撮影してくれます。

# これらをBotに組み合わせる
では、これらを以前作った地震情報のコマンドに組み込むと、以下のようになります。
```js:earthquake.js
const { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } = require('discord.js');
const request = require("request");
const puppeteer = require('puppeteer');

module.exports = {
	data: new SlashCommandBuilder()
		.setName('earthquake')
		.setDescription('地震情報を取得'),
	async execute(interaction) {
		await interaction.deferReply();
		let getData = await getEarthquake();
		const embed = new EmbedBuilder()
			.setAuthor({ name: getData.Control.Title })
			.setTimestamp(new Date(getData.Control.DateTime).getTime());
		if (getData.Control.Title == "震源・震度に関する情報" || getData.Control.Title == "震源に関する情報") {
			embed.addFields(
				{ name: '発生時間', value: getData.Body.Earthquake.OriginTime, inline: true },
				{ name: '震源地名', value: getData.Body.Earthquake.Hypocenter.Name, inline: true },
				{ name: '深さ', value: `${getData.Body.Earthquake.Hypocenter.Depth}km`, inline: true },
				{ name: '規模', value: `M${getData.Body.Earthquake.Magnitude}`, inline: true },
			);
		}
		if (getData.Control.Title == "震源・震度に関する情報" || getData.Control.Title == "震度速報") {
			let color = int_color(getData.Body.Intensity.Observation.MaxInt);
			embed.setColor(color);
			embed.addFields({ name: "最大深度", value: getData.Body.Intensity.Observation.MaxInt, inline: true });
			let intensity = [];
			for (const prefObj of getData.Body.Intensity.Observation.Pref) {
				for (const areaObj of prefObj.Area) {
					intensity.push(`${areaObj.Name} [${areaObj.MaxInt}]`);
				}
			}
			embed.addFields({ name: "各地の震度", value: `\`\`\`\n${intensity}\n\`\`\`` });
		}
		embed.addFields({ name: "コメント", value: getData.Head.Headline });
		const browser = await puppeteer.launch({
			defaultViewport: {
				width: 960,
				height: 540
			}
		});
		const page = await browser.newPage();
		await page.goto('https://ntool.online/weather/earthquake?fullscreen=true', { waitUntil: 'networkidle0' });
		new Promise(r => setTimeout(r, 1000));
		let screenshot = await page.screenshot({ path: 'screenshot.png' });
		embed.setImage("attachment://screenshot.png");
		const attachment = new AttachmentBuilder(screenshot,  { name: 'screenshot.png' });
		await browser.close();
		await interaction.editReply({ files: [attachment], embeds: [embed] });
		function getEarthquake() {
			return new Promise((resolve, reject) => {
				request({
					url: "https://dev.narikakun.net/webapi/earthquake/post_data.json",
					json: true
				}, function (error, response, body) {
					if (error) {
						reject(error);
					} else {
						resolve(body);
					}
				});
			});
		}
		function int_color(int) {
			switch (int) {
				case "1":
					return 0x46646E;
				case "2":
					return 0x1E6EE6;
				case "3":
					return 0x00C8C8;
				case "4":
					return 0xFAFA64;
				case "5-":
					return 0xFFB400;
				case "5+":
					return 0xFF7800;
				case "6-":
					return 0xE60000;
				case "6+":
					return 0xA00000;
				case "7":
					return 0x960096;
				default:
					return 0x000000;
			}
		}
	},
};
```
追加・変更した点としては、まずスクリーンショットの撮影に時間がかかります。
Discord APIは、スラッシュコマンドなどの応答は３秒以内でないとエラーが出ますので最初にフォローアップします。
```js
await interaction.deferReply();
```
そのあとに編集で更新します。
```js
await interaction.editReply({ files: [attachment], embeds: [embed] });
```

また、画像を扱うので、以下のように`AttachmentBuilder`を利用します。
```js
let screenshot = await page.screenshot({ path: 'screenshot.png' });
embed.setImage("attachment://screenshot.png");
const attachment = new AttachmentBuilder(screenshot,  { name: 'screenshot.png' });
```

このコマンドを実行すると、以下のように地震情報の画像を貼り付けることが出来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/07318c66-a6cf-dd75-1eb0-0b575fa7772b.png)

以上です、最後までお読みいただきありがとうございました。