diff --git a/.gitignore b/.gitignore index e53ee7c..f360113 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ /memory.json /config/** !/config/example.yaml -/src/feature/earthquake/generateImage/data.json +/src/feature/earthquake/generateImage/data*.json diff --git a/config/example.yaml b/config/example.yaml index 5059566..b31e541 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -12,8 +12,6 @@ earthquake: # 例: 30を指定すると、最大震度が震度3以上の地震発生情報のみを投稿します。 # 10(震度1), 20(震度2), 30(震度3), 40(震度4), 45(震度5弱), 50(震度5強), 55(震度6弱), 60(震度6強), 70(震度7)が有効です。 requireMaxScale: 30 - # 最大震度が不明な地震発生情報を投稿するかどうか boolean - canUnknownMaxScale: true # 過去のデバッグ用データを使用して地震情報を配信するかどうか boolean # デバッグ用途のみで使用してください。 useHistoryData: false diff --git a/src/feature/earthquake/generateImage/assets/epicenter.png b/src/feature/earthquake/generateImage/assets/epicenter.png index 515e605..ca8d7e8 100644 Binary files a/src/feature/earthquake/generateImage/assets/epicenter.png and b/src/feature/earthquake/generateImage/assets/epicenter.png differ diff --git a/src/feature/earthquake/generateImage/assets/information.png b/src/feature/earthquake/generateImage/assets/information.png index 7f98fe0..1931737 100644 Binary files a/src/feature/earthquake/generateImage/assets/information.png and b/src/feature/earthquake/generateImage/assets/information.png differ diff --git a/src/feature/earthquake/generateImage/index.ts b/src/feature/earthquake/generateImage/index.ts index 1d9ac3c..f065cc1 100644 --- a/src/feature/earthquake/generateImage/index.ts +++ b/src/feature/earthquake/generateImage/index.ts @@ -153,12 +153,12 @@ function getEdge(arr: T, property: keyof T[number]) { export default async function generateImage(message: any) { if ( - message.earthquake.hypocenter === undefined && + message.earthquake.hypocenter === undefined || message.points === undefined ) return "input_lack"; - const ZOOM_LEVEL = 8; + const ZOOM_LEVEL = 9; // タイル・地点取得 const tiles: { @@ -278,23 +278,120 @@ export default async function generateImage(message: any) { scale: number; }))[] = []; - // タイルのXYの各最大、最小を取得 - const tileXCount = getEdge(tiles, "tileX"); - const tileYCount = getEdge(tiles, "tileY"); + // タイルの幅、最大最小 + let tileXCount = getEdge(tiles, "tileX"); + let tileYCount = getEdge(tiles, "tileY"); + let xSize = tileXCount.most - tileXCount.least + 1; + let ySize = tileYCount.most - tileYCount.least + 1; - // タイルサイズ決定 + // タイルサイズ仮計算 let tileSize: number; - const xSize = tileXCount.most - tileXCount.least + 1; - const ySize = tileYCount.most - tileYCount.least + 1; + xSize = tileXCount.most - tileXCount.least + 1; + ySize = tileYCount.most - tileYCount.least + 1; + if (xSize > ySize) { tileSize = Math.round(WIDTH / xSize); } else { tileSize = Math.round(HEIGHT / ySize); } + // 全画面 + if (WIDTH - xSize * tileSize > 10) { + const requireTilesX = Math.ceil(WIDTH / tileSize); + const halfTilesX = Math.ceil(requireTilesX / 2); + + tileXCount = { + least: tileXCount.least - halfTilesX, + most: tileXCount.most + halfTilesX, + } + } + + if (HEIGHT - ySize * tileSize > 10) { + const requireTilesY = Math.ceil(HEIGHT / tileSize); + const halfTilesY = Math.ceil(requireTilesY / 2); + + tileYCount = { + least: tileYCount.least - halfTilesY, + most: tileYCount.most + halfTilesY, + } + } + + // 震源を中心とする + const epicenterPosition = positions.filter(position => position.type === "epicenter")[0]; + if (!epicenterPosition) + return "input_lack"; + + const epicenterTile = tiles[epicenterPosition.tileIndex]; + if (!epicenterTile) + return "input_lack"; + + const leftPx = (epicenterTile.tileX - tileXCount.least) * tileSize + epicenterPosition.innerX; + const rightPx = xSize * tileSize - leftPx; + const topPx = (epicenterTile.tileY - tileYCount.least) * tileSize + epicenterPosition.innerY; + const bottomPx = ySize * tileSize - topPx; + + if (leftPx > rightPx) { + const leftTiles = epicenterTile.tileX - tileXCount.least; + tileXCount = { + ...tileXCount, + most: epicenterTile.tileX + leftTiles, + } + } else { + const rightTiles = tileXCount.most - epicenterTile.tileX; + tileXCount = { + ...tileXCount, + least: epicenterTile.tileX - rightTiles, + } + } + + if (topPx > bottomPx) { + const topTiles = epicenterTile.tileY - tileYCount.least; + tileYCount = { + ...tileYCount, + most: epicenterTile.tileY + topTiles, + } + } else { + const bottomTiles = tileYCount.most - epicenterTile.tileY; + tileYCount = { + ...tileYCount, + least: epicenterTile.tileY - bottomTiles, + } + } + + // タイルサイズ再計算 + xSize = tileXCount.most - tileXCount.least + 1; + ySize = tileYCount.most - tileYCount.least + 1; + + if (xSize > ySize) { + tileSize = Math.round(WIDTH / xSize); + } else { + tileSize = Math.round(HEIGHT / ySize); + } + + // 欠けているタイルを取得 + for (let xIndex = 0; xIndex < xSize; xIndex++) { + const itX = xIndex + tileXCount.least; + + for (let yIndex = 0; yIndex < ySize; yIndex++) { + const itY = yIndex + tileYCount.least; + + const itTile = tiles.filter(tile => tile.tileX === itX && tile.tileY === itY)[0]; + if (itTile) + continue; + + tiles.push({ + tileX: itX, + tileY: itY, + }); + } + } + // アセット - const assets: Record> = {} + const assets: Record; + size: number; + }> = {} const assetsList = [ "scales/10", "scales/20", @@ -309,15 +406,20 @@ export default async function generateImage(message: any) { "epicenter", ]; - assetsList.forEach(name => { - const asset = readFileSync(`${import.meta.dirname}/assets/${name}.png`); - + await Promise.all(assetsList.map(async name => { const key = name.split("/").at(-1); if (!key) return; - assets[key] = asset; - }); + const asset = readFileSync(`${import.meta.dirname}/assets/${name}.png`); + const assetMeta = await sharp(asset).metadata(); + const assetSize = assetMeta.width; + + assets[key] = { + buffer: asset, + size: assetSize, + } + })); // 各画像処理 await Promise.all(tiles.map(async (tile, index) => { @@ -371,9 +473,9 @@ export default async function generateImage(message: any) { compounds.push({ ...about, - input: asset, - top: Math.round(top + position.innerY * tileResizedSize), - left: Math.round(left + position.innerX * tileResizedSize), + input: asset.buffer, + top: Math.round(top + position.innerY * tileResizedSize - asset.size / 2), + left: Math.round(left + position.innerX * tileResizedSize - asset.size / 2), }); }); })); @@ -424,6 +526,4 @@ export default async function generateImage(message: any) { const buffer = await result.png().toBuffer(); return buffer; -} - -writeFileSync("result.png", await generateImage(JSON.parse(readFileSync(`${import.meta.dirname}/data.json`, "utf8")))); \ No newline at end of file +} \ No newline at end of file diff --git a/src/feature/earthquake/index.ts b/src/feature/earthquake/index.ts index 06055b8..649b0ee 100644 --- a/src/feature/earthquake/index.ts +++ b/src/feature/earthquake/index.ts @@ -7,6 +7,7 @@ import i18next from "i18next"; import { readFileSync } from "node:fs"; import { EOL } from "node:os"; import { WebSocket } from "ws"; +import generateImage from "@/feature/earthquake/generateImage"; await initI18n(); @@ -104,15 +105,6 @@ const processMessage = async (message: any) => { { console.log("地震発生情報を受信しました"); - if ( - (message.earthquake.maxScale === -1 || - message.earthquake.maxScale === undefined) && - !config.earthquake.canUnknownMaxScale - ) { - console.log("最大震度が不明であり、最大震度が不明な場合の投稿が許可されていないため、スキップします"); - break; - } - if ( message.earthquake.maxScale !== -1 && message.earthquake.maxScale < config.earthquake.requireMaxScale @@ -177,33 +169,61 @@ const processMessage = async (message: any) => { .join(EOL.repeat(2)) .trim(); - await createUeuse({ - text: i18next.t("earthquakeNotice", { - type: typeMessage[message.issue.type] ?? "地震情報", - occuredTime: format(new Date(message.earthquake.time), "yyyy年M月d日 H:mm"), - maxScale: scaleMessages[String(message.earthquake.maxScale)], - epicenter: message.earthquake.hypocenter.name === "" - ? "不明" - : message.earthquake.hypocenter.name, - magnitude: message.earthquake.hypocenter.magnitude === -1 - ? "不明" - : `M${message.earthquake.hypocenter.magnitude.toFixed(1)}`, - depth: message.earthquake.hypocenter.depth === 0 - ? "ごく浅い" - : (message.earthquake.hypocenter.depth === -1 - ? "不明" - : `${message.earthquake.hypocenter.depth}km`), - domesticTsunami: domesticTsunamiMessages[(message.earthquake.domesticTsunami ?? "Unknown")], - foreignTsunami: foreignTsunamiMessages[(message.earthquake.foreignTsunami ?? "Unknown")], - points: pointsMsg === "" - ? "" - : EOL.repeat(2) + pointsMsg, - source: message.issue.source ?? "不明", - comment: message.comments.freeFormComment === "" - ? "" - : EOL + message.comments.freeFormComment + EOL, - }), - }, "地震発生情報"); + let earthquakeUniqid: string | null = null; + + await Promise.allSettled([ + (async () => { + const ueuses = await createUeuse({ + text: i18next.t("earthquakeNotice", { + type: typeMessage[message.issue.type] ?? "地震情報", + occuredTime: format(new Date(message.earthquake.time), "yyyy年M月d日 H:mm"), + maxScale: scaleMessages[String(message.earthquake.maxScale)], + epicenter: message.earthquake.hypocenter.name === "" + ? "不明" + : message.earthquake.hypocenter.name, + magnitude: message.earthquake.hypocenter.magnitude === -1 + ? "不明" + : `M${message.earthquake.hypocenter.magnitude.toFixed(1)}`, + depth: message.earthquake.hypocenter.depth === 0 + ? "ごく浅い" + : (message.earthquake.hypocenter.depth === -1 + ? "不明" + : `${message.earthquake.hypocenter.depth}km`), + domesticTsunami: domesticTsunamiMessages[(message.earthquake.domesticTsunami ?? "Unknown")], + foreignTsunami: foreignTsunamiMessages[(message.earthquake.foreignTsunami ?? "Unknown")], + points: pointsMsg === "" + ? "" + : EOL.repeat(2) + pointsMsg, + source: message.issue.source ?? "不明", + comment: message.comments.freeFormComment === "" + ? "" + : EOL + message.comments.freeFormComment + EOL, + }), + }, "地震発生情報"); + + earthquakeUniqid = ueuses[0]?.uniqid ?? null; + })(), + (async () => { + const result = await generateImage(message); + + if (typeof result === "string") { + console.warn("情報が不足しているため、地震の画像生成ができませんでした"); + return; + } + + while (typeof (earthquakeUniqid as string | null) !== "string") {} + + await createUeuse({ + text: "この地震の震度分布画像を生成しました。", + media: { + photo: [ + result.toString("base64"), + ] + }, + reuseid: (earthquakeUniqid as unknown as string), + }, "震度分布画像"); + })(), + ]); } break; case 552: diff --git a/src/lib/client.ts b/src/lib/client.ts index a038d99..8fc050b 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -15,12 +15,15 @@ export default client; export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: string) => { const mem = Memory.memory; - const excessedMessage = "👉返信に続きがあります。"; + const excessedMessage = "**👉返信に続きがあります。**"; let lines = data.text.split(EOL); let firstUniqid = ""; let count = 0; + type ExtractSuccess = T extends { success: true } ? T : never; + const results: ExtractSuccess[] = []; + while (lines.length > 0) { count++; let currentText = ""; @@ -76,6 +79,7 @@ export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: s if (response.success) { success = true; postedUniqid = response.uniqid; + results.push(response); break; } @@ -99,4 +103,6 @@ export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: s lines.shift(); } } + + return results; } \ No newline at end of file diff --git a/src/lib/config.ts b/src/lib/config.ts index 960c708..6b936d0 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -20,7 +20,6 @@ const schema = z.object({ z.literal(60), z.literal(70), ]), - canUnknownMaxScale: z.boolean(), useHistoryData: z.boolean(), reconnectInterval: z.number().positive(), }),