From 468173e24c44a115b59c251fb6d3ab84e3f4691c Mon Sep 17 00:00:00 2001
From: "Vink, S.A. (Sjoerd)" <s.a.vink@uu.nl>
Date: Tue, 9 Jul 2024 09:04:47 +0200
Subject: [PATCH] feat(map_nodelink): search and select

---
 .../vis/visualizations/mapvis/MapSettings.tsx |  40 +-----
 .../layers/heatmap-layer/HeatLayer.tsx        |  19 ++-
 .../layers/icon-layer/IconLayer.tsx           |  21 +++-
 .../layers/node-layer/NodeLayer.tsx           |  21 +++-
 .../layers/nodelink-layer/NodeLinkLayer.tsx   |  26 ++--
 .../vis/visualizations/mapvis/graphModel.tsx  | 119 ------------------
 .../lib/vis/visualizations/mapvis/mapvis.tsx  | 115 +++++++++--------
 .../vis/visualizations/mapvis/mapvis.types.ts |   3 +-
 .../lib/vis/visualizations/mapvis/search.tsx  |  46 +++++++
 9 files changed, 174 insertions(+), 236 deletions(-)
 delete mode 100644 libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/mapvis/search.tsx

diff --git a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
index 7f4245b4c..8ffe587a1 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx
@@ -1,47 +1,9 @@
 import React from 'react';
 import { SettingsContainer } from '../../components/config';
 import { layerSettings, layerTypes } from './components/layers';
