diff --git a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx deleted file mode 100644 index ede7b0ad24cb7ad8501228a934d00928ac5d5759..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useMemo } from 'react'; -import { SettingsContainer } from '../../components/config'; -import { layerTypes } from './components/layers'; -import { EntityPill, Input } from '../../..'; -import { VisualizationSettingsPropTypes } from '../../common'; -import { MapProps } from './mapvis'; -import { nodeColorHex } from '../nodelinkvis/components/utils'; - -const DataLayerSettings = ({ - layer, - settings, - graphMetadata, - updateSettings, -}: VisualizationSettingsPropTypes<MapProps> & { layer: keyof typeof layerTypes }) => { - switch (layer) { - case 'nodelink': - return ( - <> - {graphMetadata.nodes.labels.map((item, index) => ( - <div className="flex m-1 items-center" key={item}> - <div className="w-3/4 mr-6"> - <EntityPill title={item} /> - </div> - <div className="w-1/2"> - <div className={`h-5 w-5 border-2 border-sec-300`} style={{ backgroundColor: nodeColorHex(index + 1) }}></div> - </div> - </div> - ))} - - <Input - label="Enable brushing" - type="boolean" - value={settings.enableBrushing} - onChange={(val) => { - console.log('update brush', val); - updateSettings({ enableBrushing: val as boolean }); - }} - /> - </> - ); - default: - return; - } -}; - -export const MapSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) => { - const spatialAttributes = useMemo(() => { - if (!settings.node || !(Object.keys(graphMetadata.nodes.types).length > 0)) return []; - return Object.entries(graphMetadata.nodes.types[settings.node].attributes).map((kv) => kv[0]); - }, [settings.node]); - - return ( - <SettingsContainer> - <Input - label="Data layer" - type="dropdown" - inline - value={settings.layer} - options={Object.keys(layerTypes)} - onChange={(val) => updateSettings({ layer: val as string })} - /> - - <Input - label="Node Label" - type="dropdown" - inline - value={settings.node} - options={[...Object.keys(graphMetadata.nodes.types)]} - disabled={Object.keys(graphMetadata.nodes.types).length < 1} - onChange={(val) => { - updateSettings({ node: val as string }); - }} - /> - <Input - label="Latitude Location" - type="dropdown" - inline - value={settings.lat} - options={[...spatialAttributes]} - disabled={!settings.node || spatialAttributes.length < 1} - onChange={(val) => updateSettings({ lat: val as string })} - /> - - <Input - inline - label="Longitude Location accessor" - type="dropdown" - value={settings.lon} - options={[...spatialAttributes]} - disabled={!settings.node || spatialAttributes.length < 1} - onChange={(val) => updateSettings({ lon: val as string })} - /> - - <DataLayerSettings layer={settings.layer} settings={settings} graphMetadata={graphMetadata} updateSettings={updateSettings} /> - </SettingsContainer> - ); -}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx deleted file mode 100644 index c09ce9458e592502b7df0c8f583092ff15142815..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ChoroplethLayer } from './choropleth-layer/newChoroplethLayer'; -import { HeatLayer } from './heatmap-layer/HeatLayer'; -import { NodeLinkLayer } from './nodelink-layer/NodeLinkLayer'; -import { NodeLayer } from './node-layer/NodeLayer'; -import { NodeIconLayer } from './icon-layer/IconLayer'; - -export const layerTypes: Record<string, any> = { - node: NodeLayer, - icon: NodeIconLayer, - nodelink: NodeLinkLayer, - choropleth: ChoroplethLayer, - heatmap: HeatLayer, -}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx deleted file mode 100644 index 45362508b668cc2ea7e259435ea7b49d5ebbf8d2..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { CompositeLayer } from 'deck.gl'; -import { LineLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers'; -import { LayerProps } from '../../../mapvis.types'; -import { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions'; - -export const NodeLinkConfig = { - showLabels: false, - nodeShapeDynamic: false, - shapeAccessor: '', - iconMapping: {}, - colorMapping: {}, - edgesOnHover: true, - nodeSizeDynamic: true, - nodeSize: 2, - edgeWidth: 1.5, -}; - -export class NodeLinkLayer extends CompositeLayer<LayerProps> { - static type = 'NodeLink'; - static layerOptions = NodeLinkConfig; - - shouldUpdateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) { - return changeFlags.propsChanged; - } - - renderLayers() { - const { graph, config, visible, getNodeLocation, selected } = this.props; - - const layers = []; - - const brushingExtension = new BrushingExtension(); - const collisionFilter = new CollisionFilterExtension(); - - layers.push( - new ScatterplotLayer({ - hidden: visible, - data: graph.nodes, - pickable: true, - radiusScale: 6, - radiusMinPixels: 7, - radiusMaxPixels: 100, - lineWidthMinPixels: 1, - getPosition: (d: any) => getNodeLocation(d.id), - getFillColor: (d: any) => { - if (d.label === 'PERSON') { - return [182, 154, 239]; - } else if (d.label === 'INCIDENT') { - return [169, 25, 25]; - } - return [0, 0, 0]; - }, - getRadius: (d: any) => 5, - }), - ); - - layers.push( - new LineLayer({ - id: 'edges', - data: graph.edges, - pickable: true, - getWidth: (d: any) => 2, - getSourcePosition: (d: any) => getNodeLocation(d.from), - getTargetPosition: (d: any) => getNodeLocation(d.to), - getColor: (d: any) => [145, 168, 208], - radiusScale: 3000, - brushingEnabled: config.enableBrushing, - extensions: [brushingExtension], - }), - ); - - layers.push( - new TextLayer({ - id: 'label-target', - data: graph.nodes, - getPosition: (d: any) => getNodeLocation(d.id), - getText: (d: any) => d.id, - getSize: 15, - visible: true, - getAlignmentBaseline: 'top', - background: true, - getPixelOffset: [10, 10], - extensions: [collisionFilter], - collisionEnabled: true, - getCollisionPriority: (d: any) => d.id, - collisionTestProps: { sizeScale: 10 }, - getRadius: 10, - radiusUnits: 'pixels', - collisionGroup: 'text', - }), - ); - - return [...layers]; - } -} - -NodeLinkLayer.layerName = 'NodeLink'; diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index 47eba03d3a9ceb79afc9bf702fbd52fecfe67021..4ee0dabf584e52572d609ad0429bb72fe22d10fa 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -1,29 +1,18 @@ -import React, { useEffect, useMemo, useCallback } from 'react'; +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 { layerTypes, createBaseMap } from './layers'; +import { MapSettings } from './settings'; import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; +import { HighlightAlt, SearchOutlined } from '@mui/icons-material'; +import SearchBar from './search'; -export type MapProps = { - layer: string; - node: undefined | string; - lat: string; - lon: string; - enableBrushing: boolean; -}; +export type MapProps = { layer: string }; -const settings: MapProps = { - layer: 'node', - node: undefined, - lat: 'gp_latitude', - lon: 'gp_longitude', - enableBrushing: false, -}; +const settings: MapProps = { layer: 'nodelink' }; const INITIAL_VIEW_STATE = { latitude: 52.1006, @@ -37,13 +26,16 @@ const FLY_SPEED = 1000; const baseLayer = createBaseMap(); -export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSelect }: VisualizationPropTypes<MapProps>) => { - const [layer, setLayer] = React.useState<Layer | undefined>(undefined); - const [viewport, setViewport] = React.useState<Record<string, any>>(INITIAL_VIEW_STATE); - const [hoverObject, setHoverObject] = React.useState<Node | null>(null); - const [selected, setSelected] = React.useState<any[]>([]); - const [isSelecting, setIsSelecting] = React.useState<boolean>(false); - const [selectingRectangle, setSelectingRectangle] = React.useState<boolean>(false); +export const MapVis = ({ data, settings, handleSelect, graphMetadata }: 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 [selectingRectangle, setSelectingRectangle] = useState<boolean>(false); + const [layerIds, setLayerIds] = useState<string[]>([]); + const [isSearching, setIsSearching] = useState<boolean>(false); + + console.log(settings); const getFittedViewport = useCallback( (minLat: number, maxLat: number, minLon: number, maxLon: number) => { @@ -78,31 +70,18 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe const layerType = settings.layer ? layerTypes?.[settings.layer] : layerTypes.node; setLayer({ - id: Date.now(), - name: 'New layer', type: layerType, config: settings, - visible: true, }); }, [settings.layer]); - useEffect(() => { - console.log('configuration.node', settings.node); - }, [settings.node]); - - 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; + if (!layer || !settings.layer) return null; const coordinateLookup: { [id: string]: Coordinate } = data.nodes.reduce( (acc, node) => { - const latitude = settings.lat ? (node?.attributes?.[settings.lat] as string) : undefined; - const longitude = settings.lon ? (node?.attributes?.[settings.lon] as string) : undefined; + const latitude = settings[node.label].lat ? (node?.attributes?.[settings[node.label].lat] as string) : undefined; + const longitude = settings[node.label].lon ? (node?.attributes?.[settings[node.label].lon] as string) : undefined; if (!!latitude && !!longitude) { acc[node._id] = [parseFloat(longitude), parseFloat(latitude)]; @@ -114,19 +93,18 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe ); return new layer.type({ - id: `${layer.id}`, graph: data, - visible: layer.visible, - config: layer.config, + metaData: graphMetadata, + config: settings, selected: selected, hoverObject: hoverObject, - isSelecting: isSelecting, - getNodeLocation: (d: Node) => { - return coordinateLookup[d._id]; + getNodeLocation: (d: string) => { + return coordinateLookup[d]; }, flyToBoundingBox: flyToBoundingBox, + setLayerIds: (val: string[]) => setLayerIds(val), }); - }, [layer, data, selected, hoverObject, isSelecting, settings.lat, settings.lon, settings.node]); + }, [layer, data, selected, hoverObject, settings]); const selectionLayer = useMemo( () => @@ -134,11 +112,29 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe new (SelectionLayer as any)({ id: 'selection', selectionType: 'rectangle', - onSelect: ({ pickingInfos }: any) => { - setSelected(pickingInfos.map((item: any) => item.object)); + onSelect: ({ pickingInfos }: { pickingInfos: any[] }) => { + if (pickingInfos.length > 0) { + const nodes = []; + const edges = []; + + for (const selectedItem of pickingInfos) { + const { object } = selectedItem; + if (object._id) { + if (object.from & object.to) { + edges.push(object); + } else { + nodes.push(object); + } + } + } + setSelected(nodes.map((node) => node._id)); + handleSelect({ nodes, edges }); + } else { + handleSelect(); + } setSelectingRectangle(false); }, - layerIds: [layer?.id ? layer.id : ''], + layerIds: layerIds, getTentativeFillColor: () => [22, 37, 67, 100], }), [selectingRectangle, layer], @@ -146,8 +142,24 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe return ( <div className="w-full h-full flex-grow relative overflow-hidden"> + <div className="absolute left-0 top-0 z-50 m-1"> + <div className="cursor-pointer p-1 bg-white shadow-md rounded mb-1" onClick={() => setSelectingRectangle(true)}> + <HighlightAlt /> + </div> + <div className="cursor-pointer p-1 bg-white shadow-md rounded" onClick={() => setIsSearching(!isSearching)}> + <SearchOutlined /> + </div> + </div> + {isSearching && ( + <SearchBar + onSearch={(boundingbox: [number, number, number, number]) => { + flyToBoundingBox(...boundingbox); + setIsSearching(false); + }} + /> + )} <DeckGL - layers={[baseLayer, dataLayer]} + layers={[baseLayer, dataLayer, selectionLayer]} controller={true} initialViewState={viewport} onViewStateChange={({ viewState }) => setViewport(viewState)} @@ -155,6 +167,7 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe if (data) { if (!object) { handleSelect(); + setSelected([]); return; } if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) { @@ -178,6 +191,12 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe setHoverObject(object !== undefined ? object : null); }} /> + <div className="absolute right-0 top-0 p-1 z-50 bg-white bg-opacity-75 text-xs"> + {'© '} + <a className="underline" href="http://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer"> + OpenStreetMap + </a> + </div> </div> ); };