diff --git a/src/geo/tileRanger.ts b/src/geo/tileRanger.ts index 60ac31c..8003d78 100644 --- a/src/geo/tileRanger.ts +++ b/src/geo/tileRanger.ts @@ -149,10 +149,11 @@ export class TileRanger { if (verbose) { console.log("find minimal zoom where the the area can be converted by area the size of single tile to skip levels that can't have full hashes"); } - const dx = boundingRange.maxX - boundingRange.minX; - const dy = boundingRange.maxY - boundingRange.minY; - const minXZoom = Math.max(Math.floor(Math.log2(1 << (zoom + 1)) / dx) - 1, 0); - const minYZoom = Math.max(Math.floor(Math.log2(1 << zoom) / dy), 0); + + const xTilesCount = boundingRange.maxX - boundingRange.minX + 1; + const yTilesCount = boundingRange.maxY - boundingRange.minY + 1; + const minXZoom = Math.max(Math.floor((zoom + 1) / xTilesCount) - 1, 0); + const minYZoom = Math.max(Math.floor(zoom / yTilesCount), 0); const minZoom = Math.min(minXZoom, minYZoom); if (verbose) { diff --git a/tests/unit/geo/tileRanger.spec.ts b/tests/unit/geo/tileRanger.spec.ts index 8aba55d..dffd340 100644 --- a/tests/unit/geo/tileRanger.spec.ts +++ b/tests/unit/geo/tileRanger.spec.ts @@ -141,6 +141,28 @@ describe('TileRanger', () => { ]; expect(tileRanges).toEqual(expectedRanges); }); + + it('encodes non-bbox polygon whose bounding range is a single tile', async () => { + // Triangle fully within tile {x: 0, y: 1, zoom: 1}, which covers lon [-180, -90] lat [0, 90]. + // This exercises the single-tile edge case where xTilesCount=1, yTilesCount=1. + const poly = polygon([ + [ + [-170, 10], + [-100, 10], + [-135, 80], + [-170, 10], + ], + ]); + + const tileRanges = []; + const gen = ranger.encodeFootprint(poly, 1); + for await (const range of gen) { + tileRanges.push(range); + } + + const expectedRanges = [{ minX: 0, maxX: 0, minY: 1, maxY: 1, zoom: 1 }]; + expect(tileRanges).toEqual(expectedRanges); + }); }); describe('generateTiles', () => {