Skip to content

Commit

Permalink
feat: Add createViewportSpatialFilter()
Browse files Browse the repository at this point in the history
  • Loading branch information
donmccurdy committed Sep 24, 2024
1 parent 1e5f07b commit ddfb4e7
Show file tree
Hide file tree
Showing 5 changed files with 2,156 additions and 129 deletions.
19 changes: 6 additions & 13 deletions examples/components/widgets/base-widget.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {css, CSSResultGroup, LitElement} from 'lit';
import {SpatialFilter, WidgetSource} from '@carto/api-client';
import {
createViewportSpatialFilter,
SpatialFilter,
WidgetSource,
} from '@carto/api-client';
import {MapViewState, WebMercatorViewport} from '@deck.gl/core';

export abstract class BaseWidget extends LitElement {
Expand Down Expand Up @@ -71,18 +75,7 @@ export abstract class BaseWidget extends LitElement {

if (this.viewState) {
const viewport = new WebMercatorViewport(this.viewState);
return {
type: 'Polygon',
coordinates: [
[
viewport.unproject([0, 0]),
viewport.unproject([viewport.width, 0]),
viewport.unproject([viewport.width, viewport.height]),
viewport.unproject([0, viewport.height]),
viewport.unproject([0, 0]),
],
],
};
return createViewportSpatialFilter(viewport.getBounds());
}

return undefined;
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
"@luma.gl/core": "^9.0.12",
"@luma.gl/engine": "^9.0.12",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@turf/turf": "^7.1.0",
"@types/geojson": "^7946.0.14",
"@types/json-schema": "^7.0.15",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
161 changes: 161 additions & 0 deletions src/geo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import {
bboxClip,
bboxPolygon,
getType,
polygon,
multiPolygon,
union,
featureCollection,
feature,
} from '@turf/turf';
import type {BBox, MultiPolygon, Polygon, Position} from 'geojson';
import {SpatialFilter} from './types';

/**
* TODO: Documentation.
*/
export function createViewportSpatialFilter(
// Use explicit [number, ...], not BBox. The 'geojson' package is not a
// production dependency, and cannot be used in publicly exported APIs.
viewport: [number, number, number, number]
): SpatialFilter | undefined {
if (_isGlobalViewport(viewport)) {
return;
}

const spatialFilter = _normalizeGeometry(bboxPolygon(viewport).geometry);
if (spatialFilter) {
return spatialFilter as SpatialFilter;
}

return undefined;
}

/**
* Check if a viewport is large enough to represent a global coverage.
* In this case the spatial filter parameter for widget calculation is removed.
*
* @internalRemarks Source: @carto/react-core
*/
function _isGlobalViewport(viewport: BBox) {
const [minx, miny, maxx, maxy] = viewport;
return maxx - minx > 179.5 * 2 && maxy - miny > 85.05 * 2;
}

/**
* Normalized a geometry, coming from a mask or a viewport. The parts
* spanning outside longitude range [-180, +180] are clipped and "folded"
* back to the valid range and unioned to the polygons inide that range.
*
* It results in a Polygon or MultiPolygon strictly inside the validity range.
*
* @internalRemarks Source: @carto/react-core
*/
function _normalizeGeometry(
geometry: Polygon | MultiPolygon
): Polygon | MultiPolygon | null {
const WORLD = [-180, -90, +180, +90] as BBox;
const worldClip = _clean(
bboxClip(geometry, WORLD).geometry as Polygon | MultiPolygon
);

const geometryTxWest = _tx(geometry, 360);
const geometryTxEast = _tx(geometry, -360);

let result: Polygon | MultiPolygon | null = worldClip;

if (result && geometryTxWest) {
const worldWestClip = _clean(
bboxClip(geometryTxWest, WORLD).geometry as Polygon | MultiPolygon
);
if (worldWestClip) {
const collection = featureCollection([
feature(result),
feature(worldWestClip),
]);
const merged = union(collection);
result = merged ? _clean(merged.geometry) : result;
}
}

if (result && geometryTxEast) {
const worldEastClip = _clean(
bboxClip(geometryTxEast, WORLD).geometry as Polygon | MultiPolygon
);
if (worldEastClip) {
const collection = featureCollection([
feature(result),
feature(worldEastClip),
]);
const merged = union(collection);
result = merged ? _clean(merged.geometry) : result;
}
}

return result;
}

/** @internalRemarks Source: @carto/react-core */
function _cleanPolygonCoords(cc: Position[][]) {
const coords = cc.filter((c) => c.length > 0);
return coords.length > 0 ? coords : null;
}

/** @internalRemarks Source: @carto/react-core */
function _cleanMultiPolygonCoords(ccc: Position[][][]) {
const coords = ccc.map(_cleanPolygonCoords).filter((cc) => cc);
return coords.length > 0 ? coords : null;
}

/** @internalRemarks Source: @carto/react-core */
function _clean(
geometry: Polygon | MultiPolygon
): Polygon | MultiPolygon | null {
if (!geometry) {
return null;
} else if (getType(geometry) === 'Polygon') {
const coords = _cleanPolygonCoords((geometry as Polygon).coordinates);
return coords ? polygon(coords).geometry : null;
} else if (getType(geometry) === 'MultiPolygon') {
const coords = _cleanMultiPolygonCoords(
(geometry as MultiPolygon).coordinates
);
return coords ? multiPolygon(coords as Position[][][]).geometry : null;
} else {
return null;
}
}

/** @internalRemarks Source: @carto/react-core */
function _txContourCoords(cc: Position[], distance: number) {
return cc.map((c) => [c[0] + distance, c[1]]);
}

/** @internalRemarks Source: @carto/react-core */
function _txPolygonCoords(ccc: Position[][], distance: number) {
return ccc.map((cc) => _txContourCoords(cc, distance));
}

/** @internalRemarks Source: @carto/react-core */
function _txMultiPolygonCoords(cccc: Position[][][], distance: number) {
return cccc.map((ccc) => _txPolygonCoords(ccc, distance));
}

/** @internalRemarks Source: @carto/react-core */
function _tx(geometry: Polygon | MultiPolygon, distance: number) {
if (geometry && getType(geometry) === 'Polygon') {
const coords = _txPolygonCoords(
(geometry as Polygon).coordinates,
distance
);
return polygon(coords).geometry;
} else if (geometry && getType(geometry) === 'MultiPolygon') {
const coords = _txMultiPolygonCoords(
(geometry as MultiPolygon).coordinates,
distance
);
return multiPolygon(coords).geometry;
} else {
return null;
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './client.js';
export * from './constants.js';
export * from './filters.js';
export * from './geo.js';
export * from './sources/index.js';
export * from './types.js';
Loading

0 comments on commit ddfb4e7

Please sign in to comment.