import { WebMercatorViewport, OrthographicViewport } from '@deck.gl/core'; /* * Compute the union of bounds from multiple layers * Returns bounds in CARTESIAN coordinates */ export function joinLayerBounds( /** The layers to combine */ layers, /** A Viewport instance that is used to determine the type of the view */ viewport) { // Join the bounds of layer data const bounds = [Infinity, Infinity, -Infinity, -Infinity]; for (const layer of layers) { const layerBounds = layer.getBounds(); if (layerBounds) { const bottomLeftCommon = layer.projectPosition(layerBounds[0], { viewport, autoOffset: false }); const topRightCommon = layer.projectPosition(layerBounds[1], { viewport, autoOffset: false }); bounds[0] = Math.min(bounds[0], bottomLeftCommon[0]); bounds[1] = Math.min(bounds[1], bottomLeftCommon[1]); bounds[2] = Math.max(bounds[2], topRightCommon[0]); bounds[3] = Math.max(bounds[3], topRightCommon[1]); } } if (Number.isFinite(bounds[0])) { return bounds; } return null; } const MAX_VIEWPORT_SIZE = 2048; /** Construct a viewport that just covers the target bounds. Used for rendering to common space indexed texture. */ export function makeViewport(opts) { const { bounds, viewport, border = 0 } = opts; const { isGeospatial } = viewport; if (bounds[2] <= bounds[0] || bounds[3] <= bounds[1]) { return null; } const centerWorld = viewport.unprojectPosition([ (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, 0 ]); let { width, height, zoom } = opts; if (zoom === undefined) { // Use width and height to determine zoom width = width - border * 2; height = height - border * 2; const scale = Math.min(width / (bounds[2] - bounds[0]), height / (bounds[3] - bounds[1])); zoom = Math.min(Math.log2(scale), 20); } else if (!width || !height) { // Use zoom to determine width and height const scale = 2 ** zoom; width = Math.round(Math.abs(bounds[2] - bounds[0]) * scale); height = Math.round(Math.abs(bounds[3] - bounds[1]) * scale); const maxSize = MAX_VIEWPORT_SIZE - border * 2; if (width > maxSize || height > maxSize) { const r = maxSize / Math.max(width, height); width = Math.round(width * r); height = Math.round(height * r); zoom += Math.log2(r); } } // TODO - find a more generic way to construct this viewport // Geospatial viewports may not be web-mercator return isGeospatial ? new WebMercatorViewport({ id: viewport.id, x: border, y: border, width, height, longitude: centerWorld[0], latitude: centerWorld[1], zoom, orthographic: true }) : new OrthographicViewport({ id: viewport.id, x: border, y: border, width, height, target: centerWorld, zoom, flipY: false }); } /** Returns viewport bounds in CARTESIAN coordinates */ export function getViewportBounds(viewport, zRange) { // Viewport bounds in world coordinates let viewportBoundsWorld; if (zRange && zRange.length === 2) { const [minZ, maxZ] = zRange; const bounds0 = viewport.getBounds({ z: minZ }); const bounds1 = viewport.getBounds({ z: maxZ }); viewportBoundsWorld = [ 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 { viewportBoundsWorld = viewport.getBounds(); } // Viewport bounds in cartesian coordinates const viewportBottomLeftCommon = viewport.projectPosition(viewportBoundsWorld.slice(0, 2)); const viewportTopRightCommon = viewport.projectPosition(viewportBoundsWorld.slice(2, 4)); return [ viewportBottomLeftCommon[0], viewportBottomLeftCommon[1], viewportTopRightCommon[0], viewportTopRightCommon[1] ]; } /* * Determine the common space bounds that best cover the given data for the given viewport * Returns bounds in CARTESIAN coordinates */ export function getRenderBounds(layerBounds, viewport, zRange) { if (!layerBounds) { return [0, 0, 1, 1]; } const viewportBounds = getViewportBounds(viewport, zRange); // Expand viewport bounds by 2X. Heurestically chosen to avoid masking // errors when mask is partially out of view const paddedBounds = doubleBounds(viewportBounds); // When bounds of the layers are smaller than the viewport bounds simply use // mask bounds, so as to maximize resolution & avoid rerenders if (layerBounds[2] - layerBounds[0] <= paddedBounds[2] - paddedBounds[0] && layerBounds[3] - layerBounds[1] <= paddedBounds[3] - paddedBounds[1]) { return layerBounds; } // As viewport shrinks, to avoid pixelation along mask edges // we need to reduce the bounds and only render the visible portion // of the mask. // We pad the viewport bounds to capture the section // of the mask just outside the viewport to correctly maskByInstance. // Intersect mask & padded viewport bounds return [ Math.max(layerBounds[0], paddedBounds[0]), Math.max(layerBounds[1], paddedBounds[1]), Math.min(layerBounds[2], paddedBounds[2]), Math.min(layerBounds[3], paddedBounds[3]) ]; } function doubleBounds(bounds) { const dx = bounds[2] - bounds[0]; const dy = bounds[3] - bounds[1]; const centerX = (bounds[0] + bounds[2]) / 2; const centerY = (bounds[1] + bounds[3]) / 2; return [centerX - dx, centerY - dy, centerX + dx, centerY + dy]; }