// deck.gl-community // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors /* eslint-env browser */ import type {DefaultProps} from '@deck.gl/core'; import {H3ClusterLayer} from '@deck.gl/geo-layers'; // TODO: Fix H3 support. // import { polyfill, geoToH3 } from 'h3-js'; import {PROJECTED_PIXEL_SIZE_MULTIPLIER} from '../constants'; import {EditableGeoJsonLayer} from './editable-geojson-layer'; import {EditableLayer, EditableLayerProps} from './editable-layer'; import {ViewMode} from '../edit-modes/view-mode'; const DEFAULT_EDIT_MODE = ViewMode; const DEFAULT_H3_RESOLUTION = 9; const EMPTY_FEATURE_COLLECTION = { type: 'FeatureCollection', features: [] }; export type EditableH3ClusterLayerProps = EditableLayerProps & { data: DataT; resolution?: number; mode?: any; modeConfig?: any; selectedIndexes?: number[]; getEditedCluster?: (updatedHexagons: any[], existingCluster: any) => any; getHexagons?: (d) => number[]; onEdit?: (updatedData?, editType?: string, featureIndexes?: number[], editContext?) => void; filled?: boolean; stroked?: boolean; lineWidthScale?: number; lineWidthMinPixels?: number; lineWidthMaxPixels?: number; lineWidthUnits?: string; }; const defaultProps: DefaultProps> = { mode: DEFAULT_EDIT_MODE, ...EditableGeoJsonLayer.defaultProps, // h3 layer data: [], selectedIndexes: [], filled: false, stroked: true, lineWidthScale: PROJECTED_PIXEL_SIZE_MULTIPLIER, lineWidthMinPixels: 1, lineWidthMaxPixels: Number.MAX_SAFE_INTEGER, lineWidthUnits: 'pixels', getHexagons: (d) => d.hexIds, getEditedCluster: (updatedHexagons, existingCluster) => { if (existingCluster) { return { ...existingCluster, hexIds: updatedHexagons }; } return { hexIds: updatedHexagons }; }, resolution: DEFAULT_H3_RESOLUTION }; export class EditableH3ClusterLayer extends EditableLayer> { static layerName = 'EditableH3ClusterLayer'; static defaultProps = defaultProps; state: EditableLayer['state'] & { cursor?: 'grabbing' | 'grab' | null; tentativeHexagonIDs: string[]; } = undefined!; initializeState() { super.initializeState(); this.setState({ tentativeHexagonIDs: [] }); } // convert array of (lng, lat) coords to cluster of hexes getDerivedHexagonIDs(coords) { throw new Error('not implemented'); // TODO // return polyfill(coords, this.props.resolution, true); } // convert pair of (lng, lat) coords into single hex getDerivedHexagonID(coords) { throw new Error('not implemented'); // TODO // return geoToH3(coords[1], coords[0], this.props.resolution); } renderLayers() { const layers: any = [ new EditableGeoJsonLayer( this.getSubLayerProps({ id: 'editable-geojson', mode: this.props.mode, data: EMPTY_FEATURE_COLLECTION, selectedFeatureIndexes: [], onEdit: (editAction) => { const {editType, editContext} = editAction; switch (editType) { case 'updateTentativeFeature': // tentative feature updates, updated on every pointer move if (editContext.feature.geometry.type === 'Polygon') { const coords = editContext.feature.geometry.coordinates; const hexIDs = this.getDerivedHexagonIDs(coords); this.setState({tentativeHexagonIDs: hexIDs}); } else if (editContext.feature.geometry.type === 'Point') { const coords = editContext.feature.geometry.coordinates; const hexID = this.getDerivedHexagonID(coords); this.setState({tentativeHexagonIDs: [hexID]}); } break; case 'addFeature': const updatedData = [...this.props.data]; const {modeConfig} = this.props; if (!modeConfig || !modeConfig.booleanOperation) { // add new h3 cluster updatedData.push( this.props.getEditedCluster(this.state.tentativeHexagonIDs, null) ); } else if (this.props.selectedIndexes.length !== 1) { // eslint-disable-next-line no-console,no-undef console.warn('booleanOperation only supported for single cluster selection'); } else { // they're affecting a selected cluster let finalHexagonIDs: (string | number)[]; const committedHexagonIDs = new Set(this.getSelectedHexIDs()); const tentativeHexagonIDs = new Set( this.state.tentativeHexagonIDs ); switch (modeConfig.booleanOperation) { case 'union': default: finalHexagonIDs = [ ...new Set([...committedHexagonIDs, ...tentativeHexagonIDs]) ]; break; case 'intersection': finalHexagonIDs = [...committedHexagonIDs].filter((hexID: string | number) => tentativeHexagonIDs.has(hexID) ); break; case 'difference': finalHexagonIDs = [...committedHexagonIDs].filter( (hexID: string | number) => !tentativeHexagonIDs.has(hexID) ); break; } const selectedIndex = this.props.selectedIndexes[0]; const existingCluster = this.props.data[selectedIndex]; updatedData[selectedIndex] = this.props.getEditedCluster( finalHexagonIDs, existingCluster ); } this.setState({ tentativeHexagonIDs: [] }); this.props.onEdit({updatedData}); break; default: break; } } }) ), new H3ClusterLayer( this.getSubLayerProps({ id: 'hexagons', data: this.props.data, getHexagons: this.props.getHexagons }) ), new H3ClusterLayer( this.getSubLayerProps({ id: 'tentative-hexagons', data: [ { hexIds: this.state.tentativeHexagonIDs } ], getHexagons: (d) => d.hexIds }) ) ]; return layers; } // because data is an array of hexagon data, we take the cumulative of all selected indexes, // using props.getHexagons to support multiple data types getSelectedHexIDs() { let cumulativeHexIDs: number[] = []; this.props.selectedIndexes.forEach((index) => { const selectedCluster = this.props.data[index]; const hexIDs = this.props.getHexagons(selectedCluster); cumulativeHexIDs = cumulativeHexIDs.concat(hexIDs); }); return cumulativeHexIDs; } getCursor({isDragging}: {isDragging: boolean}): 'grabbing' | 'grab' { let {cursor} = this.state || {}; if (!cursor) { // default cursor cursor = isDragging ? 'grabbing' : 'grab'; } return cursor; } }