mapbox-gl
Version:
A WebGL interactive maps library
174 lines (137 loc) • 5.9 kB
JavaScript
// @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};
}