worldmap-viewer

git clone https://git.godosa.eu/worldmap-viewer

master

raw ยท 7575 bytes

// tiles.js โ€” deep-zoom tile maths (pure): the scheme shared with map/tiles.py, target zoom, visible tiles, patch meshes
import { R_KM, toVec, project } from './geo.js';

export const TILE = 256;
export const MIN_TILE_Z = 5;   // below this the base texture is as sharp as the tiles
export const MAX_TILE_Z = 16;
export const MESH_SEGS = 128;   // 3D patches: one vertex per server height (map/tiles.py MESH_N = 129)
export const tileSpanDeg = z => 180 / 2 ** z;
export const pxKm = z => (2 * Math.PI * R_KM) / (2 ** (z + 1) * TILE);
export const tileKey = t => `${t.z}/${t.x}/${t.y}`;
export const DETAIL_TILES = 16;   // the 3D fine grain repeats every 16 tiles (columns per zoom are a multiple of 16)
export const detailOrigin = t => [(t.x % DETAIL_TILES) * TILE, (t.y % DETAIL_TILES) * TILE];   // texels, small: float32-exact
export const parentTile = t => ({ z: t.z - 1, x: t.x >> 1, y: t.y >> 1 });

export function tileOf(lat, lon, z) {
  const s = tileSpanDeg(z), nx = 2 ** (z + 1), ny = 2 ** z;
  const x = ((Math.floor((lon + 180) / s) % nx) + nx) % nx;
  const y = Math.min(ny - 1, Math.max(0, Math.floor((90 - lat) / s)));
  return { z, x, y };
}

export function tileBounds({ z, x, y }) {
  const s = tileSpanDeg(z);
  return { west: -180 + x * s, east: -180 + (x + 1) * s, north: 90 - y * s, south: 90 - (y + 1) * s };
}

export function targetZoom(kmPerPx) {
  if (!(kmPerPx > 0)) return 0;
  const z = Math.ceil(Math.log2((2 * Math.PI * R_KM) / (kmPerPx * 2 * TILE)) - 1e-12);
  return Math.max(0, Math.min(MAX_TILE_Z, z));
}

function ringTiles(t, ring) {   // the tile first, then its neighbours (rows clipped at the poles, columns wrapped)
  const nx = 2 ** (t.z + 1), ny = 2 ** t.z, out = [t];
  for (let dy = -ring; dy <= ring; dy++) {
    const y = t.y + dy;
    if (y < 0 || y >= ny) continue;
    for (let dx = -ring; dx <= ring; dx++) if (dx || dy) out.push({ z: t.z, x: (((t.x + dx) % nx) + nx) % nx, y });
  }
  return out;
}

export function tilesForPoints(points, z, ring = 1) {
  const out = new Map();
  for (const p of points) for (const u of ringTiles(tileOf(p.lat, p.lon, z), ring)) if (!out.has(tileKey(u))) out.set(tileKey(u), u);
  return [...out.values()];
}

const children = t => [0, 1, 2, 3].map(i => ({ z: t.z + 1, x: 2 * t.x + (i & 1), y: 2 * t.y + (i >> 1) }));

// A quadtree cut for per-point zooms: every point wants its tile (and ring) at its own zoom. Any tile holding a
// wanted finer tile is split into all four children, so nothing overlaps; a split tile that was itself wanted
// (on screen) has all its children drawn, so nothing it covered goes missing. Order: first wanted first.
export function lodTiles(points, zs, ring = 1) {
  const want = new Map(), split = new Set();
  points.forEach((p, i) => {
    if (!(zs[i] >= MIN_TILE_Z)) return;
    for (const u of ringTiles(tileOf(p.lat, p.lon, zs[i]), ring)) {
      const k = tileKey(u);
      if (!want.has(k)) want.set(k, { t: u, n: want.size });
      for (let a = parentTile(u); a.z >= MIN_TILE_Z; a = parentTile(a)) split.add(tileKey(a));
    }
  });
  const out = [], roots = new Map();
  const visit = (t, cov, n) => {
    const k = tileKey(t), w = want.get(k), c = cov || !!w, m = w ? w.n : n;
    if (split.has(k)) for (const ch of children(t)) visit(ch, c, m);
    else if (c) out.push({ t, n: w ? w.n : n + 0.5 });
  };
  for (const { t } of want.values()) {
    let r = t;
    while (r.z > MIN_TILE_Z) r = parentTile(r);
    roots.set(tileKey(r), r);
  }
  for (const r of roots.values()) visit(r, false, Infinity);
  return out.sort((a, b) => a.n - b.n).map(o => o.t);
}

