import { getOSMTileIndices } from "./tile-2d-traversal.js"; const TILE_SIZE = 512; const DEFAULT_EXTENT = [-Infinity, -Infinity, Infinity, Infinity]; export const urlType = { type: 'object', value: null, validate: (value, propType) => (propType.optional && value === null) || typeof value === 'string' || (Array.isArray(value) && value.every(url => typeof url === 'string')), equal: (value1, value2) => { if (value1 === value2) { return true; } if (!Array.isArray(value1) || !Array.isArray(value2)) { return false; } const len = value1.length; if (len !== value2.length) { return false; } for (let i = 0; i < len; i++) { if (value1[i] !== value2[i]) { return false; } } return true; } }; function transformBox(bbox, modelMatrix) { const transformedCoords = [ // top-left modelMatrix.transformAsPoint([bbox[0], bbox[1]]), // top-right modelMatrix.transformAsPoint([bbox[2], bbox[1]]), // bottom-left modelMatrix.transformAsPoint([bbox[0], bbox[3]]), // bottom-right modelMatrix.transformAsPoint([bbox[2], bbox[3]]) ]; const transformedBox = [ // Minimum x coord Math.min(...transformedCoords.map(i => i[0])), // Minimum y coord Math.min(...transformedCoords.map(i => i[1])), // Max x coord Math.max(...transformedCoords.map(i => i[0])), // Max y coord Math.max(...transformedCoords.map(i => i[1])) ]; return transformedBox; } function stringHash(s) { return Math.abs(s.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)); } export function getURLFromTemplate(template, tile) { if (!template || !template.length) { return null; } const { index, id } = tile; if (Array.isArray(template)) { const i = stringHash(id) % template.length; template = template[i]; } let url = template; for (const key of Object.keys(index)) { const regex = new RegExp(`{${key}}`, 'g'); url = url.replace(regex, String(index[key])); } // Back-compatible support for {-y} if (Number.isInteger(index.y) && Number.isInteger(index.z)) { url = url.replace(/\{-y\}/g, String(Math.pow(2, index.z) - index.y - 1)); } return url; } /** * gets the bounding box of a viewport */ function getBoundingBox(viewport, zRange, extent) { let bounds; if (zRange && zRange.length === 2) { const [minZ, maxZ] = zRange; const bounds0 = viewport.getBounds({ z: minZ }); const bounds1 = viewport.getBounds({ z: maxZ }); bounds = [ Math.min(bounds0[0], bounds1[0]), Math.min(bounds0[1], bounds1[1]), Math.max(bounds0[2], bounds1[2]), Math.max(bounds0[3], bounds1[3]) ]; } else { bounds = viewport.getBounds(); } if (!viewport.isGeospatial) { return [ // Top corner should not be more then bottom corner in either direction Math.max(Math.min(bounds[0], extent[2]), extent[0]), Math.max(Math.min(bounds[1], extent[3]), extent[1]), // Bottom corner should not be less then top corner in either direction Math.min(Math.max(bounds[2], extent[0]), extent[2]), Math.min(Math.max(bounds[3], extent[1]), extent[3]) ]; } return [ Math.max(bounds[0], extent[0]), Math.max(bounds[1], extent[1]), Math.min(bounds[2], extent[2]), Math.min(bounds[3], extent[3]) ]; } /** Get culling bounds in world space */ export function getCullBounds({ viewport, z, cullRect }) { const subViewports = viewport.subViewports || [viewport]; return subViewports.map(v => getCullBoundsInViewport(v, z || 0, cullRect)); } function getCullBoundsInViewport( /** Current viewport */ viewport, /** At altitude */ z, /** Culling rectangle in screen space */ cullRect) { if (!Array.isArray(z)) { const x = cullRect.x - viewport.x; const y = cullRect.y - viewport.y; const { width, height } = cullRect; const unprojectOption = { targetZ: z }; const topLeft = viewport.unproject([x, y], unprojectOption); const topRight = viewport.unproject([x + width, y], unprojectOption); const bottomLeft = viewport.unproject([x, y + height], unprojectOption); const bottomRight = viewport.unproject([x + width, y + height], unprojectOption); return [ Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]) ]; } const bounds0 = getCullBoundsInViewport(viewport, z[0], cullRect); const bounds1 = getCullBoundsInViewport(viewport, z[1], cullRect); return [ Math.min(bounds0[0], bounds1[0]), Math.min(bounds0[1], bounds1[1]), Math.max(bounds0[2], bounds1[2]), Math.max(bounds0[3], bounds1[3]) ]; } function getIndexingCoords(bbox, scale, modelMatrixInverse) { if (modelMatrixInverse) { const transformedTileIndex = transformBox(bbox, modelMatrixInverse).map(i => (i * scale) / TILE_SIZE); return transformedTileIndex; } return bbox.map(i => (i * scale) / TILE_SIZE); } function getScale(z, tileSize) { return (Math.pow(2, z) * TILE_SIZE) / tileSize; } // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_2 export function osmTile2lngLat(x, y, z) { const scale = getScale(z, TILE_SIZE); const lng = (x / scale) * 360 - 180; const n = Math.PI - (2 * Math.PI * y) / scale; const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); return [lng, lat]; } function tile2XY(x, y, z, tileSize) { const scale = getScale(z, tileSize); return [(x / scale) * TILE_SIZE, (y / scale) * TILE_SIZE]; } export function tileToBoundingBox(viewport, x, y, z, tileSize = TILE_SIZE) { if (viewport.isGeospatial) { const [west, north] = osmTile2lngLat(x, y, z); const [east, south] = osmTile2lngLat(x + 1, y + 1, z); return { west, north, east, south }; } const [left, top] = tile2XY(x, y, z, tileSize); const [right, bottom] = tile2XY(x + 1, y + 1, z, tileSize); return { left, top, right, bottom }; } function getIdentityTileIndices(viewport, z, tileSize, extent, modelMatrixInverse) { const bbox = getBoundingBox(viewport, null, extent); const scale = getScale(z, tileSize); const [minX, minY, maxX, maxY] = getIndexingCoords(bbox, scale, modelMatrixInverse); const indices = []; /* | TILE | TILE | TILE | |(minX) |(maxX) */ for (let x = Math.floor(minX); x < maxX; x++) { for (let y = Math.floor(minY); y < maxY; y++) { indices.push({ x, y, z }); } } return indices; } /** * Returns all tile indices in the current viewport. If the current zoom level is smaller * than minZoom, return an empty array. If the current zoom level is greater than maxZoom, * return tiles that are on maxZoom. */ // eslint-disable-next-line complexity export function getTileIndices({ viewport, maxZoom, minZoom, zRange, extent, tileSize = TILE_SIZE, modelMatrix, modelMatrixInverse, zoomOffset = 0 }) { let z = viewport.isGeospatial ? Math.round(viewport.zoom + Math.log2(TILE_SIZE / tileSize)) + zoomOffset : Math.ceil(viewport.zoom) + zoomOffset; if (typeof minZoom === 'number' && Number.isFinite(minZoom) && z < minZoom) { if (!extent) { return []; } z = minZoom; } if (typeof maxZoom === 'number' && Number.isFinite(maxZoom) && z > maxZoom) { z = maxZoom; } let transformedExtent = extent; if (modelMatrix && modelMatrixInverse && extent && !viewport.isGeospatial) { transformedExtent = transformBox(extent, modelMatrix); } return viewport.isGeospatial ? getOSMTileIndices(viewport, z, zRange, extent) : getIdentityTileIndices(viewport, z, tileSize, transformedExtent || DEFAULT_EXTENT, modelMatrixInverse); } /** * Returns true if s is a valid URL template */ export function isURLTemplate(s) { return /(?=.*{z})(?=.*{x})(?=.*({y}|{-y}))/.test(s); } export function isGeoBoundingBox(v) { return (Number.isFinite(v.west) && Number.isFinite(v.north) && Number.isFinite(v.east) && Number.isFinite(v.south)); }