DRV1650-SR-POSTER-EXPORT, the "too much Antarctica" half. The big-poster renderer
stitches a grid of tiles, each drawn into an EQUAL-HEIGHT slot of the output image, and
it chose each row's latitude band by dividing the degree span evenly:
latStep = (ne.lat - sw.lat) / rows
Screen height is proportional to ln(tan(pi/4 + lat/2)), not to latitude, so equal DEGREE
bands are not equal SCREEN bands. MEASURED on a -60..75 view in 4 rows: the top band is
37.0% of the true rendered height and was being painted into 25% of the poster; the next
19.7% into 25%; the bottom 25.2% into 25%. Every row is squeezed or stretched, which is
the wrong aspect, and the bands where a degree buys the most pixels are the most
over-represented -- the Antarctica the Driver saw.
Sliced in Mercator y instead, every row is 25.0%, exactly, because that IS the screen
coordinate.
★ THIRD PLACE TODAY FOR THE SAME MISTAKE. A whiteboard chart nearly shipped with a linear
scale into degrees; a measurement harness nearly reported a false threshold by correcting
the wrong axis; and this has been shipping. Treating latitude as a screen coordinate is
the error, the conversion is one line, and it now lives in library/maps/mercator-slice.js
rather than being rediscovered a fourth time.
check-mercator-slice asserts the PROPERTY across five view/row combinations -- every band
an equal share of rendered height, contiguous, covering the span, north-first -- and its
CONTROL is the old code: degree-linear slicing must FAIL the same assertion, which it does
by 12.0 points. Without that control the test would pass on a tree where nothing changed.
A -90..90 view clamps at the Mercator limit instead of going infinite.
NOT FIXED HERE, because it is a product decision and not a defect: the export also asks
for an 8.5x11 PORTRAIT sheet (research-map.vue:1410), so a landscape map is letterboxed
onto a tall page. That is a deliberate default, it is the other half of "the wrong
vertical aspect", and changing the shape of the Driver's poster is his call, not mine.
57 lines
2.8 KiB
JavaScript
57 lines
2.8 KiB
JavaScript
/**
|
|
* mercator-slice.js — cut a map's bounds into EQUAL-HEIGHT rows, which is not the same as equal degrees.
|
|
*
|
|
* ★ THE DEFECT THIS EXISTS FOR (Driver, 2026-09-28, Simple Research's poster export): the poster "shows
|
|
* too much of Antarctica" and comes out at the wrong, vertical aspect.
|
|
*
|
|
* The big-poster renderer stitches a grid of tiles, each rendered into an EQUAL-HEIGHT slot in the
|
|
* output image. It chose each tile's latitude band by dividing the degree span evenly:
|
|
*
|
|
* latStep = (ne.lat - sw.lat) / rows
|
|
*
|
|
* Web Mercator is not linear in latitude — screen y is proportional to ln(tan(pi/4 + lat/2)) — so equal
|
|
* DEGREE bands are not equal SCREEN bands. MEASURED on a -60..75 view in 4 rows: the top band is 37.0%
|
|
* of the true screen height and is drawn into 25% of the poster; the next is 19.7% drawn into 25%. Every
|
|
* row is squeezed or stretched, which is the wrong aspect, and the high-latitude bands — where a degree
|
|
* is worth the most pixels — are the most over-represented, which is the Antarctica the Driver saw.
|
|
*
|
|
* Slicing in MERCATOR Y instead gives 25.0% per row exactly, because that IS the screen coordinate.
|
|
*
|
|
* ★ SAME MISTAKE, THIRD PLACE TODAY. A whiteboard chart nearly shipped with a linear scale into degrees,
|
|
* and a measurement harness nearly reported a false threshold by correcting the wrong axis. Anything
|
|
* that treats latitude as a screen coordinate is wrong; the conversion is one line and it belongs
|
|
* somewhere shared rather than being rediscovered.
|
|
*/
|
|
|
|
/** Web Mercator's y for a latitude, in the same units the projection's x uses (radians of longitude). */
|
|
export const mercatorY = (lat) => Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI / 180) / 2))
|
|
|
|
/** The inverse: the latitude at a Mercator y. */
|
|
export const latAtMercatorY = (y) => (Math.atan(Math.sinh(y)) * 180) / Math.PI
|
|
|
|
/** Web Mercator is undefined at the poles; the engines clamp here, so clamp before projecting. */
|
|
export const MERCATOR_MAX_LAT = 85.051129
|
|
|
|
const clampLat = (lat) => Math.max(-MERCATOR_MAX_LAT, Math.min(MERCATOR_MAX_LAT, lat))
|
|
|
|
/**
|
|
* Latitude bands of EQUAL RENDERED HEIGHT, north to south.
|
|
* @param {number} southLat
|
|
* @param {number} northLat
|
|
* @param {number} rows
|
|
* @returns {Array<[number, number]>} [southLat, northLat] per row, ordered north-first to match a
|
|
* top-to-bottom tile grid
|
|
*/
|
|
export function equalHeightLatBands(southLat, northLat, rows) {
|
|
const n = Math.max(1, Math.floor(rows) || 1)
|
|
const yN = mercatorY(clampLat(Math.max(southLat, northLat)))
|
|
const yS = mercatorY(clampLat(Math.min(southLat, northLat)))
|
|
const step = (yN - yS) / n
|
|
const out = []
|
|
for (let r = 0; r < n; r++) {
|
|
const top = yN - r * step
|
|
const bottom = yN - (r + 1) * step
|
|
out.push([latAtMercatorY(bottom), latAtMercatorY(top)])
|
|
}
|
|
return out
|
|
}
|