s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
187 lines (186 loc) • 7.33 kB
JavaScript
import { project } from '../mat4.js';
import { boxIntersects, pointBoundaries } from './index.js';
import { idFromIJ, llToTile, llToTilePx } from 'gis-tools/index.js';
/**
* Given a zoom, lon, lat, and projector, get the tiles in view
* Due to the nature of the Web Mercator design,
* it's easiest to store an MVP matrix for each tile
*
* NOTE: Real World Tiles must be created/cached BEFORE creating out of bounds tiles
* as the out of bounds tiles will need to reference the real world tiles.
* So we sort all out of bounds tiles to the end of the list.
* @param zoom - the zoom leve
* @param lon - the longitude
* @param lat - the latitude
* @param projector - the projection object
* @returns a list of Tile IDs in view
*/
export function getTilesInViewWM(zoom, lon, lat, projector) {
const { tileSize, duplicateHorizontally } = projector;
if (zoom < 1)
zoom = 0;
const tiles = new Map();
const checkList = [];
const checkedTiles = new Set();
zoom = zoom << 0; // move to whole number
const size = 1 << zoom;
// let's find the current tile and store it
const { x, y } = llToTile({ x: lon, y: lat }, zoom, tileSize);
const id = idFromIJ(0, x, y, zoom);
tiles.set(id, { id, face: 0, zoom, x, y });
// add the first set of neighbors
addNeighbors(zoom, x, y, duplicateHorizontally, checkedTiles, checkList);
while (checkList.length > 0) {
// grab a tile to check and get its face and bounds
const check = checkList.pop();
if (check === undefined)
break;
const [, xCheck, yCheck] = check;
// get the tiles matrix
const matrix = tileMatrix(projector, zoom, xCheck, yCheck);
// project the four corners of the tile
const bl = project(matrix, { x: 0, y: 0, z: 0 });
const br = project(matrix, { x: 1, y: 0, z: 0 });
const tl = project(matrix, { x: 0, y: 1, z: 0 });
const tr = project(matrix, { x: 1, y: 1, z: 0 });
// check if the tile is in view
if (pointBoundaries(bl, br, tl, tr) || boxIntersects(bl, br, tl, tr)) {
// ensure S2CellId uses the wrapped x-y values.
const s2ID = idFromIJ(0, mod(xCheck, size), mod(yCheck, size), zoom);
// if the tile is in view, add it to the list
const wmID = toWMID(zoom, xCheck, yCheck, duplicateHorizontally);
const id = wmID ?? s2ID;
// IF the wmID exists, it means it's an out of bounds ID, we we need to store the "wrapped" id which is the s2CellId
const wrappedID = wmID !== undefined ? s2ID : undefined;
tiles.set(id, { id, face: 0, zoom: zoom, x: xCheck, y: yCheck, wrappedID });
// add the surounding tiles we have not checked
addNeighbors(zoom, xCheck, yCheck, duplicateHorizontally, checkedTiles, checkList);
}
}
return ([...tiles.values()]
// first we sort by id to avoid text filtering to awkwardly swap back and forth
.sort((a, b) => {
// First sort by x
if (a.x < b.x)
return -1;
else if (a.x > b.x)
return 1;
// Then sort by y
else if (a.y < b.y)
return -1;
else if (a.y > b.y)
return 1;
// assume equal enough
else
return 0;
})
// then we sort by real world tiles first and out of bounds tiles last
// NOTE: I am commenting this out for now. I am not convinced it is necessary
// .sort((a, b) => {
// if (a.wrappedID !== undefined) return 1;
// else if (b.wrappedID !== undefined) return -1;
// else return 0;
// })
);
}
/**
* Add neighbors to the checkList
* @param zoom - the zoom level
* @param x - the x tile-coordinate
* @param y - the y tile-coordinate
* @param duplicateHorizontally - whether to duplicate horizontally
* @param checkedTiles - the set of tiles we have already checked
* @param checkList - the list of tiles to check
*/
function addNeighbors(zoom, x, y, duplicateHorizontally, checkedTiles, checkList) {
// add the surounding tiles we have not checked
for (const [nZoom, nX, nY] of neighborsXY(zoom, x, y, duplicateHorizontally)) {
const zxy = `${String(nZoom)}-${String(nX)}-${String(nY)}`;
if (!checkedTiles.has(zxy)) {
checkedTiles.add(zxy);
checkList.push([nZoom, nX, nY]);
}
}
}
/**
* Get the matrix for a specific tile
* @param projector - the projection object's current state
* @param tileZoom - the zoom level
* @param tileX - the x tile-coordinate
* @param tileY - the y tile-coordinate
* @returns the matrix for the tile
*/
function tileMatrix(projector, tileZoom, tileX, tileY) {
const { zoom, lon, lat } = projector;
const scale = Math.pow(2, zoom - tileZoom);
const offset = llToTilePx({ x: lon, y: lat }, [tileZoom, tileX, tileY], 1);
return projector.getMatrix(scale, offset);
}
/**
* grab the tiles next to the current tiles zoom-x-y
* only include adjacent tiles, not diagonal.
* If includeOutOfBounds set to true, it will include out of bounds tiles
* on the x-axis
* @param zoom - tile's zoom
* @param x - tile's x-coordinate
* @param y - tile's y-coordinate
* @param includeOutOfBounds - flag to keep out of bounds tiles if true
* @returns neighboring tiles, including out of bounds if flag set
*/
export function neighborsXY(zoom, x, y, includeOutOfBounds = false) {
const size = 1 << zoom;
const neighbors = [];
const xOutOfBounds = x < 0 || x >= size;
if (x - 1 >= 0 || includeOutOfBounds)
neighbors.push([zoom, x - 1, y]);
if (x + 1 < size || includeOutOfBounds)
neighbors.push([zoom, x + 1, y]);
if (!xOutOfBounds && y - 1 >= 0)
neighbors.push([zoom, x, y - 1]);
if (!xOutOfBounds && y + 1 < size)
neighbors.push([zoom, x, y + 1]);
return neighbors;
}
/**
* Convert zoom-x-y to a singular number
* It may resolve to itself. This is useful for maps that have
* `duplicateHorizontally` set to true. It forces the tile to be
* within the bounds of the quad tree.
* @param zoom - the zoom level
* @param x - the x tile-coordinate
* @param y - the y tile-coordinate
* @param duplicateHorizontally - whether to duplicate horizontally
* @returns the singular number
*/
export function toWMID(zoom, x, y, duplicateHorizontally) {
if (!duplicateHorizontally)
return undefined;
const size = 1 << zoom;
// skip tiles that are NOT out of bounds
if (x >= 0 && x < size && y >= 0 && y < size)
return;
// otherwise store the out of boudns tile
const maxX = 1 << zoom;
const adjustedX = mod(x, maxX); // Adjust x to be within [0, maxX)
const remainder = zigzag(x - adjustedX);
return ((BigInt(1 << zoom) * BigInt(y) + BigInt(adjustedX)) * 32n +
BigInt(zoom) +
(BigInt(remainder) << 65n));
}
/**
* encode a number as always positive interweaving negative and postive values
* @param n - the number
* @returns the encoded number
*/
export function zigzag(n) {
return (n >> 31) ^ (n << 1);
}
/**
* a modulo function that works with negative numbers
* @param x - the number
* @param n - the modulus
* @returns the result
*/
export function mod(x, n) {
return ((x % n) + n) % n;
}