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))
}