From 14fbcde542f6c487dd5e3133d1455dcbaf00eb2d Mon Sep 17 00:00:00 2001
From: "Vink, S.A. (Sjoerd)" <s.a.vink@uu.nl>
Date: Tue, 13 Aug 2024 15:43:43 +0000
Subject: [PATCH] feat(mapvisSearch): tooltip for search result location

---
 .../mapvis/components/ActionBar.tsx           |  8 ++-
 .../mapvis/components/Attribution.tsx         |  2 +-
 .../mapvis/components/SearchBar.tsx           |  4 +-
 .../mapvis/components/Tooltip.tsx             | 57 +++++++++++++------
 .../visualizations/mapvis/components/index.ts |  4 ++
 .../lib/vis/visualizations/mapvis/mapvis.tsx  | 42 +++++++++-----
 .../vis/visualizations/mapvis/mapvis.types.ts | 13 +++++
 7 files changed, 94 insertions(+), 36 deletions(-)
 create mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/index.ts

diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/ActionBar.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/ActionBar.tsx
index 41f4e58db..975ebd3b1 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/ActionBar.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/ActionBar.tsx
@@ -5,12 +5,13 @@ import { SearchBar } from './SearchBar';
 
 type Props = {
   isSearching: boolean;
-  setSelectingRectangle: (val: boolean) => void;
   setIsSearching: (val: boolean) => void;
+  setSearchResult: (val: any) => void;
+  setSelectingRectangle: (val: boolean) => void;
   flyToBoundingBox: (minLat: number, maxLat: number, minLon: number, maxLon: number) => void;
 };
 