-import { EntityPill, Input } from '../../..';
+import { 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 DataLayerSettings = settings.layer && layerSettings?.[settings.layer];
diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx
index dfe99c570..c991eb3a9 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx
@@ -44,10 +44,16 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
   }
 
   renderLayers() {
-    const { graph, config, getNodeLocation } = this.props;
+    const { graph, config, getNodeLocation, setLayerIds } = this.props;
 
-    return graph.metaData.nodes.labels.map(
-      (label: string) =>
+    const layers: any[] = [];
+    const layerIds: string[] = [];
+
+    graph.metaData.nodes.labels.forEach((label: string) => {
+      const layerId = `${label}-nodes-iconlayer`;
+      layerIds.push(layerId);
+
+      layers.push(
         new HeatmapLayer({
           id: `${label}-nodes-iconlayer`,
           data: graph.nodes.filter((node: Node) => node.label === label),
@@ -56,7 +62,12 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
           getWeight: (d) => 1,
           aggregation: 'SUM',
         }),
-    );
+      );
+    });
+
+    setLayerIds(layerIds);
+
+    return layers;
 
     // const layers = [];
 
diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/icon-layer/IconLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/icon-layer/IconLayer.tsx
index 6f3a9520f..7db411237 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/icon-layer/IconLayer.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/icon-layer/IconLayer.tsx
@@ -16,12 +16,18 @@ export class NodeIconLayer extends CompositeLayer<LayerProps> {
   }
 
   renderLayers() {
-    const { graph, config, getNodeLocation } = this.props;
+    const { graph, config, getNodeLocation, setLayerIds } = this.props;
 
-    return graph.metaData.nodes.labels.map(
-      (label: string) =>
+    const layers: any[] = [];
+    const layerIds: string[] = [];
+
+    graph.metaData.nodes.labels.forEach((label: string) => {
+      const layerId = `${label}-nodes-iconlayer`;
+      layerIds.push(layerId);
+
+      layers.push(
         new IconLayer({
-          id: `${label}-nodes-iconlayer`,
+          id: layerId,
           data: graph.nodes.filter((node: Node) => node.label === label),
           visible: !config[label].hidden,
           iconAtlas: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png',
@@ -33,7 +39,12 @@ export class NodeIconLayer extends CompositeLayer<LayerProps> {
           getPosition: (d: any) => getNodeLocation(d._id),
           getSize: (d: any) => 3,
         }),
-    );
+      );
+    });
+
+    setLayerIds(layerIds);
+
+    return layers;
   }
 }
 
diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/node-layer/NodeLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/node-layer/NodeLayer.tsx
index af67e9b81..8177d5dd5 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/node-layer/NodeLayer.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/node-layer/NodeLayer.tsx
@@ -28,12 +28,18 @@ export class NodeLayer extends CompositeLayer<LayerProps> {
   }
 
   renderLayers() {
-    const { graph, config, getNodeLocation } = this.props;
+    const { graph, config, getNodeLocation, setLayerIds } = this.props;
 
-    return graph.metaData.nodes.labels.map(
-      (label: string) =>
+    const layers: any[] = [];
+    const layerIds: any[] = [];
+
+    graph.metaData.nodes.labels.forEach((label: string) => {
+      const layerId = `${label}-nodes-scatterplot`;
+      layerIds.push(layerId);
+
+      layers.push(
         new ScatterplotLayer({
-          id: `${label}-nodes-scatterplot`,
+          id: layerId,
           visible: !config[label].hidden,
           data: graph.nodes.filter((node: Node) => node.label === label),
           pickable: true,
@@ -46,6 +52,11 @@ export class NodeLayer extends CompositeLayer<LayerProps> {
           getFillColor: (d: any) => config[label].color,
           getRadius: (d: any) => this.getRadius(d, config),
         }),
-    );
+      );
+    });
+
+    setLayerIds(layerIds);
+
+    return layers;
   }
 }
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 1a8a8626c..b6b436a46 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
@@ -13,17 +13,21 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> {
   }
 
   renderLayers() {
-    const { graph, config, getNodeLocation, selected } = this.props;
+    const { graph, config, getNodeLocation, selected, setLayerIds } = this.props;
 
     const layers = [];
+    const layerIds = [];
 
     const brushingExtension = new BrushingExtension();
     const collisionFilter = new CollisionFilterExtension();
 
-    graph.metaData.nodes.labels.map((label: string) => {
+    graph.metaData.nodes.labels.forEach((label: string) => {
+      const layerId = `${label}-nodes-scatterplot`;
+      layerIds.push(layerId);
+
       layers.push(
         new ScatterplotLayer({
-          id: `${label}-nodes-scatterplot`,
+          id: layerId,
           visible: !config[label].hidden,
           data: graph.nodes.filter((node: Node) => node.label === label),
           pickable: true,
@@ -39,13 +43,16 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> {
       );
     });
 
-    graph.metaData.edges.labels.map((label: string) => {
+    graph.metaData.edges.labels.forEach((label: string) => {
+      const layerId = `${label}-edges-line`;
+      layerIds.push(layerId);
+
       const edgeData =
         selected.length > 0 ? graph.edges.filter((edge: Edge) => selected.includes(edge.from) || selected.includes(edge.to)) : graph.edges;
 
       layers.push(
         new LineLayer({
-          id: `${label}-edges-line`,
+          id: layerId,
           data: edgeData,
           pickable: true,
           getWidth: (d: any) => config[label].width,
@@ -59,9 +66,12 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> {
       );
     });
 
+    const textLayerId = 'label-target';
+    layerIds.push(textLayerId);
+
     layers.push(
       new TextLayer({
-        id: 'label-target',
+        id: textLayerId,
         data: graph.nodes,
         getPosition: (d: any) => getNodeLocation(d._id),
         getText: (d: any) => d.id,
@@ -80,7 +90,9 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> {
       }),
     );
 
-    return [...layers];
+    setLayerIds(layerIds);
+
+    return layers;
   }
 }
 
diff --git a/libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx b/libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx
deleted file mode 100644
index 5cd11593e..000000000
--- a/libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { GraphType, Node, Edge, Coordinate } from './mapvis.types';
-
-export default class GraphModel implements GraphType {
-  nodeMap: { [id: string]: Node };
-  edgeMap: { [id: string]: Edge };
-  graphInfo: {
-    nodeAttributes: { [attribute: string]: any };
-    edgeAttributes: { [attribute: string]: any };
-  };
-
-  constructor() {
-    this.nodeMap = {};
-    this.edgeMap = {};
-
-    this.graphInfo = {
-      nodeAttributes: {},
-      edgeAttributes: {},
-    };
-  }
-
-  getNode(id: string): Node | null {
-    return this.nodeMap[id] ?? null;
-  }
-
-  getNodes(): Node[] {
-    return Object.values(this.nodeMap);
-  }
-
-  getNodeLocation(id: string): Coordinate | null {
-    const node: Node | null = this.getNode(id);
-    return node ? [node.attributes.long, node.attributes.lat] : null;
-  }
-
-  getConnectedEdges(nodeId: string) {
-    const node = this.getNode(nodeId);
-    if (node) {
-      const edgeIds = node.connectedEdges;
-      if (edgeIds.length > 0) {
-        const edges = edgeIds.map((id: string) => this.edgeMap[id]);
-        return edges;
-      } else {
-        return [];
-      }
-    } else {
-      return [];
-    }
-  }
-
-  getEdge(id: string): Edge | null {
-    return this.edgeMap[id] ?? null;
-  }
-
-  getEdges(): Edge[] {
-    return Object.values(this.edgeMap);
-  }
-
-  consumeMessageFromBackend(queryResult: { [key: string]: any }) {
-    queryResult.nodes.map((node: Node) => {
-      const connectedEdges: string[] = [];
-      queryResult.edges.map((edge: Edge) => {
-        this.edgeMap[edge.id] = edge;
-        this.collectAttributeInfo(this.graphInfo.edgeAttributes, edge);
-        if (edge.from === node.id || edge.to === node.id) {
-          connectedEdges.push(edge.id);
-        }
-      });
-
-      this.nodeMap[node.id] = { ...node, connectedEdges };
-      this.collectAttributeInfo(this.graphInfo.nodeAttributes, node);
-    });
-  }
-
-  getGraphInfo(): {
-    nodeAttributes: { [attribute: string]: any };
-    edgeAttributes: { [attribute: string]: any };
-  } {
-    return this.graphInfo;
-  }
-
-  collectAttributeInfo(attributeCollection: { [key: string]: any }, entity: any) {
-    for (const attribute in entity) {
-      const value = entity[attribute];
-      const dataType = typeof value;
-
-      if (dataType == 'object') {
-        this.collectAttributeInfo(attributeCollection, value);
-      } else {
-        if (attributeCollection[attribute]) {
-          attributeCollection[attribute].count++;
-          if (dataType == 'number') {
-            const currentRange = attributeCollection[attribute].values;
-            attributeCollection[attribute].values = [Math.min(currentRange[0], value), Math.max(currentRange[1], value)];
-          } else if (dataType == 'string') {
-            attributeCollection[attribute].values.add(value);
-          }
-        } else {
-          if (dataType == 'number') {
-            attributeCollection[attribute] = {
-              count: 1,
-              dataType,
-              values: [value, value],
-            };
-          } else if (dataType == 'string') {
-            attributeCollection[attribute] = {
-              count: 1,
-              dataType,
-              values: new Set([value]),
-            };
-          } else if (dataType == 'boolean') {
-            attributeCollection[attribute] = {
-              count: 1,
-              dataType,
-            };
-          }
-        }
-      }
-    }
-  }
-}
diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
index e122e2b27..2ba0f449b 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
@@ -8,22 +8,13 @@ import { layerTypes } from './components/layers';
 import { createBaseMap } from './components/BaseMap';
 import { MapSettings } from './MapSettings';
 import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
+import { HighlightAlt, SearchOutlined } from '@mui/icons-material';
+import SearchBar from './search';
+import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 
-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: 'node' };
 
 const INITIAL_VIEW_STATE = {
   latitude: 52.1006,
@@ -37,36 +28,14 @@ const FLY_SPEED = 1000;
 
 const baseLayer = createBaseMap();
 
-export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSelect }: VisualizationPropTypes<MapProps>) => {
+export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSelect, dispatch }: 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 [isSelecting, setIsSelecting] = useState<boolean>(false);
   const [selectingRectangle, setSelectingRectangle] = useState<boolean>(false);
-
-  const handleKeyDown = useCallback((event: KeyboardEvent) => {
-    if (event.ctrlKey && event.key === 'a') {
-      event.preventDefault();
-      setSelectingRectangle(true);
-    }
-  }, []);
-
-  const handleKeyUp = useCallback((event: KeyboardEvent) => {
-    if (event.key === 'Control' || event.key === 'a') {
-      setSelectingRectangle(false);
-    }
-  }, []);
-
-  useEffect(() => {
-    window.addEventListener('keydown', handleKeyDown);
-    window.addEventListener('keyup', handleKeyUp);
-
-    return () => {
-      window.removeEventListener('keydown', handleKeyDown);
-      window.removeEventListener('keyup', handleKeyUp);
-    };
-  }, [handleKeyDown, handleKeyUp]);
+  const [layerIds, setLayerIds] = useState<string[]>([]);
+  const [isSearching, setIsSearching] = useState<boolean>(false);
 
   const getFittedViewport = useCallback(
     (minLat: number, maxLat: number, minLon: number, maxLon: number) => {
@@ -99,29 +68,25 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe
 
   useEffect(() => {
     const layerType = settings.layer ? layerTypes?.[settings.layer] : layerTypes.node;
+    const newLayerId = `layer-${Date.now()}`;
 
     setLayer({
-      id: Date.now(),
-      name: 'New layer',
+      id: newLayerId,
       type: layerType,
       config: settings,
       visible: true,
     });
-  }, [settings.layer]);
 
-  useEffect(() => {
-    if (settings.node != undefined && !graphMetadata.nodes.labels.includes(settings.node)) {
-      updateSettings({ node: undefined });
-    }
-  }, [graphMetadata.nodes.types, data, settings]);
+    setLayerIds((prevIds) => [...prevIds, newLayerId]);
+  }, [settings.layer]);
 
   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[node.label].lat] as string) : undefined;
-        const longitude = settings.lon ? (node?.attributes?.[settings[node.label].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)];
@@ -139,13 +104,13 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSe
       config: settings,
       selected: selected,
       hoverObject: hoverObject,
-      isSelecting: isSelecting,
       getNodeLocation: (d: string) => {
         return coordinateLookup[d];
       },
       flyToBoundingBox: flyToBoundingBox,
+      setLayerIds: (val: string[]) => setLayerIds(val),
     });
-  }, [layer, data, selected, hoverObject, isSelecting, settings]);
+  }, [layer, data, selected, hoverObject, settings]);
 
   const selectionLayer = useMemo(
     () =>
@@ -153,11 +118,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],
@@ -165,6 +148,22 @@ 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, selectionLayer]}
         controller={true}