function grid(t, segs) {   // rows north โ†’ south, columns west โ†’ east (same winding as geo.sphereMesh)
  const b = tileBounds(t), n = segs + 1;
  const lat = new Float64Array(n * n), lon = new Float64Array(n * n), uvs = new Float32Array(n * n * 2);
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      const k = i * n + j;
      lat[k] = b.north - ((b.north - b.south) * i) / segs;
      lon[k] = b.west + ((b.east - b.west) * j) / segs;
      uvs[2 * k] = j / segs;
      uvs[2 * k + 1] = 1 - i / segs;
    }
  }
  const indices = new Uint32Array(segs * segs * 6);
  let q = 0;
  for (let i = 0; i < segs; i++) {
    for (let j = 0; j < segs; j++) {
      const a = i * n + j, c = a + n;
      indices.set([a, c, c + 1, a, c + 1, a + 1], q);
      q += 6;
    }
  }
  return { b, lat, lon, uvs, indices };
}

const R_M = R_KM * 1000;

function edgeRing(segs) {   // grid indices around the border, each once, in order (N, E, S, W)
  const n = segs + 1, out = [];
  for (let j = 0; j < segs; j++) out.push(j);
  for (let i = 0; i < segs; i++) out.push(i * n + segs);
  for (let j = segs; j > 0; j--) out.push(segs * n + j);
  for (let i = segs; i > 0; i--) out.push(i * n);
  return out;
}

export function globePatch(t, segs = 16, heights = null, exag = 1) {   // positions relative to the patch centre
  const g = grid(t, segs), N = g.lat.length;
  const center = toVec((g.b.north + g.b.south) / 2, (g.b.west + g.b.east) / 2);
  const edge = heights ? edgeRing(segs) : [], M = N + edge.length;
  const spacingM = (((g.b.north - g.b.south) * Math.PI) / 180 / segs) * R_M;
  const skirt = heights ? 2 * spacingM * Math.max(1, exag) : 0;   // m: hides cracks against neighbours up to 2 zooms coarser
  const positions = new Float32Array(M * 3), uvs = new Float32Array(M * 2);
  const skirtFlags = heights ? new Float32Array(M).fill(1, N) : undefined;   // 1: skirt (the cliff shading skips it)
  const put = (k, lat, lon, m) => {
    const v = toVec(lat, lon), r = 1 + m / R_M;
    for (let i = 0; i < 3; i++) positions[3 * k + i] = v[i] * r - center[i];
  };
  for (let k = 0; k < N; k++) put(k, g.lat[k], g.lon[k], heights ? heights[k] * exag : 0);
  uvs.set(g.uvs);
  edge.forEach((k, i) => {
    put(N + i, g.lat[k], g.lon[k], heights[k] * exag - skirt);
    uvs[2 * (N + i)] = g.uvs[2 * k];
    uvs[2 * (N + i) + 1] = g.uvs[2 * k + 1];
  });
  const indices = new Uint32Array(g.indices.length + edge.length * 6);
  indices.set(g.indices);
  for (let i = 0, q = g.indices.length; i < edge.length; i++, q += 6) {
    const a = edge[i], b = edge[(i + 1) % edge.length], a2 = N + i, b2 = N + ((i + 1) % edge.length);
    indices.set([a, a2, b2, a, b2, b], q);
  }
  return { center, positions, uvs, indices, skirt: skirtFlags };
}

export function gridHeight(t, heights, lat, lon, segs = MESH_SEGS) {   // bilinear height inside a tile's mesh
  const b = tileBounds(t), n = segs + 1, s = b.east - b.west;
  const fx = ((((lon - b.west) % 360) + 360) % 360 / s) * segs, fy = ((b.north - lat) / (b.north - b.south)) * segs;
  const j = Math.min(segs - 1, Math.max(0, Math.floor(fx))), i = Math.min(segs - 1, Math.max(0, Math.floor(fy)));
  const u = Math.min(1, Math.max(0, fx - j)), v = Math.min(1, Math.max(0, fy - i)), h = (a, c) => heights[a * n + c];
  return (1 - v) * ((1 - u) * h(i, j) + u * h(i, j + 1)) + v * ((1 - u) * h(i + 1, j) + u * h(i + 1, j + 1));
}

export function flatPatch(t, kind, segs = 16, zOff = 0) {
  const g = grid(t, segs), N = g.lat.length, D = Math.PI / 180;
  const c = project(kind, (g.b.north + g.b.south) / 2, (g.b.west + g.b.east) / 2);
  const positions = new Float32Array(N * 3), ll = new Float32Array(N * 2);
  for (let k = 0; k < N; k++) {
    const p = project(kind, g.lat[k], g.lon[k]);
    positions[3 * k] = p.x - c.x;
    positions[3 * k + 1] = p.y - c.y;
    ll[2 * k] = g.lat[k] * D;
    ll[2 * k + 1] = g.lon[k] * D;
  }
  return { center: [c.x, c.y, zOff], positions, uvs: g.uvs, indices: g.indices, ll };
}