-export default function ActionBar({ isSearching, setIsSearching, setSelectingRectangle, flyToBoundingBox }: Props) {
+export function ActionBar({ isSearching, setIsSearching, setSearchResult, setSelectingRectangle, flyToBoundingBox }: Props) {
   return (
     <div>
       <div className="absolute left-0 top-0 m-1">
@@ -23,9 +24,10 @@ export default function ActionBar({ isSearching, setIsSearching, setSelectingRec
       </div>
       {isSearching && (
         <SearchBar
-          onSearch={(boundingBox: BoundingBoxType) => {
+          onSearch={(boundingBox: BoundingBoxType, locationInfo) => {
             flyToBoundingBox(...boundingBox);
             setIsSearching(false);
+            setSearchResult(locationInfo);
           }}
         />
       )}
diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/Attribution.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/Attribution.tsx
index 34d421ddd..3ed032f0d 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/Attribution.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/Attribution.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
 
-export default function Attribution() {
+export function Attribution() {
   return (
     <div className="absolute right-0 bottom-0 p-1 bg-secondary-200 bg-opacity-75 text-xs">
       {'© '}
diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/SearchBar.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/SearchBar.tsx
index 05626d492..8f53c2c12 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/SearchBar.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/SearchBar.tsx
@@ -5,7 +5,7 @@ import React, { useState } from 'react';
 import { BoundingBoxType } from '../mapvis.types';
 
 interface SearchBarProps {
-  onSearch: (boundingBox: BoundingBoxType) => void;
+  onSearch: (boundingBox: BoundingBoxType, locationInfo: any) => void;
 }
 
 export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
@@ -22,7 +22,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
       if (data.length > 0) {
         const { boundingbox } = data[0];
         if (boundingbox) {
-          onSearch(boundingbox.map(parseFloat));
+          onSearch(boundingbox.map(parseFloat), data[0]);
         }
       } else {
         dispatch(addError('No results found'));
diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/Tooltip.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/Tooltip.tsx
index dd1d546bf..772e9ea72 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/Tooltip.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/Tooltip.tsx
@@ -1,21 +1,15 @@
 import React from 'react';
-import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components';
 import { NodeType } from '../../nodelinkvis/types';
 import { GeoJsonType } from '../mapvis.types';
+import { SearchResultType } from '../mapvis.types';
 
 export type NodelinkPopupProps = {
-  type: 'node' | 'area';
-  data: {
-    node: NodeType | GeoJsonType;
-    pos: {
-      x: number;
-      y: number;
-    };
-  };
+  type: 'node' | 'area' | 'location';
+  data: NodeType | GeoJsonType | SearchResultType;
   onClose: () => void;
 };
 
-const isGeoJsonType = (data: NodeType | GeoJsonType): data is GeoJsonType => {
+const isGeoJsonType = (data: NodeType | GeoJsonType | SearchResultType): data is GeoJsonType => {
   return (data as GeoJsonType).properties !== undefined;
 };
 
@@ -59,24 +53,55 @@ export const MapTooltip = (props: NodelinkPopupProps) => {
     </div>
   );
 
+  const renderLocationDetails = (location: SearchResultType) => (
+    <div>
+      <div className="flex flex-row gap-3">
+        <span>Name: </span>
+        <span className="ml-auto max-w-[10rem] text-right truncate">
+          <span>{location?.display_name}</span>
+        </span>
+      </div>
+      <div className="flex flex-row gap-3">
+        <span>Type: </span>
+        <span className="ml-auto max-w-[10rem] text-right truncate">
+          <span>{location?.addresstype}</span>
+        </span>
+      </div>
+      <div className="flex flex-row gap-3">
+        <span>Coordinate: </span>
+        <span className="ml-auto max-w-[10rem] text-right truncate">
+          <span>
+            [{location?.lon}, {location?.lat}]
+          </span>
+        </span>
+      </div>
+    </div>
+  );
+
   return (
     <div className="text-[0.9rem] min-w-[10rem]">
       <div className="card-body p-0">
         <span className="px-2.5 pt-2">
           <span>{type === 'node' ? 'Node' : 'Area'}</span>
           <span className="float-right">
-            {type === 'node' ? (data.node as NodeType)?._id : isGeoJsonType(data.node) ? data.node.properties?.name : 'N/A'}
+            {type === 'node'
+              ? (data as NodeType)?._id
+              : isGeoJsonType(data)
+                ? data.properties?.name
+                : type === 'location'
+                  ? (data as SearchResultType)?.name
+                  : 'N/A'}
           </span>
         </span>
         <div className="h-[1px] w-full bg-secondary-200"></div>
         <div className="px-2.5 text-[0.8rem]">
           {type === 'node'
-            ? data.node && 'attributes' in data.node
-              ? renderNodeDetails(data.node as NodeType)
+            ? data && 'attributes' in data
+              ? renderNodeDetails(data as NodeType)
               : null
-            : data.node && isGeoJsonType(data.node)
-              ? renderAreaDetails(data.node as GeoJsonType)
-              : null}
+            : data && isGeoJsonType(data)
+              ? renderAreaDetails(data as GeoJsonType)
+              : renderLocationDetails(data as SearchResultType)}
         </div>
         <div className="h-[1px] w-full"></div>
       </div>
diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/index.ts b/libs/shared/lib/vis/visualizations/mapvis/components/index.ts
new file mode 100644
index 000000000..9b32285c3
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/index.ts
@@ -0,0 +1,4 @@
+export * from './ActionBar';
+export * from './Attribution';
+export * from './MapSettings';
+export * from './Tooltip';
diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
index b18b40972..e6f2a240c 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
@@ -1,15 +1,12 @@
-import React, { useEffect, useMemo, useCallback, useState, useRef } from 'react';
+import React, { useEffect, useCallback, useState, useRef } from 'react';
 import DeckGL from '@deck.gl/react';
 import { CompositeLayer, FlyToInterpolator, WebMercatorViewport } from '@deck.gl/core';
-import { CompositeLayerType, Coordinate, LayerSettingsType, LocationInfo } from './mapvis.types';
+import { CompositeLayerType, Coordinate, LayerSettingsType, LocationInfo, SearchResultType } from './mapvis.types';
 import { VISComponentType, VisualizationPropTypes } from '../../common';
 import { layerTypes, createBaseMap, LayerTypes } from './layers';
-import { MapSettings } from './components/MapSettings';
 import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
-import { MapTooltip } from './components/Tooltip';
 import { geoCentroid } from 'd3';
-import Attribution from './components/Attribution';
-import ActionBar from './components/ActionBar';
+import { Attribution, ActionBar, MapTooltip, MapSettings } from './components';
 import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components';
 import { useSelectionLayer, useCoordinateLookup } from './hooks';
 
@@ -42,6 +39,7 @@ export const MapVis = (props: VisualizationPropTypes<MapProps>) => {
   const [selectingRectangle, setSelectingRectangle] = useState<boolean>(false);
   const [layerIds, setLayerIds] = useState<string[]>([]);
   const [isSearching, setIsSearching] = useState<boolean>(false);
+  const [searchResult, setSearchResult] = useState<SearchResultType | undefined>(undefined);
 
   const coordinateLookup = useCoordinateLookup(props.data.nodes, props.settings.location);
 
@@ -154,6 +152,7 @@ export const MapVis = (props: VisualizationPropTypes<MapProps>) => {
         if (!object) {
           props.handleSelect();
           setSelected([]);
+          setSearchResult(undefined);
           return;
         }
         if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) {
@@ -181,6 +180,17 @@ export const MapVis = (props: VisualizationPropTypes<MapProps>) => {
     [props, coordinateLookup, coordinateToXY],
   );
 
+  useEffect(() => {
+    if (searchResult) {
+      const coordinate: Coordinate = [parseFloat(searchResult.lon), parseFloat(searchResult.lat)];
+      const [x, y] = coordinateToXY(coordinate);
+
+      if (searchResult.x !== x || searchResult.y !== y) {
+        setSearchResult((prev) => (prev ? { ...prev, x, y } : undefined));
+      }
+    }
+  }, [viewport, searchResult, coordinateToXY]);
+
   return (
     <div className="w-full h-full flex-grow relative overflow-hidden" ref={ref}>
       <DeckGL
@@ -193,8 +203,9 @@ export const MapVis = (props: VisualizationPropTypes<MapProps>) => {
       />
       <ActionBar
         isSearching={isSearching}
-        setSelectingRectangle={setSelectingRectangle}
         setIsSearching={setIsSearching}
+        setSearchResult={setSearchResult}
+        setSelectingRectangle={setSelectingRectangle}
         flyToBoundingBox={flyToBoundingBox}
       />
       {selected.length > 0 &&
@@ -202,21 +213,24 @@ export const MapVis = (props: VisualizationPropTypes<MapProps>) => {
           <Tooltip key={index} open={true} interactive={false} boundaryElement={ref} showArrow={true}>
             <TooltipTrigger x={node.x} y={node.y} />
             <TooltipContent>
-              <MapTooltip
-                type={node.selectedType}
-                onClose={() => {}}
-                data={{ node: { ...node }, pos: { x: node.x, y: node.y } }}
-                key={node._id}
-              />
+              <MapTooltip type={node.selectedType} onClose={() => {}} data={{ ...node }} key={node._id} />
             </TooltipContent>
           </Tooltip>
         ))}
+      {searchResult && (
+        <Tooltip open={true} interactive={false} boundaryElement={ref} showArrow={true}>
+          <TooltipTrigger x={searchResult.x} y={searchResult.y} />
+          <TooltipContent>
+            <MapTooltip type="location" onClose={() => {}} data={{ ...searchResult }} key={searchResult.name} />
+          </TooltipContent>
+        </Tooltip>
+      )}
       <Attribution />
     </div>
   );
 };
 
-export const MapComponent: VISComponentType<MapProps> = {
+const MapComponent: VISComponentType<MapProps> = {
   displayName: 'MapVis',
   description: 'Geographical Features',
   component: MapVis,
diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts b/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts
index ca106950c..08ef8bd7a 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts
+++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts
@@ -105,4 +105,17 @@ export type GeoJsonType = {
   [id: string]: any;
 };
 
+export type SearchResultType = {
+  addresstype: string;
+  boundingbox: number[];
+  class: string;
+  display_name: string;
+  name: string;
+  lat: string;
+  lon: string;
+  type: string;
+  x?: number;
+  y?: number;
+};
+
 export type BoundingBoxType = [number, number, number, number];
-- 
GitLab