雨雲レーダーのタイルサーバー
雨雲レーダーのタイルを配信していますhata6502.icon
https://storage.googleapis.com/precipitation-almap-430107/tiles/{z}/{x}/{y}.png
利用例 https://almap.hata6502.com/
https://gyazo.com/12e7e8fc04d5163b499e179075c950cd
© OpenStreetMap contributors https://www.openstreetmap.org/copyright
仕様
現在時刻における情報を、3時間ごとに更新しています
データ源 gfs.????????/??/atmos/gfs.t??z.pgrb2.0p25.f???
レーダー反射強度(Composite radar reflectivity)のbandを使用しています
タイルを加工する必要性が出てきやすい
gdal_translateの-scaleによって、8ビットのグレースケールに量子化されています
zは0から2まで
3以降のズームで表示したい場合は、z=2のタイルを切り抜いて拡大する必要があります
CC0 1.0 Universal Licenseとします
タイルは、NOAA Global Forecast Systemの天気予報データから生成されています
https://console.cloud.google.com/marketplace/product/noaa-public/gfs
https://www.weather.gov/disclaimer
Leafletでのレイヤー実装例
code: cloud-layer.ts
import L from "leaflet";
const tileCache = new Map<string, Promise<HTMLCanvasElement>>();
const createTile: L.GridLayer"createTile" = function (
this: L.GridLayer,
coords,
done,
) {
const canvas = document.createElement("canvas");
const tileSize = this.getTileSize();
canvas.style.width = ${tileSize.x}px;
canvas.style.height = ${tileSize.y}px;
(async () => {
try {
const z = Math.min(coords.z, 2);
const zoomFactor = 2 ** (coords.z - z);
const x = Math.floor(coords.x / zoomFactor);
const y = Math.floor(coords.y / zoomFactor);
const tileCacheKey = ${x}-${y}-${z};
const cachedTile = tileCache.get(tileCacheKey);
const cachingTile = cachedTile ?? fetchTile({ x, y, z });
tileCache.set(tileCacheKey, cachingTile);
const tile = await cachingTile;
canvas.width = tile.width;
canvas.height = tile.height;
const canvasContext = canvas.getContext("2d");
if (!canvasContext) {
throw new Error("Failed to get canvas context");
}
canvasContext.drawImage(
tile,
-canvas.width * (coords.x - x * zoomFactor),
-canvas.height * (coords.y - y * zoomFactor),
canvas.width * zoomFactor,
canvas.height * zoomFactor,
);
done(undefined, canvas);
} catch (exception) {
if (!(exception instanceof Error)) {
throw exception;
}
console.error(exception);
done(exception, canvas);
}
})();
return canvas;
};
const options: L.GridLayerOptions = {
className: "leaflet-cloud-layer",
opacity: 0.375,
};
export const CloudLayer = L.GridLayer.extend({ createTile, options });
const fetchTile = async ({ x, y, z }: { x: number; y: number; z: number }) => {
const tileResponse = await fetch(
new URL(
${encodeURIComponent(z)}/${encodeURIComponent(x)}/${encodeURIComponent(y)}.png,
"https://storage.googleapis.com/precipitation-almap-430107/tiles/",
),
);
if (!tileResponse.ok) {
throw new Error(
Failed to fetch tile: ${tileResponse.status} ${tileResponse.statusText},
);
}
const imageBitmap = await createImageBitmap(await tileResponse.blob());
const canvas = document.createElement("canvas");
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const canvasContext = canvas.getContext("2d");
if (!canvasContext) {
throw new Error("Failed to get canvas context");
}
canvasContext.drawImage(imageBitmap, 0, 0);
const imageData = canvasContext.getImageData(
0,
0,
canvas.width,
canvas.height,
);
for (let i = 0; i < imageData.data.length; i += 4) {
const precipitation = imageData.datai + 0;
imageData.datai + 3 = precipitation >= 4 ? 255 : 0;
imageData.datai + 0 =
imageData.datai + 1 =
imageData.datai + 2 =
255 - precipitation;
}
canvasContext.putImageData(imageData, 0, 0);
return canvas;
};
GDALによる雨雲タイル生成コード
code: update-precipitation.ts
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { Storage, TransferManager } from "@google-cloud/storage";
import { $ } from "zx";
import { getPrecipitationBucketName } from "./env.js";
const tempDirectory = await mkdtemp(
path.join(tmpdir(), "update-precipitation-"),
);
try {
const grib2File = path.join(tempDirectory, "gfs.grb2");
const gfsDate = new Date();
const forcastHour = gfsDate.getUTCHours() % 6 < 3 ? 6 : 9;
gfsDate.setUTCHours(gfsDate.getUTCHours() - (gfsDate.getUTCHours() % 6) - 6);
const grib2URL = `https://storage.googleapis.com/global-forecast-system/gfs.${encodeURIComponent(
gfsDate.getUTCFullYear(),
)}${encodeURIComponent(
String(gfsDate.getUTCMonth() + 1).padStart(2, "0"),
)}${encodeURIComponent(
String(gfsDate.getUTCDate()).padStart(2, "0"),
)}/${encodeURIComponent(
String(gfsDate.getUTCHours()).padStart(2, "0"),
)}/atmos/gfs.t${encodeURIComponent(
String(gfsDate.getUTCHours()).padStart(2, "0"),
)}z.pgrb2.0p25.f${encodeURIComponent(String(forcastHour).padStart(3, "0"))}`;
console.log("Downloading GFS data from", grib2URL);
await $curl -o ${grib2File} ${grib2URL};
const gdalinfoOutput =
await $gdalinfo -json ${grib2File} | jq ${'.bands[] | select(.metadata[""].GRIB_COMMENT | contains("Composite radar reflectivity")) | .band'};
const band = gdalinfoOutput.stdout.trim();
console.log("Extracted band number:", band);
if (!/^\d+$/.test(band)) {
throw new Error(
Failed to extract band number from gdalinfo output: ${gdalinfoOutput.stdout},
);
}
const geoTIFFFile = path.join(tempDirectory, "precipitation.tif");
await $gdal_translate -b ${band} -ot Byte -scale -srcwin 1 1 1438 719 ${grib2File} ${geoTIFFFile};
const tilesDirectory = path.join(tempDirectory, "tiles");
await $gdal2tiles.py --s_srs EPSG:4326 --webviewer=none --xyz --zoom=0-2 ${geoTIFFFile} ${tilesDirectory};
await new TransferManager(
new Storage().bucket(getPrecipitationBucketName()),
).uploadManyFiles(tilesDirectory, {
customDestinationBuilder: (filePath) => {
const destination = path.relative(tempDirectory, filePath);
console.log("Uploading tile to:", destination);
return destination;
},
});
console.log("Precipitation tiles updated successfully.");
} finally {
await rm(tempDirectory, { recursive: true, force: true });
}
#個人開発
? 雨雲レーダーのタイルサーバー