import React, { useEffect, useMemo, useCallback, useState } from 'react'; import DeckGL from '@deck.gl/react'; import { FlyToInterpolator, WebMercatorViewport } from '@deck.gl/core'; import { SelectionLayer } from '@deck.gl-community/editable-layers'; import { Coordinate, Layer } from './mapvis.types'; import { VISComponentType, VisualizationPropTypes } from '../../common'; import { layerTypes } from './components/layers'; import { createBaseMap } from './components/BaseMap'; import { MapSettings } from './MapSettings'; import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; export type MapProps = { layer: string; node: undefined | string; lat: string; lon: string; enableBrushing: boolean; enableBrushing: boolean; }; const settings: MapProps = { layer: 'node', node: undefined, lat: 'gp_latitude', lon: 'gp_longitude', enableBrushing: false, enableBrushing: false, }; const INITIAL_VIEW_STATE = { latitude: 52.1006, longitude: 5.6464, zoom: 6, bearing: 0, pitch: 0, }; const FLY_SPEED = 1000; const baseLayer = createBaseMap(); export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSelect }: VisualizationPropTypes<MapProps>) => { const [layer, setLayer] = useState<Layer | undefined>(undefined); const [viewport, setViewport] = useState<Record<string, any>>(INITIAL_VIEW_STATE); const [hoverObject, setHoverObject] = useState<Node | null>(null); const [selected, setSelected] = useState<any[]>([]); const [isSelecting, setIsSelecting] = useState<boolean>(false); const [selectingRectangle, setSelectingRectangle] = useState<boolean>(false); const handleKeyDown = useCallback((event: KeyboardEvent) => { if (event.ctrlKey && event.key === 'a') { event.preventDefault(); setSelectingRectangle(true); } }, []); const handleKeyUp = useCallback((event: KeyboardEvent) => { if (event.key === 'Control' || event.key === 'a') { setSelectingRectangle(false); } }, []); useEffect(() => { window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); }; }, [handleKeyDown, handleKeyUp]); const getFittedViewport = useCallback( (minLat: number, maxLat: number, minLon: number, maxLon: number) => { const viewportWebMercator = new WebMercatorViewport(viewport).fitBounds( [ [minLon, minLat], [maxLon, maxLat], ], { padding: 20 }, ); const { zoom, longitude, latitude } = viewportWebMercator; return { zoom, longitude, latitude }; }, [viewport], ); const flyToBoundingBox = useCallback( (minLat: number, maxLat: number, minLon: number, maxLon: number, options = {}) => { const fittedViewport = getFittedViewport(minLat, maxLat, minLon, maxLon); setViewport((prevViewport) => ({ ...prevViewport, ...options, ...fittedViewport, transitionDuration: FLY_SPEED, transitionInterpolator: new FlyToInterpolator(), })); }, [getFittedViewport], ); useEffect(() => { const layerType = settings.layer ? layerTypes?.[settings.layer] : layerTypes.node; setLayer({ id: Date.now(), name: 'New layer', type: layerType, config: settings, config: settings, visible: true, }); }, [settings.layer]); useEffect(() => { if (settings.node != undefined && !graphMetadata.nodes.labels.includes(settings.node)) { updateSettings({ node: undefined }); } }, [graphMetadata.nodes.types, data, settings]); const dataLayer = useMemo(() => { if (!layer || !settings.node || !settings.lat || !settings.lon) return null; const coordinateLookup: { [id: string]: Coordinate } = data.nodes.reduce( (acc, node) => { const latitude = settings.lat ? (node?.attributes?.[settings[node.label].lat] as string) : undefined; const longitude = settings.lon ? (node?.attributes?.[settings[node.label].lon] as string) : undefined; if (!!latitude && !!longitude) { acc[node._id] = [parseFloat(longitude), parseFloat(latitude)]; } return acc; }, {} as { [id: string]: Coordinate }, ); return new layer.type({ id: `${layer.id}`, graph: data, visible: layer.visible, config: settings, selected: selected, hoverObject: hoverObject, isSelecting: isSelecting, getNodeLocation: (d: string) => { return coordinateLookup[d]; }, flyToBoundingBox: flyToBoundingBox, }); }, [layer, data, selected, hoverObject, isSelecting, settings]); const selectionLayer = useMemo( () => selectingRectangle && new (SelectionLayer as any)({ id: 'selection', selectionType: 'rectangle', onSelect: ({ pickingInfos }: any) => { setSelected(pickingInfos.map((item: any) => item.object)); setSelectingRectangle(false); }, layerIds: [layer?.id ? layer.id : ''], getTentativeFillColor: () => [22, 37, 67, 100], }), [selectingRectangle, layer], ); return ( <div className="w-full h-full flex-grow relative overflow-hidden"> <DeckGL layers={[baseLayer, dataLayer, selectionLayer]} controller={true} initialViewState={viewport} onViewStateChange={({ viewState }) => setViewport(viewState)} onClick={({ object }) => { if (data) { if (!object) { handleSelect(); setSelected([]); return; } if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) { handleSelect({ nodes: [object] }); setSelected([object.id]); } if (object.type === 'Feature') { const ids = object.properties.nodes; if (ids.length > 0) { const nodes = data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id)); handleSelect({ nodes: [...nodes] }); } else { handleSelect(); setSelected([]); return; } } } }} onHover={({ object }) => { setHoverObject(object !== undefined ? object : null); }} /> </div> ); }; export const MapComponent: VISComponentType<MapProps> = { displayName: 'MapVis', component: MapVis, settingsComponent: MapSettings, settings: settings, }; export default MapComponent;