UNPKG

@deck.gl/carto

Version:

CARTO official integration with Deck.gl. Build geospatial applications using CARTO and Deck.gl.

135 lines 6.02 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { _Tileset2D as Tileset2D } from '@deck.gl/geo-layers'; import { polygonToCells, latLngToCell, getResolution, cellToBoundary, cellToParent, gridDisk, edgeLength, UNITS, originToDirectedEdges } from 'h3-js'; const MAX_LATITUDE = 85.051128; /** * `polygonToCells()` fills based on hexagon center, this function will * pad the bounds such that all cells that overlap the bounds will be included. * * @param bbox - The bounding box to pad. * @param resolution - The resolution of the hexagons. * @param scale - The scale of the buffer. 1 is to pad by the max edge length of the tile cell. * @returns The padded bounding box. */ function padBoundingBox({ west, north, east, south }, resolution, scale = 1.0) { const corners = [ [north, east], [south, east], [south, west], [north, west] ]; const cornerCells = corners.map(c => latLngToCell(c[0], c[1], resolution)); const cornerEdgeLengths = cornerCells.map(c => (Math.max(...originToDirectedEdges(c).map(e => edgeLength(e, UNITS.rads))) * 180) / Math.PI); const bufferLat = Math.max(...cornerEdgeLengths) * scale; const bufferLon = Math.min(180, bufferLat / Math.cos((((north + south) / 2) * Math.PI) / 180)); return { north: Math.min(north + bufferLat, MAX_LATITUDE), east: east + bufferLon, south: Math.max(south - bufferLat, -MAX_LATITUDE), west: west - bufferLon }; } function getHexagonsInBoundingBox({ west, north, east, south }, resolution) { const longitudeSpan = Math.abs(east - west); if (longitudeSpan > 180) { // This is a known issue in h3-js: polygonToCells does not work correctly // when longitude span is larger than 180 degrees. const nSegments = Math.ceil(longitudeSpan / 180); let h3Indices = []; for (let s = 0; s < nSegments; s++) { const segmentWest = west + s * 180; const segmentEast = Math.min(segmentWest + 179.9999999, east); h3Indices = h3Indices.concat(getHexagonsInBoundingBox({ west: segmentWest, north, east: segmentEast, south }, resolution)); } return [...new Set(h3Indices)]; } const polygon = [ [north, east], [south, east], [south, west], [north, west], [north, east] ]; return polygonToCells(polygon, resolution); } function tileToBoundingBox(index) { const coordinates = cellToBoundary(index); const latitudes = coordinates.map(c => c[0]); const longitudes = coordinates.map(c => c[1]); const west = Math.min(...longitudes); const south = Math.min(...latitudes); const east = Math.max(...longitudes); const north = Math.max(...latitudes); const bbox = { west, south, east, north }; // H3 child cells extend beyond their parent's boundary forming a "snowflake" // fractal pattern. The required buffer is approximately 10% of the // edge length of the tile cell, add a bit more to be safe. return padBoundingBox(bbox, getResolution(index), 0.12); } // Resolution conversion function. Takes a WebMercatorViewport and returns // a H3 resolution such that the screen space size of the hexagons is // similar // Relative scale factor (0 = no biasing, 2 = a few hexagons cover view) const BIAS = 2; export function getHexagonResolution(viewport, tileSize) { // Difference in given tile size compared to deck's internal 512px tile size, // expressed as an offset to the viewport zoom. const zoomOffset = Math.log2(tileSize / 512); const hexagonScaleFactor = (2 / 3) * (viewport.zoom - zoomOffset); const latitudeScaleFactor = Math.log(1 / Math.cos((Math.PI * viewport.latitude) / 180)); // Clip and bias return Math.max(0, Math.floor(hexagonScaleFactor + latitudeScaleFactor - BIAS)); } export default class H3Tileset2D extends Tileset2D { /** * 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. */ // @ts-expect-error Tileset2D should be generic over TileIndex getTileIndices({ viewport, minZoom, maxZoom }) { if (viewport.latitude === undefined) return []; const [west, south, east, north] = viewport.getBounds(); const { tileSize } = this.opts; let z = getHexagonResolution(viewport, tileSize); let indices; if (typeof minZoom === 'number' && Number.isFinite(minZoom) && z < minZoom) { // TODO support `extent` prop return []; } if (typeof maxZoom === 'number' && Number.isFinite(maxZoom) && z > maxZoom) { z = maxZoom; // Once we are at max zoom, getHexagonsInBoundingBox doesn't work, simply // get a ring centered on the hexagon in the viewport center const center = latLngToCell(viewport.latitude, viewport.longitude, maxZoom); indices = gridDisk(center, 1); } else { const paddedBounds = padBoundingBox({ west, north, east, south }, z); indices = getHexagonsInBoundingBox(paddedBounds, z); } return indices.map(i => ({ i })); } // @ts-expect-error Tileset2D should be generic over TileIndex getTileId({ i }) { return i; } // @ts-expect-error Tileset2D should be generic over TileIndex getTileMetadata({ i }) { return { bbox: tileToBoundingBox(i) }; } // @ts-expect-error Tileset2D should be generic over TileIndex getTileZoom({ i }) { return getResolution(i); } // @ts-expect-error Tileset2D should be generic over TileIndex getParentIndex(index) { const resolution = getResolution(index.i); const i = cellToParent(index.i, resolution - 1); return { i }; } } //# sourceMappingURL=h3-tileset-2d.js.map