From cb54eda73af75c2e03c88e7f99589dd6276ed4a7 Mon Sep 17 00:00:00 2001
From: Leonardo <leomilho@gmail.com>
Date: Tue, 18 Jun 2024 17:39:09 +0200
Subject: [PATCH] feat(map_nodelink): added nodelink layer

---
 .../vis/components/config/SelectionConfig.tsx |   4 +-
 .../vis/visualizations/mapvis/MapSettings.tsx |  50 ++++-
 .../mapvis/components/layers/index.tsx        |   2 +-
 .../layers/nodelink-layer/NodeLinkLayer.tsx   | 206 +++++-------------
 .../lib/vis/visualizations/mapvis/mapvis.tsx  |  51 +++--
 5 files changed, 132 insertions(+), 181 deletions(-)

diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx
index 7d41cf4a1..8fc395725 100644
--- a/libs/shared/lib/vis/components/config/SelectionConfig.tsx
+++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx
@@ -25,7 +25,7 @@ export const SelectionConfig = () => {
         />
       </div>
       {selection.content.map((item, index) => (
-        <>
+        <div key={index}>
           <div key={index + 'id'} className="flex justify-between items-center px-4 py-1 gap-1">
             <span className="text-xs font-normal">ID</span>
             <span className="text-xs">{item._id}</span>
@@ -43,7 +43,7 @@ export const SelectionConfig = () => {
               </div>
             );
           })}
-        </>
+        </div>
       ))}
     </div>
   );
diff --git a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
index 5e411a08c..ede7b0ad2 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
@@ -1,19 +1,51 @@
 import React, { useMemo } from 'react';
 import { SettingsContainer } from '../../components/config';
 import { layerTypes } from './components/layers';
-import { Input } from '../../..';
+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) return [];
-  //   return Object.entries(graphMetadata.nodes.types[settings.node].attributes)
-  //     .filter((kv) => kv[1].dimension === 'spatial')
-  //     .map((kv) => kv[0]);
-  // }, [settings.node]);
   const spatialAttributes = useMemo(() => {
-    if (!settings.node) return [];
+    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]);
 
@@ -58,6 +90,8 @@ export const MapSettings = ({ settings, graphMetadata, updateSettings }: Visuali
         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
index e4f8f75cd..c09ce9458 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx
@@ -7,7 +7,7 @@ import { NodeIconLayer } from './icon-layer/IconLayer';
 export const layerTypes: Record<string, any> = {
   node: NodeLayer,
   icon: NodeIconLayer,
-  // nodelink: NodeLinkLayer,
+  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
index 81a8c1a0b..45362508b 100644
--- 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
@@ -1,10 +1,8 @@
 import React from 'react';
 import { CompositeLayer } from 'deck.gl';
-import { IconLayer, LineLayer, TextLayer } from '@deck.gl/layers';
-import NodeLinkOptions from './NodeLinkOptions';
-import { createIcon } from './shapeFactory';
-import { getProperty } from '../../../utlis';
-import { Edge, Node, LayerProps } from '../../../mapvis.types';
+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,
@@ -22,159 +20,75 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> {
   static type = 'NodeLink';
   static layerOptions = NodeLinkConfig;
 
-  static generateLayerOptions(layer: any, updatedLayer: void, graphInfo: { [key: string]: any }, deleteLayer: void) {
-    return <NodeLinkOptions layer={layer} updatedLayer={updatedLayer} graphInfo={graphInfo} deleteLayer={deleteLayer} />;
-  }
-
   shouldUpdateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) {
     return changeFlags.propsChanged;
   }
 
-  updateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) {
-    console.log(props, oldProps, context, changeFlags);
-  }
-
   renderLayers() {
-    const { graph, config, visible, isSelecting, hoverObject } = this.props;
-
-    // if (!visible) return;
+    const { graph, config, visible, getNodeLocation, selected } = this.props;
 
     const layers = [];
 
-    layers.push(
-      new IconLayer(
-        this.getSubLayerProps({
-          id: 'all-nodes',
-          data: graph.getNodes().filter((node: Node) => !this.props.selected.includes(node)),
-          pickable: true,
-          sizeScale: 5,
-          getSize: (d: any) => (config.nodeSizeDynamic ? Math.min(d.connectedEdges.length, 5) : config.nodeSize),
-          opacity: this.props.selected.length > 0 ? 0.05 : 1,
-          getPosition: (d: any) => [d.attributes.long, d.attributes.lat],
-          getIcon: (d: any) => {
-            const shapeProperty = getProperty(d, `attributes.${this.props.config.shapeAccessor}`);
-            const shape = this.props.config.iconMapping[shapeProperty];
-            const color = this.props.config.colorMapping[shapeProperty] || [255, 125, 0];
+    const brushingExtension = new BrushingExtension();
+    const collisionFilter = new CollisionFilterExtension();
 
-            return {
-              url: createIcon(shape, color),
-              width: 24,
-              height: 24,
-            };
-          },
-          mask: true,
-        }),
-      ),
+    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,
+      }),
     );
 
