78 lines
3.9 KiB
JavaScript
78 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-mercator-slice — a poster's tile rows must be equal on SCREEN, not equal in degrees.
|
|
*
|
|
* THE DEFECT: Simple Research's Export → poster showed far too much of Antarctica
|
|
* and comes out at the wrong, vertical aspect.
|
|
*
|
|
* The big-poster renderer stitches tiles, each drawn into an EQUAL-HEIGHT slot of the output. It picked
|
|
* each row's latitude band by dividing the degree span evenly — but screen height is proportional to
|
|
* ln(tan(pi/4 + lat/2)), not to latitude. So equal degree bands are unequal screen bands, and painting
|
|
* them into equal slots squeezes some and stretches others.
|
|
*
|
|
* ★ THE ASSERTION IS THE PROPERTY, AND THE CONTROL IS THE OLD CODE. Every band must occupy the same
|
|
* share of the rendered height, AND the degree-linear slicing it replaced must FAIL that same check —
|
|
* otherwise this passes on a tree where nothing was fixed.
|
|
*
|
|
* Exit 0 pass · 1 fail.
|
|
*/
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const { equalHeightLatBands, mercatorY, latAtMercatorY, MERCATOR_MAX_LAT } =
|
|
await import(resolve(ROOT, 'library/maps/mercator-slice.js'))
|
|
|
|
const fails = []
|
|
const shares = (bands, s, n) => {
|
|
const tot = mercatorY(n) - mercatorY(s)
|
|
return bands.map(([bs, bn]) => (mercatorY(bn) - mercatorY(bs)) / tot)
|
|
}
|
|
|
|
for (const [s, n, rows] of [[-60, 75, 4], [-85, 85, 6], [-10, 10, 3], [20, 60, 5], [-60, 75, 1]]) {
|
|
const bands = equalHeightLatBands(s, n, rows)
|
|
if (bands.length !== rows) { fails.push(`${rows} rows requested, ${bands.length} returned`); continue }
|
|
const sh = shares(bands, s, n)
|
|
const worst = Math.max(...sh.map((x) => Math.abs(x - 1 / rows)))
|
|
if (worst > 1e-9) {
|
|
fails.push(`bands of a ${s}..${n} view in ${rows} rows are not equal on screen: `
|
|
+ `shares ${sh.map((x) => (x * 100).toFixed(1) + '%').join(' ')}`)
|
|
}
|
|
// north-first, contiguous, and covering the whole span
|
|
if (Math.abs(bands[0][1] - Math.max(s, n)) > 1e-9) fails.push(`first band does not start at the north edge (${s}..${n})`)
|
|
if (Math.abs(bands[rows - 1][0] - Math.min(s, n)) > 1e-9) fails.push(`last band does not end at the south edge (${s}..${n})`)
|
|
for (let i = 1; i < rows; i++) {
|
|
if (Math.abs(bands[i][1] - bands[i - 1][0]) > 1e-9) fails.push(`bands ${i - 1}/${i} are not contiguous`)
|
|
}
|
|
}
|
|
|
|
// ★ THE CONTROL: the degree-linear slicing this replaced MUST fail the same property.
|
|
const linearBands = (s, n, rows) => {
|
|
const step = (n - s) / rows
|
|
return Array.from({ length: rows }, (_, r) => [n - (r + 1) * step, n - r * step])
|
|
}
|
|
const linWorst = (() => {
|
|
const sh = shares(linearBands(-60, 75, 4), -60, 75)
|
|
return Math.max(...sh.map((x) => Math.abs(x - 0.25)))
|
|
})()
|
|
if (linWorst <= 1e-9) {
|
|
fails.push('degree-linear slicing PASSES the equal-height assertion, so the assertion cannot detect '
|
|
+ 'the defect it exists for — it is measuring nothing.')
|
|
}
|
|
|
|
// The poles are undefined in Mercator; a world view must clamp rather than produce Infinity.
|
|
const polar = equalHeightLatBands(-90, 90, 4)
|
|
if (!polar.every(([a, b]) => Number.isFinite(a) && Number.isFinite(b))) {
|
|
fails.push('a -90..90 view produces non-finite bands; Mercator is undefined at the poles and must clamp')
|
|
}
|
|
if (Math.abs(polar[0][1]) > MERCATOR_MAX_LAT + 1e-6) fails.push('the north edge was not clamped to the Mercator limit')
|
|
if (Math.abs(latAtMercatorY(mercatorY(41.2)) - 41.2) > 1e-9) fails.push('mercatorY/latAtMercatorY are not inverses')
|
|
|
|
if (fails.length) {
|
|
console.error('MERCATOR-SLICE: FAIL')
|
|
for (const f of fails) console.error(` ✗ ${f}`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`MERCATOR-SLICE: PASS — every tile row occupies an equal share of the RENDERED height across five `
|
|
+ `view/row combinations, degree-linear slicing fails the same check (worst band off by ${(linWorst * 100).toFixed(1)} `
|
|
+ 'points), and a polar view clamps instead of going infinite.')
|