From dd65283ae2671ff970ebb724070b2b3a8696f4c1 Mon Sep 17 00:00:00 2001
From: "Vink, S.A. (Sjoerd)" <s.a.vink@uu.nl>
Date: Tue, 9 Jul 2024 14:48:32 +0200
Subject: [PATCH] feat(map_nodelink): rebase

---
 .../vis/visualizations/mapvis/MapSettings.tsx |  97 --------------
 .../mapvis/components/layers/index.tsx        |  13 --
 .../layers/nodelink-layer/NodeLinkLayer.tsx   |  97 --------------
 .../lib/vis/visualizations/mapvis/mapvis.tsx  | 123 ++++++++++--------
 4 files changed, 71 insertions(+), 259 deletions(-)
 delete mode 100644 libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
 delete mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx
 delete mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx

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 ede7b0ad2..000000000
--- 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 c09ce9458..000000000
--- 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 45362508b..000000000
--- 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 47eba03d3..4ee0dabf5 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>
   );
 };
-- 
GitLab