-    if (this.props.selected.length > 0) {
-      const edges = graph.getEdges().filter((edge: Edge) => new Set(this.props.selected.map((node: Node) => node.id)).has(edge.from));
-
-      const nodes = edges.map((edge: Edge) => graph.getNode(edge.to));
-
-      layers.push([
-        new IconLayer(
-          this.getSubLayerProps({
-            id: 'selected-nodes',
-            data: this.props.selected,
-            pickable: true,
-            sizeScale: 5,
-            getSize: (d: any) => (config.nodeSizeDynamic ? Math.min(d.connectedEdges.length, 5) : config.nodeSize),
-            opacity: 1,
-            getPosition: (d: any) => [d.attributes.long, d.attributes.lat],
-            getIcon: (d: any) => {
-              const shapeProperty = getProperty(d, `attributes.${this.props.config.shapeAccessor}`);
-              const shape = this.props.config.iconMapping[shapeProperty];
-              const color = this.props.config.colorMapping[shapeProperty] || [255, 125, 0];
-              return {
-                url: createIcon(shape, color),
-                width: 24,
-                height: 24,
-              };
-            },
-            mask: true,
-            getColor: (d: any) => [200, 140, 0],
-          }),
-        ),
-        new IconLayer(
-          this.getSubLayerProps({
-            id: 'target-nodes',
-            data: nodes,
-            pickable: true,
-            sizeScale: 5,
-            getSize: (d: any) => (config.nodeSizeDynamic ? Math.min(d.connectedEdges.length, 5) : config.nodeSize),
-            opacity: 1,
-            getPosition: (d: any) => [d.attributes.long, d.attributes.lat],
-            getIcon: (d: any) => {
-              const shapeProperty = getProperty(d, `attributes.${this.props.config.shapeAccessor}`);
-              const shape = this.props.config.iconMapping[shapeProperty];
-              const color = this.props.config.colorMapping[shapeProperty] || [255, 125, 0];
-              return {
-                url: createIcon(shape, color),
-                width: 24,
-                height: 24,
-              };
-            },
-            mask: true,
-            getColor: (d: any) => [200, 140, 0],
-          }),
-        ),
-        new LineLayer(
-          this.getSubLayerProps({
-            id: 'edges',
-            data: edges,
-            pickable: true,
-            getWidth: (d: any) => config.edgeWidth,
-            getSourcePosition: (d: any) => graph.getNodeLocation(d.from),
-            getTargetPosition: (d: any) => graph.getNodeLocation(d.to),
-            getColor: (d: any) => [0, 0, 0],
-          }),
-        ),
-        new TextLayer(
-          this.getSubLayerProps({
-            id: 'label-selected',
-            data: this.props.selected,
-            getPosition: (d: any) => [d.attributes.long, d.attributes.lat],
-            getText: (d: any) => d.id,
-            getSize: 15,
-            visible: config.showLabels,
-            getAlignmentBaseline: 'top',
-            background: true,
-            getBackgroundColor: [255, 125, 0],
-            getPixelOffset: [10, 10],
-          }),
-        ),
-        new TextLayer(
-          this.getSubLayerProps({
-            id: 'label-target',
-            data: nodes,
-            getPosition: (d: any) => [d.attributes.long, d.attributes.lat],
-            getText: (d: any) => d.id,
-            getSize: 15,
-            visible: config.showLabels,
-            getAlignmentBaseline: 'top',
-            background: true,
-            getPixelOffset: [10, 10],
-          }),
-        ),
-      ]);
-    }
+    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],
+      }),
+    );
 