@@ -198,6 +197,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>
   );
 };
diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts b/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts
index 241feb6fe..8156ce3b5 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts
+++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.types.ts
@@ -10,8 +10,7 @@ export interface GeoJSONData {
 }
 
 export type Layer = {
-  id: number;
-  name: string;
+  id: string;
   type: any;
   config: any;
   visible: boolean;
diff --git a/libs/shared/lib/vis/visualizations/mapvis/search.tsx b/libs/shared/lib/vis/visualizations/mapvis/search.tsx
new file mode 100644
index 000000000..f781ba798
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/mapvis/search.tsx
@@ -0,0 +1,46 @@
+import { Button, Input } from '@graphpolaris/shared/lib/components';
+import { useAppDispatch } from '@graphpolaris/shared/lib/data-access';
+import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice';
+import React, { useState } from 'react';
+
+interface SearchBarProps {
+  onSearch: (boundingbox: [number, number, number, number]) => void;
+}
+
+const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
+  const dispatch = useAppDispatch();
+  const [query, setQuery] = useState('');
+  const [isLoading, setIsLoading] = useState(false);
+
+  const handleSearch = async () => {
+    setIsLoading(true);
+
+    try {
+      const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json`);
+      const data = await response.json();
+      if (data.length > 0) {
+        const { boundingbox } = data[0];
+        if (boundingbox) {
+          onSearch(boundingbox.map(parseFloat));
+        }
+      } else {
+        dispatch(addError('No results found'));
+      }
+    } catch (error) {
+      dispatch(addError('Error fetching coordinates'));
+    }
+
+    setIsLoading(false);
+  };
+
+  return (
+    <div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 z-50 m-1 p-2 bg-white shadow-md rounded w-full max-w-xl">
+      <div className="flex gap-2 items-center">
+        <Input type="text" size="xs" value={query} onChange={(value) => setQuery(value)} />
+        <Button label="Search" size="xs" onClick={handleSearch} disabled={isLoading} />
+      </div>
+    </div>
+  );
+};
+
+export default SearchBar;
-- 
GitLab