@deck.gl/geo-layers
Version:
deck.gl layers supporting geospatial use cases and GIS formats
323 lines (285 loc) • 9.25 kB
JavaScript
import {
h3ToGeoBoundary,
h3GetResolution,
h3ToGeo,
geoToH3,
h3IsPentagon,
h3Distance,
edgeLength,
UNITS
} from 'h3-js';
import {lerp} from '@math.gl/core';
import {CompositeLayer, createIterable} from '@deck.gl/core';
import {ColumnLayer, PolygonLayer} from '@deck.gl/layers';
// There is a cost to updating the instanced geometries when using highPrecision: false
// This constant defines the distance between two hexagons that leads to "significant
// distortion." Smaller value makes the column layer more sensitive to viewport change.
const UPDATE_THRESHOLD_KM = 10;
// normalize longitudes w.r.t center (refLng), when not provided first vertex
export function normalizeLongitudes(vertices, refLng) {
refLng = refLng === undefined ? vertices[0][0] : refLng;
for (const pt of vertices) {
const deltaLng = pt[0] - refLng;
if (deltaLng > 180) {
pt[0] -= 360;
} else if (deltaLng < -180) {
pt[0] += 360;
}
}
}
// scale polygon vertices w.r.t center (hexId)
export function scalePolygon(hexId, vertices, factor) {
const [lat, lng] = h3ToGeo(hexId);
const actualCount = vertices.length;
// normalize with respect to center
normalizeLongitudes(vertices, lng);
// `h3ToGeoBoundary` returns same array object for first and last vertex (closed polygon),
// if so skip scaling the last vertex
const vertexCount = vertices[0] === vertices[actualCount - 1] ? actualCount - 1 : actualCount;
for (let i = 0; i < vertexCount; i++) {
vertices[i][0] = lerp(lng, vertices[i][0], factor);
vertices[i][1] = lerp(lat, vertices[i][1], factor);
}
}
function getHexagonCentroid(getHexagon, object, objectInfo) {
const hexagonId = getHexagon(object, objectInfo);
const [lat, lng] = h3ToGeo(hexagonId);
return [lng, lat];
}
function h3ToPolygon(hexId, coverage = 1, flatten) {
const vertices = h3ToGeoBoundary(hexId, true);
if (coverage !== 1) {
// scale and normalize vertices w.r.t to center
scalePolygon(hexId, vertices, coverage);
} else {
// normalize w.r.t to start vertex
normalizeLongitudes(vertices);
}
if (flatten) {
const positions = new Float64Array(vertices.length * 2);
let i = 0;
for (const pt of vertices) {
positions[i++] = pt[0];
positions[i++] = pt[1];
}
return positions;
}
return vertices;
}
function mergeTriggers(getHexagon, coverage) {
let trigger;
if (getHexagon === undefined || getHexagon === null) {
trigger = coverage;
} else if (typeof getHexagon === 'object') {
trigger = {...getHexagon, coverage};
} else {
trigger = {getHexagon, coverage};
}
return trigger;
}
const defaultProps = {
...PolygonLayer.defaultProps,
highPrecision: 'auto',
coverage: {type: 'number', min: 0, max: 1, value: 1},
centerHexagon: null,
getHexagon: {type: 'accessor', value: x => x.hexagon},
extruded: true
};
// not supported
delete defaultProps.getLineDashArray;
/**
* A subclass of HexagonLayer that uses H3 hexagonIds in data objects
* rather than centroid lat/longs. The shape of each hexagon is determined
* based on a single "center" hexagon, which can be selected by passing in
* a center lat/lon pair. If not provided, the map center will be used.
*
* Also sets the `hexagonId` field in the onHover/onClick callback's info
* objects. Since this is calculated using math, hexagonId will be present
* even when no corresponding hexagon is in the data set. You can check
* index !== -1 to see if picking matches an actual object.
*/
export default class H3HexagonLayer extends CompositeLayer {
shouldUpdateState({changeFlags}) {
return this._shouldUseHighPrecision()
? changeFlags.propsOrDataChanged
: changeFlags.somethingChanged;
}
updateState({props, oldProps, changeFlags}) {
if (
props.highPrecision !== true &&
(changeFlags.dataChanged ||
(changeFlags.updateTriggers && changeFlags.updateTriggers.getHexagon))
) {
const dataProps = this._calculateH3DataProps(props);
this.setState(dataProps);
}
this._updateVertices(this.context.viewport);
}
_calculateH3DataProps(props) {
let resolution = -1;
let hasPentagon = false;
let hasMultipleRes = false;
const {iterable, objectInfo} = createIterable(props.data);
for (const object of iterable) {
objectInfo.index++;
const hexId = props.getHexagon(object, objectInfo);
// Take the resolution of the first hex
const hexResolution = h3GetResolution(hexId);
if (resolution < 0) {
resolution = hexResolution;
if (!props.highPrecision) break;
} else if (resolution !== hexResolution) {
hasMultipleRes = true;
break;
}
if (h3IsPentagon(hexId)) {
hasPentagon = true;
break;
}
}
return {
resolution,
edgeLengthKM: resolution >= 0 ? edgeLength(resolution, UNITS.km) : 0,
hasMultipleRes,
hasPentagon
};
}
_shouldUseHighPrecision() {
if (this.props.highPrecision === 'auto') {
const {resolution, hasPentagon, hasMultipleRes} = this.state;
const {viewport} = this.context;
return (
viewport.resolution || hasMultipleRes || hasPentagon || (resolution >= 0 && resolution <= 5)
);
}
return this.props.highPrecision;
}
_updateVertices(viewport) {
if (this._shouldUseHighPrecision()) {
return;
}
const {resolution, edgeLengthKM, centerHex} = this.state;
if (resolution < 0) {
return;
}
const hex =
this.props.centerHexagon || geoToH3(viewport.latitude, viewport.longitude, resolution);
if (centerHex === hex) {
return;
}
if (centerHex) {
const distance = h3Distance(centerHex, hex);
// h3Distance returns a negative number if the distance could not be computed
// due to the two indexes very far apart or on opposite sides of a pentagon.
if (distance >= 0 && distance * edgeLengthKM < UPDATE_THRESHOLD_KM) {
return;
}
}
const {unitsPerMeter} = viewport.distanceScales;
let vertices = h3ToPolygon(hex);
const [centerLat, centerLng] = h3ToGeo(hex);
const [centerX, centerY] = viewport.projectFlat([centerLng, centerLat]);
vertices = vertices.map(p => {
const worldPosition = viewport.projectFlat(p);
return [
(worldPosition[0] - centerX) / unitsPerMeter[0],
(worldPosition[1] - centerY) / unitsPerMeter[1]
];
});
this.setState({centerHex: hex, vertices});
}
renderLayers() {
return this._shouldUseHighPrecision() ? this._renderPolygonLayer() : this._renderColumnLayer();
}
_getForwardProps() {
const {
elevationScale,
material,
coverage,
extruded,
wireframe,
stroked,
filled,
lineWidthUnits,
lineWidthScale,
lineWidthMinPixels,
lineWidthMaxPixels,
getFillColor,
getElevation,
getLineColor,
getLineWidth,
transitions,
updateTriggers
} = this.props;
return {
elevationScale,
extruded,
coverage,
wireframe,
stroked,
filled,
lineWidthUnits,
lineWidthScale,
lineWidthMinPixels,
lineWidthMaxPixels,
material,
getElevation,
getFillColor,
getLineColor,
getLineWidth,
transitions,
updateTriggers: {
getFillColor: updateTriggers.getFillColor,
getElevation: updateTriggers.getElevation,
getLineColor: updateTriggers.getLineColor,
getLineWidth: updateTriggers.getLineWidth
}
};
}
_renderPolygonLayer() {
const {data, getHexagon, updateTriggers, coverage} = this.props;
const SubLayerClass = this.getSubLayerClass('hexagon-cell-hifi', PolygonLayer);
const forwardProps = this._getForwardProps();
forwardProps.updateTriggers.getPolygon = mergeTriggers(updateTriggers.getHexagon, coverage);
return new SubLayerClass(
forwardProps,
this.getSubLayerProps({
id: 'hexagon-cell-hifi',
updateTriggers: forwardProps.updateTriggers
}),
{
data,
_normalize: false,
_windingOrder: 'CCW',
positionFormat: 'XY',
getPolygon: (object, objectInfo) => {
const hexagonId = getHexagon(object, objectInfo);
return h3ToPolygon(hexagonId, coverage, true);
}
}
);
}
_renderColumnLayer() {
const {data, getHexagon, updateTriggers} = this.props;
const SubLayerClass = this.getSubLayerClass('hexagon-cell', ColumnLayer);
const forwardProps = this._getForwardProps();
forwardProps.updateTriggers.getPosition = updateTriggers.getHexagon;
return new SubLayerClass(
forwardProps,
this.getSubLayerProps({
id: 'hexagon-cell',
flatShading: true,
updateTriggers: forwardProps.updateTriggers
}),
{
data,
diskResolution: 6, // generate an extruded hexagon as the base geometry
radius: 1,
vertices: this.state.vertices,
getPosition: getHexagonCentroid.bind(null, getHexagon)
}
);
}
}
H3HexagonLayer.defaultProps = defaultProps;
H3HexagonLayer.layerName = 'H3HexagonLayer';