-    if (hoverObject && config.edgesOnHover) {
-      layers.push(
-        new LineLayer(
-          this.getSubLayerProps({
-            id: 'edges-hover',
-            data: graph.getEdges().filter((edge: Edge) => edge.from === hoverObject.id),
-            pickable: true,
-            getWidth: (d: any) => config.edgeWidth,
-            getSourcePosition: (d: any) => graph.getNodeLocation(d.from),
-            getTargetPosition: (d: any) => graph.getNodeLocation(d.to),
-            getColor: (d: any) => [0, 0, 0],
-          }),
-        ),
-      );
-    }
+    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];
   }
diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
index 9a254d9de..47eba03d3 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
@@ -14,6 +14,7 @@ export type MapProps = {
   node: undefined | string;
   lat: string;
   lon: string;
+  enableBrushing: boolean;
 };
 
 const settings: MapProps = {
@@ -21,6 +22,7 @@ const settings: MapProps = {
   node: undefined,
   lat: 'gp_latitude',
   lon: 'gp_longitude',
+  enableBrushing: false,
 };
 
 const INITIAL_VIEW_STATE = {
@@ -35,7 +37,7 @@ const FLY_SPEED = 1000;
 
 const baseLayer = createBaseMap();
 
-export const MapVis = ({ data, settings, updateSettings, graphMetadata }: VisualizationPropTypes<MapProps>) => {
+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);
@@ -79,9 +81,7 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual
       id: Date.now(),
       name: 'New layer',
       type: layerType,
-      config: {
-        ...layerType.layerOptions,
-      },
+      config: settings,
       visible: true,
     });
   }, [settings.layer]);
@@ -112,7 +112,6 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual
       },
       {} as { [id: string]: Coordinate },
     );
-    // console.log('coordinateLookup', coordinateLookup);
 
     return new layer.type({
       id: `${layer.id}`,
@@ -145,24 +144,6 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual
     [selectingRectangle, layer],
   );
 
-  const handleSelect = useCallback((info: any, event: any) => {
-    const shiftPressed = event.srcEvent.shiftKey;
-    setIsSelecting(shiftPressed);
-    setSelected((prevSelected) => {
-      if (!shiftPressed) {
-        return info.object !== undefined ? [info.object] : [];
-      } else {
-        const selectedIndex = prevSelected.findIndex((obj) => obj === info.object);
-        if (selectedIndex !== -1) {
-          prevSelected.splice(selectedIndex, 1);
-        } else {
-          prevSelected.push(info.object);
-        }
-        return [...prevSelected];
-      }
-    });
-  }, []);
-
   return (
     <div className="w-full h-full flex-grow relative overflow-hidden">
       <DeckGL
@@ -170,7 +151,29 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual
         controller={true}
         initialViewState={viewport}
         onViewStateChange={({ viewState }) => setViewport(viewState)}
-        onClick={handleSelect}
+        onClick={({ object }) => {
+          if (data) {
+            if (!object) {
+              handleSelect();
+              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);
         }}
-- 
GitLab