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