File size: 4,244 Bytes
695f088 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | // What the globe draws: map tiles made in the browser from the current field, and the Major TOM cell under a point.
//
// MapLibre asks for tile z/x/y (Web Mercator, 256 px)
// -> each pixel's longitude and latitude
// -> the Major TOM cell it falls in (row from latitude, then column within that row's own width)
// -> grid field: the cell's value -> colour scale; image field: the preview pixel at that lon/lat
// -> ImageBitmap handed back to MapLibre (no encoding: MapLibre uses decoded pixels as they are)
export const ROWS = 2004, COLS = 4008 // the (row, column) array: rows -1002..1001, columns -2004..2003
const R_KM = 6378.137, D_KM = 10, DLAT = 180 / ROWS
const TILE = 256
/** Columns in grid row r (as majortom.cells: from the latitude of the row's lower edge). */
const nCols = r => Math.max(Math.ceil((2 * Math.PI * R_KM * Math.cos((r * DLAT * Math.PI) / 180)) / D_KM), 1)
/** Row and column of the cell under (lon, lat), lon in -180..180. */
function rowCol(lon, lat) {
const row = Math.min(Math.max(Math.floor(lat / DLAT), -ROWS / 2), ROWS / 2 - 1)
const n = nCols(row)
const col = Math.min(Math.max(Math.floor(lon / (360 / n)), -Math.floor(n / 2)), n - 1 - Math.floor(n / 2))
return [row, col, n]
}
/** Index of the cell under (lon, lat) in the (row, column) array, or at `level` in its 2^level-coarser block means
* (a dataset's overview); used for every pixel, so no objects. */
export function cellIndex(lon, lat, level = 0) {
const row = Math.min(Math.max(Math.floor(lat / DLAT), -ROWS / 2), ROWS / 2 - 1)
const n = nCols(row)
const col = Math.min(Math.max(Math.floor(lon / (360 / n)), -Math.floor(n / 2)), n - 1 - Math.floor(n / 2))
if (!level) return (row + ROWS / 2) * COLS + col + COLS / 2
return ((row + ROWS / 2) >> level) * Math.ceil(COLS / 2 ** level) + ((col + COLS / 2) >> level)
}
/** The cell under (lon, lat), for the cursor: row, column, name (as majortom.cells), index, bounds in degrees. */
export function cellAt(lon, lat) {
lon = ((lon + 180) % 360 + 360) % 360 - 180
const [row, col, n] = rowCol(lon, lat), dlon = 360 / n
return {
row, col, index: (row + ROWS / 2) * COLS + col + COLS / 2,
name: `${Math.abs(row)}${row >= 0 ? 'U' : 'D'}_${Math.abs(col)}${col >= 0 ? 'R' : 'L'}`,
bounds: [col * dlon, row * DLAT, (col + 1) * dlon, (row + 1) * DLAT],
}
}
/** A field of values, one per cell, on the (row, column) array (or its block means at `level`); NaN where there
* is no cell or no value. */
export function gridField(values, scale, level = 0) {
return {
values, scale, level,
valueAt: (lon, lat) => values[cellIndex(((lon + 180) % 360 + 360) % 360 - 180, lat, level)],
pixel(lon, lat, out, o) {
const v = values[cellIndex(lon, lat, level)]
if (Number.isNaN(v)) { out[o + 3] = 0; return }
const i = Math.round(scale.position(v) * 255) * 3
out[o] = scale.lut[i]; out[o + 1] = scale.lut[i + 1]; out[o + 2] = scale.lut[i + 2]; out[o + 3] = 255
},
}
}
/** A field read from a preview image of the (row, column) array, north up (build_images.py): pixel (x, y) is the
* cell grid_col_r = x - 2004, grid_row_u = 1001 - y, so a point finds its pixel exactly as live data finds its value. */
export function imageField(rgba, scale) {
const colour = (lon, lat, out, o) => {
const k = cellIndex(lon, lat), i = ((ROWS - 1 - Math.floor(k / COLS)) * COLS + (k % COLS)) * 4
out[o] = rgba[i]; out[o + 1] = rgba[i + 1]; out[o + 2] = rgba[i + 2]; out[o + 3] = rgba[i + 3]
}
return { scale, valueAt: () => undefined, pixel: colour } // colours only: exact values come from the Parquet files
}
/** One 256 px tile of `field` at z/x/y. */
export async function renderTile(field, z, x, y) {
const n = 2 ** z, out = new Uint8ClampedArray(TILE * TILE * 4)
const lons = new Float64Array(TILE)
for (let i = 0; i < TILE; i++) lons[i] = ((x + (i + 0.5) / TILE) / n) * 360 - 180
for (let j = 0; j < TILE; j++) {
const lat = (Math.atan(Math.sinh(Math.PI - (2 * Math.PI * (y + (j + 0.5) / TILE)) / n)) * 180) / Math.PI
for (let i = 0; i < TILE; i++) field.pixel(lons[i], lat, out, (j * TILE + i) * 4)
}
return createImageBitmap(new ImageData(out, TILE, TILE))
}
|