From dcf3d1ad7ad6fee38f898d14f55e6f9e5d51c4cb 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/visualizations/mapvis/MapSettings.tsx | 97 +++++++++++++++++++
 .../mapvis/components/layers/index.tsx        | 13 +++
 .../layers/nodelink-layer/NodeLinkLayer.tsx   | 97 +++++++++++++++++++
 3 files changed, 207 insertions(+)
 create mode 100644 libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx
 create 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
new file mode 100644
index 000000000..ede7b0ad2
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
@@ -0,0 +1,97 @@
+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
new file mode 100644
index 000000000..c09ce9458
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 000000000..45362508b
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx
@@ -0,0 +1,97 @@
+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';
-- 
GitLab