UNPKG

mapbox-gl

Version:
174 lines (137 loc) 5.9 kB
// @flow // logic for generating non-Mercator adaptive raster tile reprojection meshes with MARTINI import tileTransform from '../geo/projection/tile_transform.js'; import EXTENT from '../data/extent.js'; import {lngFromMercatorX, latFromMercatorY} from '../geo/mercator_coordinate.js'; import {TileBoundsArray, TriangleIndexArray} from '../data/array_types.js'; import type {CanonicalTileID} from './tile_id.js'; import type Projection from '../geo/projection/projection.js'; const meshSize = 32; const gridSize = meshSize + 1; const numTriangles = meshSize * meshSize * 2 - 2; const numParentTriangles = numTriangles - meshSize * meshSize; const coords = new Uint16Array(numTriangles * 4); // precalculate RTIN triangle coordinates for (let i = 0; i < numTriangles; i++) { let id = i + 2; let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0; if (id & 1) { bx = by = cx = meshSize; // bottom-left triangle } else { ax = ay = cy = meshSize; // top-right triangle } while ((id >>= 1) > 1) { const mx = (ax + bx) >> 1; const my = (ay + by) >> 1; if (id & 1) { // left half bx = ax; by = ay; ax = cx; ay = cy; } else { // right half ax = bx; ay = by; bx = cx; by = cy; } cx = mx; cy = my; } const k = i * 4; coords[k + 0] = ax; coords[k + 1] = ay; coords[k + 2] = bx; coords[k + 3] = by; } // temporary arrays we'll reuse for MARTINI mesh code const reprojectedCoords = new Uint16Array(gridSize * gridSize * 2); const used = new Uint8Array(gridSize * gridSize); const indexMap = new Uint16Array(gridSize * gridSize); type TileMesh = { vertices: TileBoundsArray, indices: TriangleIndexArray }; // There can be visible seams between neighbouring tiles because of precision issues // and resampling differences. Adding a bit of padding around the edges of tiles hides // most of these issues. const commonRasterTileSize = 256; const paddingSize = meshSize / commonRasterTileSize / 4; function seamPadding(n) { if (n === 0) return -paddingSize; else if (n === gridSize - 1) return paddingSize; else return 0; } export default function getTileMesh(canonical: CanonicalTileID, projection: Projection): TileMesh { const cs = tileTransform(canonical, projection); const z2 = Math.pow(2, canonical.z); for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { const lng = lngFromMercatorX((canonical.x + (x + seamPadding(x)) / meshSize) / z2); const lat = latFromMercatorY((canonical.y + (y + seamPadding(y)) / meshSize) / z2); const p = projection.project(lng, lat); const k = y * gridSize + x; reprojectedCoords[2 * k + 0] = Math.round((p.x * cs.scale - cs.x) * EXTENT); reprojectedCoords[2 * k + 1] = Math.round((p.y * cs.scale - cs.y) * EXTENT); } } used.fill(0); indexMap.fill(0); // iterate over all possible triangles, starting from the smallest level for (let i = numTriangles - 1; i >= 0; i--) { const k = i * 4; const ax = coords[k + 0]; const ay = coords[k + 1]; const bx = coords[k + 2]; const by = coords[k + 3]; const mx = (ax + bx) >> 1; const my = (ay + by) >> 1; const cx = mx + my - ay; const cy = my + ax - mx; const aIndex = ay * gridSize + ax; const bIndex = by * gridSize + bx; const mIndex = my * gridSize + mx; // calculate error in the middle of the long edge of the triangle const rax = reprojectedCoords[2 * aIndex + 0]; const ray = reprojectedCoords[2 * aIndex + 1]; const rbx = reprojectedCoords[2 * bIndex + 0]; const rby = reprojectedCoords[2 * bIndex + 1]; const rmx = reprojectedCoords[2 * mIndex + 0]; const rmy = reprojectedCoords[2 * mIndex + 1]; // raster tiles are typically 512px, and we use 1px as an error threshold; 8192 / 512 = 16 const isUsed = Math.hypot((rax + rbx) / 2 - rmx, (ray + rby) / 2 - rmy) >= 16; used[mIndex] = used[mIndex] || (isUsed ? 1 : 0); if (i < numParentTriangles) { // bigger triangles; accumulate error with children const leftChildIndex = ((ay + cy) >> 1) * gridSize + ((ax + cx) >> 1); const rightChildIndex = ((by + cy) >> 1) * gridSize + ((bx + cx) >> 1); used[mIndex] = used[mIndex] || used[leftChildIndex] || used[rightChildIndex]; } } const vertices = new TileBoundsArray(); const indices = new TriangleIndexArray(); let numVertices = 0; function addVertex(x, y) { const k = y * gridSize + x; if (indexMap[k] === 0) { vertices.emplaceBack( reprojectedCoords[2 * k + 0], reprojectedCoords[2 * k + 1], x * EXTENT / meshSize, y * EXTENT / meshSize); // save new vertex index so that we can reuse it indexMap[k] = ++numVertices; } return indexMap[k] - 1; } function addTriangles(ax, ay, bx, by, cx, cy) { const mx = (ax + bx) >> 1; const my = (ay + by) >> 1; if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && used[my * gridSize + mx]) { // triangle doesn't approximate the surface well enough; drill down further addTriangles(cx, cy, ax, ay, mx, my); addTriangles(bx, by, cx, cy, mx, my); } else { const ai = addVertex(ax, ay); const bi = addVertex(bx, by); const ci = addVertex(cx, cy); indices.emplaceBack(ai, bi, ci); } } addTriangles(0, 0, meshSize, meshSize, meshSize, 0); addTriangles(meshSize, meshSize, 0, 0, 0, meshSize); return {vertices, indices}; }