Commit 98450f89 authored by Heisenberg5124's avatar Heisenberg5124
Browse files

Initial commit

parent f2aa86d9
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
// pnpm add -D tsx @types/node @napi-rs/canvas
// run: pnpm exec tsx scripts/make-ndvi-tiles.ts 9.16 48.77 9.19 48.79 14 16 2000 2025
import {createCanvas} from '@napi-rs/canvas';
import {mkdirSync, writeFileSync} from 'fs';
import {join} from 'path';
const [, , minLonS, minLatS, maxLonS, maxLatS, zMinS, zMaxS, startYearS, endYearS] = process.argv;
if (!endYearS) {
console.error('Usage: tsx scripts/make-ndvi-tiles.ts <minLon> <minLat> <maxLon> <maxLat> <zMin> <zMax> <startYear> <endYear>');
process.exit(1);
}
const minLon = parseFloat(minLonS), minLat = parseFloat(minLatS);
const maxLon = parseFloat(maxLonS), maxLat = parseFloat(maxLatS);
const zMin = parseInt(zMinS), zMax = parseInt(zMaxS);
const startYear = parseInt(startYearS), endYear = parseInt(endYearS);
const TILE = 256;
// tile math
const lon2x = (lon: number, z: number) => Math.floor(((lon + 180) / 360) * 2 ** z);
const lat2y = (lat: number, z: number) => {
const r = lat * Math.PI / 180;
return Math.floor((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2 * 2 ** z);
};
const x2lon = (x: number, z: number) => x / 2 ** z * 360 - 180;
const y2lat = (y: number, z: number) => {
const n = Math.PI - 2 * Math.PI * y / 2 ** z;
return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
};
// deterministic pseudo-noise
const noise = (lon: number, lat: number, seed = 0) => {
const s = Math.sin(lon * 12.9898 + lat * 78.233 + seed) * 43758.5453;
return s - Math.floor(s);
};
// synthetic NDVI in [-0.2, 0.8], trending slightly by year
function ndviAt(lon: number, lat: number, year: number) {
const phase = (year - 2000) * 0.6;
let v = 0.35
+ 0.25 * Math.sin((lon + phase) * 0.2)
+ 0.20 * Math.cos((lat - phase) * 0.3)
+ 0.10 * (noise(lon, lat, 1) - 0.5);
// a “construction” spot near bbox center that reduces NDVI after 2018
const cx = (minLon + maxLon) / 2, cy = (minLat + maxLat) / 2;
const dx = lon - cx, dy = lat - cy;
const r = Math.sqrt(dx * dx + dy * dy);
if (r < 0.01) v -= (year >= 2019 ? 0.25 : 0.05);
return Math.max(-0.2, Math.min(0.8, v));
}
// color ramp
type RGB = [number, number, number];
const stops: [number, RGB][] = [
[-0.2, [165, 42, 42]],
[0.0, [255, 255, 224]],
[0.2, [173, 255, 47]],
[0.6, [0, 128, 0]]
];
const interpColor = (v: number): RGB => {
for (let i = 0; i < stops.length - 1; i++) {
const [v1, c1] = stops[i], [v2, c2] = stops[i + 1];
if (v >= v1 && v <= v2) {
const t = (v - v1) / (v2 - v1);
return [0, 1, 2].map(k => Math.round(c1[k] + t * (c2[k] - c1[k]))) as RGB;
}
}
return v < stops[0][0] ? stops[0][1] : stops.at(-1)![1];
};
for (let year = startYear; year <= endYear; year++) {
console.log(`→ year ${year}`);
for (let z = zMin; z <= zMax; z++) {
const xMin = lon2x(minLon, z), xMax = lon2x(maxLon, z);
const yMin = lat2y(maxLat, z), yMax = lat2y(minLat, z); // y grows south
for (let x = xMin; x <= xMax; x++) {
for (let y = yMin; y <= yMax; y++) {
const canvas = createCanvas(TILE, TILE);
const ctx = canvas.getContext('2d');
const lonL = x2lon(x, z), lonR = x2lon(x + 1, z);
const latT = y2lat(y, z), latB = y2lat(y + 1, z);
const img = ctx.createImageData(TILE, TILE);
let p = 0;
for (let py = 0; py < TILE; py++) {
const lat = latT + (latB - latT) * (py + 0.5) / TILE;
for (let px = 0; px < TILE; px++) {
const lon = lonL + (lonR - lonL) * (px + 0.5) / TILE;
const v = ndviAt(lon, lat, year);
const [r, g, b] = interpColor(v);
img.data[p++] = r;
img.data[p++] = g;
img.data[p++] = b;
img.data[p++] = 220;
}
}
ctx.putImageData(img, 0, 0);
const out = join(process.cwd(), 'public', 'tiles', `ndvi_${year}`, `${z}`, `${x}`);
mkdirSync(out, {recursive: true});
writeFileSync(join(out, `${y}.png`), canvas.toBuffer('image/png'));
}
}
}
}
console.log('✅ Done. Tiles under /public/tiles/ndvi_{year}/{z}/{x}/{y}.png');
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"types": [
"vite/client"
],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src"
]
}
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from "@tailwindcss/vite";
import path from "path"
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment