From df67b6596629e6216e100fddb1e60573d73689ee Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Fri, 17 Jan 2020 23:58:07 -0800 Subject: [PATCH] fix tile indices generation in edge cases --- .../src/tile-layer/utils/viewport-util.js | 73 +++++++++---------- .../tile-layer/viewport-util.spec.js | 20 ++++- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/modules/geo-layers/src/tile-layer/utils/viewport-util.js b/modules/geo-layers/src/tile-layer/utils/viewport-util.js index 0c15e7eee41..c20d0641999 100644 --- a/modules/geo-layers/src/tile-layer/utils/viewport-util.js +++ b/modules/geo-layers/src/tile-layer/utils/viewport-util.js @@ -2,6 +2,9 @@ import {lngLatToWorld} from '@math.gl/web-mercator'; const TILE_SIZE = 512; +/** + * gets the bounding box of a viewport + */ function getBoundingBox(viewport) { const corners = [ viewport.unproject([0, 0]), @@ -10,13 +13,17 @@ function getBoundingBox(viewport) { viewport.unproject([viewport.width, viewport.height]) ]; return [ - corners.reduce((minLng, p) => (minLng < p[0] ? minLng : p[0]), 180), - corners.reduce((minLat, p) => (minLat < p[1] ? minLat : p[1]), 90), - corners.reduce((maxLng, p) => (maxLng > p[0] ? maxLng : p[0]), -180), - corners.reduce((maxLat, p) => (maxLat > p[1] ? maxLat : p[1]), -90) + Math.min(corners[0][0], corners[1][0], corners[2][0], corners[3][0]), + Math.min(corners[0][1], corners[1][1], corners[2][1], corners[3][1]), + Math.max(corners[0][0], corners[1][0], corners[2][0], corners[3][0]), + Math.max(corners[0][1], corners[1][1], corners[2][1], corners[3][1]) ]; } +/* + * get the OSM tile index at the given location + * https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames + */ function getTileIndex(lngLat, scale) { let [x, y] = lngLatToWorld(lngLat); x *= scale / TILE_SIZE; @@ -30,48 +37,40 @@ function getTileIndex(lngLat, scale) { * return tiles that are on maxZoom. */ export function getTileIndices(viewport, maxZoom, minZoom) { - const z = Math.ceil(viewport.zoom); - if (minZoom && z < minZoom) { + let z = Math.ceil(viewport.zoom); + if (Number.isFinite(minZoom) && z < minZoom) { return []; } + if (Number.isFinite(maxZoom) && z > maxZoom) { + z = maxZoom; + } const bbox = getBoundingBox(viewport); const scale = 2 ** z; - let [minX, minY] = getTileIndex([bbox[0], bbox[3]], scale); - let [maxX, maxY] = getTileIndex([bbox[2], bbox[1]], scale); /* - | TILE | TILE | TILE | - |(minPixel) |(maxPixel) - |(minIndex) |(maxIndex) + minX, maxX could be out of bounds if longitude is near the 180 meridian or multiple worlds + are shown: + | | + actual -2 -1 0 1 2 3 + expected 2 3 0 1 2 3 */ - minX = Math.max(0, Math.floor(minX)); - maxX = Math.min(scale, Math.ceil(maxX)); - minY = Math.max(0, Math.floor(minY)); - maxY = Math.min(scale, Math.ceil(maxY)); - - const indices = []; + const [minX, minY] = getTileIndex([bbox[0], bbox[3]], scale); + const [maxX, maxY] = getTileIndex([bbox[2], bbox[1]], scale); + const indices = {}; - for (let x = minX; x < maxX; x++) { - for (let y = minY; y < maxY; y++) { - if (maxZoom && z > maxZoom) { - indices.push(getAdjustedTileIndex({x, y, z}, maxZoom)); - } else { - indices.push({x, y, z}); - } + /* + | TILE | TILE | TILE | + |(minX) |(maxX) + */ + for (let x = Math.floor(minX); x < maxX; x++) { + for (let y = Math.floor(minY); y < maxY; y++) { + // Cast to valid x between [0, scale] + const normalizedX = x - Math.floor(x / scale) * scale; + // dedupe + const key = `${z}-${normalizedX}-${y}`; + indices[key] = indices[key] || {x: normalizedX, y, z}; } } - return indices; -} - -/** - * Calculates and returns a new tile index {x, y, z}, with z being the given adjustedZ. - */ -function getAdjustedTileIndex({x, y, z}, adjustedZ) { - const m = Math.pow(2, z - adjustedZ); - return { - x: Math.floor(x / m), - y: Math.floor(y / m), - z: adjustedZ - }; + return Object.values(indices); } diff --git a/test/modules/geo-layers/tile-layer/viewport-util.spec.js b/test/modules/geo-layers/tile-layer/viewport-util.spec.js index d74781c3bc4..37eaa7e1d8e 100644 --- a/test/modules/geo-layers/tile-layer/viewport-util.spec.js +++ b/test/modules/geo-layers/tile-layer/viewport-util.spec.js @@ -82,7 +82,7 @@ const TEST_CASES = [ title: 'z0 repeat', viewport: new WebMercatorViewport({ width: 800, - height: 400, + height: 200, longitude: -90, latitude: 0, zoom: 0 @@ -90,15 +90,27 @@ const TEST_CASES = [ minZoom: undefined, maxZoom: undefined, output: ['0,0,0'] + }, + { + title: 'near 180 meridian', + viewport: new WebMercatorViewport({ + width: 800, + height: 200, + longitude: -152, + latitude: 0, + zoom: 3 + }), + maxZoom: 2, + output: ['0,1,2', '0,2,2', '3,1,2', '3,2,2'] } ]; function getTileIds(tiles) { - const set = new Set(); + const ids = []; for (const tile of tiles) { - set.add(`${tile.x},${tile.y},${tile.z}`); + ids.push(`${tile.x},${tile.y},${tile.z}`); } - return Array.from(set).sort(); + return Array.from(ids).sort(); } test('getTileIndices', t => {