雨雲レーダーのタイルサーバー

雨雲レーダーのタイルサーバー

雨雲レーダーのタイルを配信していますhata6502
https://storage.googleapis.com/precipitation-almap-430107/tiles/{z}/{x}/{y}.png

© OpenStreetMap contributors https://www.openstreetmap.org/copyright

仕様
現在時刻における情報を、3時間ごとに更新しています
レーダー反射強度(Composite radar reflectivity)のbandを使用しています
タイルを加工する必要性が出てきやすい
gdal_translateの-scaleによって、8ビットのグレースケールに量子化されています
zは0から2まで
3以降のズームで表示したい場合は、z=2のタイルを切り抜いて拡大する必要があります


タイルは、NOAA Global Forecast Systemの天気予報データから生成されています



Leafletでのレイヤー実装例
cloud-layer.ts
Copied!
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.data[i + 0];

imageData.data[i + 3] = precipitation >= 4 ? 255 : 0;

imageData.data[i + 0] =
imageData.data[i + 1] =
imageData.data[i + 2] =
255 - precipitation;
}
canvasContext.putImageData(imageData, 0, 0);

return canvas;
};


GDALによる雨雲タイル生成コード
update-precipitation.ts
Copied!
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 });
}


Powered by Helpfeel