mirror of
https://github.com/luxfi/tel.git
synced 2026-10-30 15:15:54 +00:00
Globe.tsx was 23% comment. The code says what it does; the comments now say only what it cannot — why sidereal and not a phase, why one seam carries no cross-links, why a terminal tracks one satellite. Same for Nav, the tests, the workflow and LLM.md. Co-authored-by: Hanzo Dev <[email protected]>
92 lines
3.2 KiB
JavaScript
92 lines
3.2 KiB
JavaScript
/*
|
||
Rasterises coastlines into `src/content/land.ts`.
|
||
Run: node scripts/land.mjs path/to/countries.geojson
|
||
|
||
Scanline by latitude row, even-odd fill — so lakes fall out without ring winding,
|
||
and it is ~200x less work than point-in-polygon per cell.
|
||
*/
|
||
import { readFileSync, writeFileSync } from 'node:fs'
|
||
|
||
const STEP = 1.5
|
||
const COLS = Math.round(360 / STEP)
|
||
const ROWS = Math.round(180 / STEP)
|
||
|
||
const src = process.argv[2]
|
||
if (!src) {
|
||
console.error('usage: node scripts/land.mjs <countries.geojson>')
|
||
process.exit(1)
|
||
}
|
||
|
||
/** Every ring of every feature, flattened — the fill rule does not care which is which. */
|
||
const rings = []
|
||
for (const f of JSON.parse(readFileSync(src, 'utf8')).features) {
|
||
const g = f.geometry
|
||
if (!g) continue
|
||
if (g.type === 'Polygon') rings.push(...g.coordinates)
|
||
else if (g.type === 'MultiPolygon') for (const poly of g.coordinates) rings.push(...poly)
|
||
}
|
||
|
||
const bits = new Uint8Array(Math.ceil((COLS * ROWS) / 8))
|
||
const set = (col, row) => {
|
||
const i = row * COLS + col
|
||
bits[i >> 3] |= 1 << (i & 7)
|
||
}
|
||
|
||
for (let row = 0; row < ROWS; row++) {
|
||
// Cell CENTRE, not its edge: a row sampled at its boundary sits exactly on the
|
||
// coastline for any coast that runs east-west, and picks up a rounding coin flip.
|
||
const lat = -90 + (row + 0.5) * STEP
|
||
const xs = []
|
||
for (const ring of rings) {
|
||
for (let i = 0; i < ring.length - 1; i++) {
|
||
const [lon1, lat1] = ring[i]
|
||
const [lon2, lat2] = ring[i + 1]
|
||
// Half-open in latitude so a vertex exactly on the row counts once, not twice.
|
||
if (lat1 <= lat === lat2 <= lat) continue
|
||
xs.push(lon1 + ((lat - lat1) / (lat2 - lat1)) * (lon2 - lon1))
|
||
}
|
||
}
|
||
if (xs.length < 2) continue
|
||
xs.sort((a, b) => a - b)
|
||
for (let i = 0; i + 1 < xs.length; i += 2) {
|
||
const from = Math.ceil((xs[i] + 180) / STEP - 0.5)
|
||
const to = Math.floor((xs[i + 1] + 180) / STEP - 0.5)
|
||
for (let col = Math.max(0, from); col <= Math.min(COLS - 1, to); col++) set(col, row)
|
||
}
|
||
}
|
||
|
||
let land = 0
|
||
for (const b of bits) land += b.toString(2).replace(/0/g, '').length
|
||
|
||
writeFileSync(
|
||
new URL('../src/content/land.ts', import.meta.url),
|
||
`/*
|
||
Where the land is — ${COLS}×${ROWS} cells of ${STEP}°, one bit each, row 0 at the
|
||
south pole, column 0 at 180°W. ${land} land cells. Generated by scripts/land.mjs.
|
||
|
||
A mask, not a list of dots: the renderer picks its own lattice and asks this.
|
||
*/
|
||
export const LAND_STEP = ${STEP}
|
||
export const LAND_COLS = ${COLS}
|
||
export const LAND_ROWS = ${ROWS}
|
||
|
||
const PACKED =
|
||
'${Buffer.from(bits).toString('base64')}'
|
||
|
||
const bits_ = typeof atob === 'function'
|
||
? Uint8Array.from(atob(PACKED), (c) => c.charCodeAt(0))
|
||
: Uint8Array.from(Buffer.from(PACKED, 'base64'))
|
||
|
||
/** True where (lat, lon) in degrees is over land. */
|
||
export function isLand(lat: number, lon: number): boolean {
|
||
const row = Math.floor((lat + 90) / LAND_STEP)
|
||
const col = Math.floor(((((lon + 180) % 360) + 360) % 360) / LAND_STEP)
|
||
if (row < 0 || row >= LAND_ROWS) return false
|
||
const i = row * LAND_COLS + col
|
||
return (bits_[i >> 3] & (1 << (i & 7))) !== 0
|
||
}
|
||
`,
|
||
)
|
||
|
||
console.log(`land.ts: ${COLS}x${ROWS} @ ${STEP}deg, ${land} land cells (${((land / (COLS * ROWS)) * 100).toFixed(1)}%), ${bits.length} bytes`)
|