From 8e7e5a5a53cc0c54357f965573f895b2eb2458c1 Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@suma-ev.de>
Date: Thu, 8 Feb 2024 16:58:58 +0100
Subject: [PATCH] fix routefinder

---
 app/package-lock.json                       |  11 +-
 app/package.json                            |   3 +-
 app/resources/js/NavigationModule.js        |   2 +
 app/resources/js/Result.js                  |  36 +--
 app/resources/js/Route.js                   |  62 ++---
 app/resources/js/RouteFinder.js             | 274 ++++++++++----------
 app/resources/js/SearchModule.js            |   4 +-
 app/resources/js/app.js                     |  15 +-
 app/resources/js/maps/Maplibre.js           |  31 ++-
 app/resources/js/maps/MetaGerMap.js         |  18 ++
 app/resources/js/maps/Openlayers.js         |  33 ++-
 app/resources/js/utils.js                   |   8 +-
 app/resources/less/map.less                 |   1 +
 app/resources/less/openlayers/controls.less |   4 +
 app/yarn.lock                               |   7 +-
 15 files changed, 281 insertions(+), 228 deletions(-)

diff --git a/app/package-lock.json b/app/package-lock.json
index bf124a6..c00c68c 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -7,7 +7,8 @@
       "dependencies": {
         "@turf/turf": "^6.5.0",
         "maplibre-gl": "^4.0.0",
-        "ol": "^8.2.0"
+        "ol": "^8.2.0",
+        "ol-popup": "^5.1.0"
       },
       "devDependencies": {
         "axios": "^0.21",
@@ -8258,6 +8259,14 @@
         "url": "https://opencollective.com/openlayers"
       }
     },
+    "node_modules/ol-popup": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/ol-popup/-/ol-popup-5.1.0.tgz",
+      "integrity": "sha512-vCxtOQQUI1LZoXjwN+Ic/A7BXii/WtDCmdvRhyaEMJSNpbk2k77yNkIvucTEG3+0gS1zKgYCBMaXOKbKBXpY4A==",
+      "peerDependencies": {
+        "ol": ">=5.0.0"
+      }
+    },
     "node_modules/on-finished": {
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
diff --git a/app/package.json b/app/package.json
index d928b9a..19130e9 100644
--- a/app/package.json
+++ b/app/package.json
@@ -23,6 +23,7 @@
   "dependencies": {
     "@turf/turf": "^6.5.0",
     "maplibre-gl": "^4.0.0",
-    "ol": "^8.2.0"
+    "ol": "^8.2.0",
+    "ol-popup": "^5.1.0"
   }
 }
diff --git a/app/resources/js/NavigationModule.js b/app/resources/js/NavigationModule.js
index 2f23a97..e863cd0 100644
--- a/app/resources/js/NavigationModule.js
+++ b/app/resources/js/NavigationModule.js
@@ -743,3 +743,5 @@ class NavigationModule {
     map.routeModule.updateInterface();
   }
 }
+
+export const navigationModule = new NavigationModule();
\ No newline at end of file
diff --git a/app/resources/js/Result.js b/app/resources/js/Result.js
index c09d569..a83214a 100644
--- a/app/resources/js/Result.js
+++ b/app/resources/js/Result.js
@@ -2,6 +2,7 @@ import { featureCollection, point } from "@turf/turf";
 import { routeModule } from "./RouteFinder";
 import { RenderOptions } from "./maps/RenderOptions";
 import { map } from "./maps/MetaGerMapModule";
+import { gpsManager } from "./GpsManager";
 
 export class Result {
   data = null;
@@ -68,29 +69,6 @@ export class Result {
     if (this.data && callback) callback();
   }
 
-  showMarker() {
-    if (this.marker && !this.error_message) {
-      if (this.route && this.route.data) {
-        let snapped_coordinates = this.route.data.paths[this.route.selected].snapped_waypoints.coordinates[0];
-        this.marker.setLngLat(snapped_coordinates).addTo(map.map);
-      } else {
-        this.marker = this.marker
-          .setLngLat([this.data.lon, this.data.lat])
-          .addTo(map.map);
-      }
-    }
-    if (this.source == "gps" && this.data && this.data.location) {
-      map.showUserPosition(turf.point([this.data.location.coords.longitude, this.data.location.coords.latitude]), this.data.location.coords.heading, this.data.location.coords.accuracy);
-    } else if (this.source == "gps") {
-      map.hideUserPosition();
-    }
-  }
-
-  removeMarker() {
-    if (this.marker) this.marker = this.marker.remove();
-    if (this.source == "gps" && this.data && this.data.location) map.hideUserPosition();
-  }
-
   toString() {
     switch (this.source) {
       case "gps":
@@ -102,11 +80,6 @@ export class Result {
     }
   }
 
-  updateRank(rank) {
-    this.rank = rank;
-    this.#createDomElement();
-  }
-
   async #getFromGPS(callback) {
     let maximumAge = 500;
     let lastUpdate = Date.now() - maximumAge;
@@ -169,7 +142,7 @@ export class Result {
 
     let error_callback = (error) => {
       console.log(error);
-      map.gpsManager.clearWatch(this.geolocation_watch_id);
+      gpsManager.clearWatch(this.geolocation_watch_id);
       this.geolocation_watch_id = null;
       if (this.data == null) {
         switch (error.code) {
@@ -197,12 +170,12 @@ export class Result {
       maximumAge: 60000,
     };
 
-    map.gpsManager.getCurrentPosition(position => {
+    gpsManager.getCurrentPosition(position => {
       position_callback(position);
       // Try to upgrade to a high accuracy position
       options.enableHighAccuracy = true;
       options.maximumAge = 500;
-      this.geolocation_watch_id = map.gpsManager.watchPosition(position_callback.bind(this), error_callback, options);
+      this.geolocation_watch_id = gpsManager.watchPosition(position_callback.bind(this), error_callback, options);
     }, error_callback, options);
   }
 
@@ -321,6 +294,7 @@ export class Result {
       el.style.filter = `hue-rotate(${huerotate}deg)`;
     }
 
+    this.dom_element.querySelector(".result-marker").innerHTML = "";
     this.dom_element.querySelector(".result-marker").appendChild(el);
 
     return el;
diff --git a/app/resources/js/Route.js b/app/resources/js/Route.js
index 667febc..d9f6881 100644
--- a/app/resources/js/Route.js
+++ b/app/resources/js/Route.js
@@ -1,4 +1,8 @@
-class Route {
+import { LngLat } from "maplibre-gl";
+import { map } from "./maps/MetaGerMapModule";
+import { formatDistance, formatDuration } from "./utils";
+
+export class Route {
   data = null;
   profile = "guess";
   selected = null;
@@ -169,11 +173,11 @@ class Route {
       !["car", "bike", "foot"].includes(this.profile)
     ) {
       // Guess a default profile based on the distance between both points
-      let from_point = new maplibregl.LngLat(
+      let from_point = new LngLat(
         waypoint_from.data.lon,
         waypoint_from.data.lat
       );
-      let to_point = new maplibregl.LngLat(
+      let to_point = new LngLat(
         waypoint_to.data.lon,
         waypoint_to.data.lat
       );
@@ -353,11 +357,8 @@ class Route {
     if (window.screen.width < 800) {
       paddingInline = 50;
     }
-    map.map.fitBounds(
-      [
-        [bbox[0], bbox[1]],
-        [bbox[2], bbox[3]],
-      ],
+    map.fitBounds(
+      bbox,
       {
         padding: {
           top: paddingInline,
@@ -390,36 +391,37 @@ class Route {
       } else {
         time_difference += " kürzer";
       }
-      let new_popup = new maplibregl.Popup({
-        closeButton: false,
-        closeOnClick: false,
-      })
-        .setLngLat(path.intersection_point)
-        .setHTML(
-          `<div class="alt-route-popup"><label>Alternative Route</label>${description}<div>${time_difference}</div></div>`
-        );
-
-      new_popup.path_index = index;
+      let popup_dom_element = document.createElement("div");
+      popup_dom_element.classList.add("alt-route-popup");
+      let popup_label = document.createElement("label");
+      popup_label.textContent = "Alternative Route";
+      popup_dom_element.appendChild(popup_label);
+      let popup_description = document.createElement("label");
+      popup_description.textContent = description;
+      popup_dom_element.appendChild(popup_description);
+      let popup_time = document.createElement("div");
+      popup_time.textContent = time_difference;
+      popup_dom_element.appendChild(popup_time);
+      let popup = map.createPopup(popup_dom_element, path.intersection_point);
+
+      popup_dom_element.addEventListener("click", e => {
+        this.selected = index;
+        this.#createDomNode();
+        this.#createPopups();
+        this.dom_node.dispatchEvent(new Event("change"));
+      });
 
-      this.popups.push(new_popup);
+      this.popups.push(popup);
     }
   }
 
   showPopups() {
-    for (const [index, popup] of this.popups.entries()) {
-      this.popups[index] = popup.addTo(map.map);
-      this.popups[index].getElement().onclick = () => {
-        this.selected = popup.path_index;
-        this.#createDomNode();
-        this.#createPopups();
-        this.dom_node.dispatchEvent(new Event("change"));
-      };
-    }
+    this.#createPopups();
   }
 
   hidePopups() {
     for (const [index, popup] of this.popups.entries()) {
-      this.popups[index] = popup.remove();
+      map.removePopup(popup);
     }
   }
 
@@ -465,4 +467,4 @@ class Route {
       path.intersection_point = target_point.geometry.coordinates;
     }
   }
-}
+}
\ No newline at end of file
diff --git a/app/resources/js/RouteFinder.js b/app/resources/js/RouteFinder.js
index 926e031..180faa8 100644
--- a/app/resources/js/RouteFinder.js
+++ b/app/resources/js/RouteFinder.js
@@ -1,3 +1,14 @@
+import { feature, featureCollection, lineString, point } from "@turf/turf";
+import { gpsManager } from "./GpsManager";
+import { navigationModule } from "./NavigationModule";
+import { Result } from "./Result";
+import { switchModule } from "./app";
+import { map } from "./maps/MetaGerMapModule";
+import { Route } from "./Route";
+import { LngLatBounds } from "maplibre-gl";
+import { formatDistance, formatDuration } from "./utils";
+import { RenderOptions } from "./maps/RenderOptions";
+
 class RouteFinder {
   htmlmodule = document.getElementById("route-finder-addon");
   searchinput = this.htmlmodule.querySelector("input#search-waypoint-input");
@@ -12,25 +23,23 @@ class RouteFinder {
   waypoints = [];
   routes = [];
 
-  mapsourcename = "routefinder"; // Source where all Features of the Routefinder are displayed in
-  linesourcename = `${this.mapsourcename}-line`;
-  fillsourcename = `${this.mapsourcename}-fill`;
-  inactivesourcename = `${this.mapsourcename}-inactive-line`;
+  map_layer_id = "routefinder"; // Source where all Features of the Routefinder are displayed in
+  map_layer_id_route_active = `${this.map_layer_id}_active`;
+  map_layer_id_route_inactive = `${this.map_layer_id}_inactive`;
+
   markers = [];
 
   constructor() {
-    this.addMapLayers();
-
     this.searchinput.onkeyup = (e) => {
       let query = this.htmlmodule.querySelector(
         "input#search-waypoint-input"
       ).value;
-      map.switchModule("search", {
+      switchModule("search", {
         query: query,
         search: true,
         stateupdates: false,
         exit_callback: () => {
-          map.switchModule("route-finding");
+          switchModule("route-finding");
         },
       });
     };
@@ -60,7 +69,7 @@ class RouteFinder {
       while (this.waypoints.length > 0) {
         this.removeWaypoint(0);
       }
-      map.switchModule("search");
+      switchModule("search");
     };
   }
 
@@ -74,11 +83,11 @@ class RouteFinder {
     this.htmlmodule.classList.add("active");
     this.searchinput.value = "";
 
-    map.map.off("moveend", this.moveend_callback);
-    map.map.on("moveend", this.moveend_callback);
+    map.off("moveend", this.moveend_callback);
+    map.on("moveend", this.moveend_callback);
 
-    map.map.off("click", this.click_callback);
-    map.map.on("click", this.click_callback);
+    map.off("click", this.click_callback);
+    map.on("click", this.click_callback);
 
     if (vehicle) this.vehicle = vehicle;
     this.navigation_active = navigation_active;
@@ -90,7 +99,7 @@ class RouteFinder {
       }
     }
     // Add GPS options
-    await map.gpsManager.isGPSAvailable().then((gps_available) => {
+    await gpsManager.isGPSAvailable().then((gps_available) => {
       this.htmlmodule.classList.remove("navigation-enabled");
       if (
         gps_available &&
@@ -140,12 +149,12 @@ class RouteFinder {
     }
 
     if (mapposition) {
-      mapposition = new maplibregl.LngLatBounds(
+      mapposition = new LngLatBounds(
         mapposition._sw,
         mapposition._ne
       );
-      let pos = map.map.cameraForBounds(mapposition);
-      map.map.easeTo(
+      let pos = map.cameraForBounds(mapposition);
+      map.easeTo(
         {
           center: pos.center,
           zoom: pos.zoom,
@@ -159,10 +168,9 @@ class RouteFinder {
 
   removeWaypoint(index) {
     if (this.waypoints.length < index - 1) return;
-    this.waypoints[index].removeMarker();
 
     if (this.waypoints[index].geolocation_watch_id) {
-      map.gpsManager.clearWatch(this.waypoints[index].geolocation_watch_id);
+      gpsManager.clearWatch(this.waypoints[index].geolocation_watch_id);
     }
     if (this.waypoints[index].route) this.waypoints[index].route.hidePopups();
     if (index > 0) {
@@ -170,7 +178,6 @@ class RouteFinder {
       this.waypoints[index - 1].route = null;
     }
     this.waypoints.splice(index, 1);
-    this.updateWaypointRanks();
   }
 
   addWaypoint(waypoint, first = false) {
@@ -180,10 +187,9 @@ class RouteFinder {
     } else {
       this.waypoints.push(waypoint);
     }
-    this.updateWaypointRanks();
     this.updateInterface();
     // Register Event handler for gps updates
-    if (waypoint.rank == 0 && waypoint.source == "gps") {
+    if (first && waypoint.source == "gps") {
       waypoint.dom_element.addEventListener("update", this.gpsUpdate.bind(this));
       waypoint.dom_element.addEventListener("navigation_available", e => {
         this.htmlmodule.classList.add("navigation-enabled");
@@ -200,15 +206,6 @@ class RouteFinder {
     // Check if the route needs to be recalculated
   }
 
-  // Go through all waypoints and validate that the stored rank is correct
-  updateWaypointRanks() {
-    for (const [index, waypoint] of this.waypoints.entries()) {
-      if (waypoint.rank != index) {
-        waypoint.updateRank(index);
-      }
-    }
-  }
-
   async calculateRoutes() {
     let routes_calculated = 0;
     for (const [index, waypoint] of this.waypoints.entries()) {
@@ -324,7 +321,6 @@ class RouteFinder {
     for (const [index, waypoint] of this.waypoints.entries()) {
       if (waypoint.error_code == Result.ERROR_GEOLOCATION_PERMISSION_DENIED || waypoint.error_code == Result.ERROR_GEOLOCATION_TIMEOUT) {
         // User denied Geolocation Access. Remove GPS waypoint
-        waypoint.removeMarker();
         this.waypoints.splice(index, 1);
         this.updateInterface();
         this.updateState();
@@ -337,7 +333,6 @@ class RouteFinder {
           e
         ) => {
           this.removeWaypoint(index);
-          this.updateWaypointRanks();
           this.calculateRoutes()
             .then(() => this.updateInterface())
             .then(() => this.fitRoutesOnMap())
@@ -362,7 +357,6 @@ class RouteFinder {
                   this.waypoints[index],
                 ];
               }
-              this.updateWaypointRanks();
               this.calculateRoutes()
                 .then(() => this.updateInterface())
                 .then(() => this.fitRoutesOnMap())
@@ -390,70 +384,132 @@ class RouteFinder {
    */
   updateMap() {
     // Add the markers to the map
-    for (const [index, waypoint] of this.waypoints.entries()) {
-      if (!this.navigation_active || index > 0) waypoint.showMarker();
-      waypoint.showGeoJson(this.linesourcename, this.fillsourcename);
+    let geojson_features = [];
+    let geojson_route_active_features = [];
+    let geojson_route_inactive_features = [];
 
-      if (waypoint.route) {
-        waypoint.route.showPopups();
-        waypoint.route.showGeoJson(
-          this.linesourcename,
-          this.inactivesourcename
+    for (const [index, waypoint] of this.waypoints.entries()) {
+      if (!waypoint.data) continue;
+      let show_marker = !this.navigation_active || index > 0;
+      let chr = String.fromCharCode(65 + index);
+      let new_feature = waypoint.data.geojson ? feature(waypoint.data.geojson, { text: chr }) : null;
+
+      if (new_feature == null || !["Point", "MultiPoint"].includes(new_feature.geometry.type)) {
+        // Add A marker
+        let new_point = point(
+          [parseFloat(waypoint.data.lon), parseFloat(waypoint.data.lat)],
+          { text: chr }
         );
+        if (new_feature != null)
+          geojson_features.push(new_feature);
+        if (show_marker)
+          geojson_features.push(new_point);
+      } else if (show_marker && new_feature != null) {
+        geojson_features.push(new_feature);
+      }
+      waypoint.addMarkerToDom(index);
+      if (waypoint.route && waypoint.route.data) {
+        for (const [index, path] of waypoint.route.data.paths.entries()) {
+          let feature = lineString(path.points.coordinates);
+          if (waypoint.route.selected == index) {
+            geojson_route_active_features.push(feature);
+          } else {
+            geojson_route_inactive_features.push(feature);
+          }
+        }
       }
     }
+    // Draw the waypoints
+    map.drawFeatures(this.map_layer_id, featureCollection(geojson_features), new RenderOptions(
+      {
+        point: {
+          "text-translate": [0, -19],
+          "text-color": "#ffffff",
+        },
+        line: {
+          "line-color": "rgb(255,127,0)",
+          "line-width": 5,
+        },
+        fill: {
+          "fill-color": "#593008",
+          "fill-outline-color": "rgb(255,127,0)",
+          "fill-opacity": 0.3,
+        },
+      },
+      {
+        point: {
+          "icon-image": "result-marker",
+          "icon-size": 1,
+          "icon-anchor": "bottom",
+          "icon-ignore-placement": true,
+          "text-field": ["get", "text"],
+          "text-line-height": 1,
+          "text-padding": 0,
+          "text-anchor": "bottom",
+          "text-justify": "center",
+          "text-ignore-placement": true,
+        },
+      })
+    );
+    // Draw Inactive Routes
+    map.drawFeatures(this.map_layer_id_route_inactive, featureCollection(geojson_route_inactive_features), new RenderOptions({
+      line: {
+        "line-color": "rgb(177, 180, 185)",
+        "line-width": 5,
+        "line-opacity": 0.6,
+      },
+    }, {}));
+    map.drawFeatures(this.map_layer_id_route_active, featureCollection(geojson_route_active_features), new RenderOptions({
+      line: {
+        "line-color": "#2e8bc0",
+        "line-width": 5,
+        "line-opacity": 1,
+      },
+    }, {}));
   }
-  clearMap() {
-    for (const waypoint of this.waypoints) {
-      waypoint.removeMarker();
-      if (waypoint.route) waypoint.route.hidePopups();
-    }
-    let line_data = map.map.getSource(this.linesourcename)._data;
-    line_data.features = [];
-    map.map.getSource(this.linesourcename).setData(line_data);
 
-    let inactive_line_data = map.map.getSource(this.inactivesourcename)._data;
-    inactive_line_data.features = [];
-    map.map.getSource(this.inactivesourcename).setData(inactive_line_data);
-
-    let fill_data = map.map.getSource(this.fillsourcename)._data;
-    fill_data.features = [];
-    map.map.getSource(this.fillsourcename).setData(fill_data);
+  clearMap() {
+    map.removeFeatures(this.map_layer_id);
+    map.removeFeatures(this.map_layer_id_route_active);
+    map.removeFeatures(this.map_layer_id_route_inactive);
   }
 
   startNavigation() {
     if (this.waypoints[0].source != "gps" || !this.waypoints[0].route) return;
-    map.gpsManager.clearWatch(this.waypoints[0].geolocation_watch_id);
+    gpsManager.clearWatch(this.waypoints[0].geolocation_watch_id);
     this.waypoints[0].geolocation_watch_id = null;
 
     this.navigation_active = true;
     this.htmlmodule.classList.remove("active"); // Route Overview is not shown while navigating
-    map.map.off("click", this.click_callback); // No reverse searches whilst navigating
-    map.map.off("moveend", this.moveend_callback); // No state updates whilst navigating
+    map.off("click", this.click_callback); // No reverse searches whilst navigating
+    map.off("moveend", this.moveend_callback); // No state updates whilst navigating
     this.updateState(); // But a final state update
-    map.navigationModule.enable();
+    navigationModule.enable();
     this.updateInterface();
   }
   stopNavigation() {
     this.navigation_active = false;
     this.htmlmodule.classList.add("active"); // Route Overview is not shown while navigating
-    map.map.on("click", this.click_callback); // No reverse searches whilst navigating
-    map.map.on("moveend", this.moveend_callback); // No state updates whilst navigating
+    map.on("click", this.click_callback); // No reverse searches whilst navigating
+    map.on("moveend", this.moveend_callback); // No state updates whilst navigating
     this.updateState(); // But a final state update
   }
   handleMapClick(event) {
+    if (event.coordinate) {
+      event.lngLat = new LngLat(event.coordinate[0], event.coordinate[1]);
+    }
     let search_params = {
       reverse: {
-        lngLat: event.lngLat,
-        zoom: Math.round(map.map.getZoom()),
+        center: event.lngLat,
+        zoom: Math.round(map.getZoom()),
       },
       stateupdates: false,
       search: true,
       exit_callback: () => {
-        map.switchModule("route-finding");
+        switchModule("route-finding");
       },
     };
-    map.switchModule("search", search_params);
+    switchModule("search", search_params);
   }
 
   fitRoutesOnMap() {
@@ -468,77 +524,7 @@ class RouteFinder {
       else bbox_collection = turf.featureCollection([bbox]);
     }
     let bbox = turf.bbox(bbox_collection);
-    map.fitBbox([
-      [bbox[0], bbox[1]],
-      [bbox[2], bbox[3]],
-    ]);
-  }
-
-  addMapLayers() {
-    // Variables for map features
-    let lineSource = {
-      type: "geojson",
-      data: {
-        type: "FeatureCollection",
-        features: [],
-      },
-    };
-    let lineLayer = {
-      id: `${this.linesourcename}`,
-      type: "line",
-      source: `${this.linesourcename}`,
-      paint: {
-        "line-color": "#2e8bc0",
-        "line-width": 5,
-        "line-opacity": 1,
-      },
-    };
-
-    let inactiveLineSource = {
-      type: "geojson",
-      data: {
-        type: "FeatureCollection",
-        features: [],
-      },
-    };
-    let inactiveLineLayer = {
-      id: `${this.inactivesourcename}`,
-      type: "line",
-      source: `${this.inactivesourcename}`,
-      paint: {
-        "line-color": "rgb(177, 180, 185)",
-        "line-width": 5,
-        "line-opacity": 0.6,
-      },
-    };
-
-    let fillSource = {
-      type: "geojson",
-      data: {
-        type: "FeatureCollection",
-        features: [],
-      },
-    };
-    let fillLayer = {
-      id: `${this.fillsourcename}`,
-      type: "fill",
-      source: `${this.fillsourcename}`,
-      paint: {
-        "fill-color": "rgb(255,127,0)",
-        "fill-outline-color": "rgb(255,127,0)",
-        "fill-opacity": 0.3,
-      },
-    };
-
-    let addLayers = () => {
-      map.map.addSource(this.inactivesourcename, inactiveLineSource);
-      map.map.addLayer(inactiveLineLayer);
-      map.map.addSource(this.linesourcename, lineSource);
-      map.map.addLayer(lineLayer);
-
-      map.map.addSource(this.fillsourcename, fillSource);
-      map.map.addLayer(fillLayer);
-    };
+    map.fitBounds(new LngLatBounds(bbox));
   }
 
   clearInterface() {
@@ -558,8 +544,8 @@ class RouteFinder {
 
     this.clearInterface();
 
-    map.map.off("moveend", this.moveend_callback);
-    map.map.off("click", this.click_callback);
+    map.off("moveend", this.moveend_callback);
+    map.off("click", this.click_callback);
   }
 
   updateState(event) {
@@ -580,7 +566,7 @@ class RouteFinder {
     }
     url += waypoint_strings.join(";");
 
-    let current_pos = map.map.getBounds();
+    let current_pos = map.getBounds();
 
     // Check if current_pos changed enough
     if (
@@ -589,12 +575,12 @@ class RouteFinder {
       window.history.state.module == "route-finder" &&
       window.history.state.navigation_active == this.navigation_active
     ) {
-      let old_pos = new maplibregl.LngLatBounds(
+      let old_pos = new LngLatBounds(
         window.history.state.mapposition._sw,
         window.history.state.mapposition._ne
       );
-      let old_pos_camera = map.map.cameraForBounds(old_pos);
-      let new_pos_camera = map.map.cameraForBounds(current_pos);
+      let old_pos_camera = map.cameraForBounds(old_pos);
+      let new_pos_camera = map.cameraForBounds(current_pos);
       if (
         old_pos_camera.center.distanceTo(new_pos_camera.center) <= 250 &&
         Math.abs(old_pos_camera.zoom - new_pos_camera.zoom) < 0.5
diff --git a/app/resources/js/SearchModule.js b/app/resources/js/SearchModule.js
index af18ef6..67d8d04 100644
--- a/app/resources/js/SearchModule.js
+++ b/app/resources/js/SearchModule.js
@@ -3,6 +3,7 @@ import { map } from "./maps/MetaGerMapModule";
 import { Result } from "./Result";
 import { RenderOptions } from "./maps/RenderOptions";
 import { feature, featureCollection, point } from "@turf/turf";
+import { switchModule } from "./app";
 
 class SearchModule {
   htmlmodule = document.getElementById("search-addon");
@@ -385,7 +386,6 @@ class SearchModule {
       this.searchresultscontainer.appendChild(label);
     }
     for (const [index, result] of this.results.entries()) {
-      result.updateRank(index);
       this.searchresultscontainer.appendChild(result.dom_element);
       // Add Click listener
       result.dom_element.onclick = (e) => {
@@ -418,7 +418,7 @@ class SearchModule {
         this.updateMap();
         let result_data = result.data;
         result_data.rank = 0;
-        map.switchModule("route-finding", {
+        switchModule("route-finding", {
           waypoints: [new Result(result.data, false, result.source)],
         });
       };
diff --git a/app/resources/js/app.js b/app/resources/js/app.js
index 64004d4..60e48ce 100644
--- a/app/resources/js/app.js
+++ b/app/resources/js/app.js
@@ -4,6 +4,8 @@ import { gpsManager } from "./GpsManager";
 import { searchModule } from "./SearchModule";
 import { LngLatBounds } from "maplibre-gl";
 import { fakeGpsModule } from "./FakeGPSModule";
+import { routeModule } from "./RouteFinder";
+import { navigationModule } from "./NavigationModule";
 
 let module = null; // CUrrent module
 
@@ -218,9 +220,9 @@ export function switchModule(name, args) {
       }
       break;
     case "route-finding":
-      this.module = this.routeModule;
+      module = routeModule;
       if (typeof args == "object") {
-        this.module.enable(
+        module.enable(
           args.vehicle,
           args.waypoints,
           args.mapposition,
@@ -228,15 +230,12 @@ export function switchModule(name, args) {
           args.navigation_active
         );
       } else {
-        this.module.enable();
+        module.enable();
       }
       break;
-    case "offline-karten":
-      this.module = new OfflineModule(this);
-      break;
     case "navigation":
-      this.module = this.navigationModule;
-      this.module.enable();
+      module = navigationModule;
+      module.enable();
       break;
     case "fakegps":
       module = fakeGpsModule;
diff --git a/app/resources/js/maps/Maplibre.js b/app/resources/js/maps/Maplibre.js
index 9ad74da..1893680 100644
--- a/app/resources/js/maps/Maplibre.js
+++ b/app/resources/js/maps/Maplibre.js
@@ -1,7 +1,7 @@
 import { config } from "../Config";
 import { GeolocationControl } from "../maplibre/GeolocationControl";
 import { MetaGerMap } from "./MetaGerMap";
-import { AttributionControl, LngLat, Map, NavigationControl, ScaleControl, setRTLTextPlugin } from "maplibre-gl";
+import { AttributionControl, LngLat, Map, NavigationControl, Popup, ScaleControl, setRTLTextPlugin } from "maplibre-gl";
 import { RenderOptions } from "./RenderOptions";
 import { circle, featureCollection, point } from "@turf/turf";
 import { NavbarControl } from "../maplibre/NavbarControl";
@@ -196,6 +196,10 @@ export class Maplibre extends MetaGerMap {
       }
     }
 
+    if (layer_id == "routefinder_active") {
+      console.log(lineStringLayer, source);
+    }
+
     // Points will be drawn as Markers
     // The feature can have a property `text` to change the label on the marker
     let pointLayer = this._map.getLayer(`${layer_id}_point`);
@@ -319,4 +323,29 @@ export class Maplibre extends MetaGerMap {
     let layer_id = "userposition";
     this.removeFeatures(layer_id);
   }
+
+  /**
+ * 
+ * @param {HTMLDivElement} dom_element 
+ * @param {LngLat} position 
+ */
+  createPopup(dom_element, position) {
+    let new_popup = new Popup({
+      closeButton: false,
+      closeOnClick: false,
+    });
+    new_popup.setDOMContent(dom_element);
+    new_popup.setLngLat(position);
+    new_popup.addTo(this._map);
+    return new_popup;
+  }
+
+  /**
+   * Removes a popup previously created by createPopup
+   * the popup argument is the result of the createPopup Method
+   * @param {Popup} popup 
+   */
+  removePopup(popup) {
+    popup.remove();
+  }
 }
diff --git a/app/resources/js/maps/MetaGerMap.js b/app/resources/js/maps/MetaGerMap.js
index f31420f..9aa5706 100644
--- a/app/resources/js/maps/MetaGerMap.js
+++ b/app/resources/js/maps/MetaGerMap.js
@@ -170,4 +170,22 @@ export class MetaGerMap {
   hideUserPosition() {
     throw new Error("Abstract function showUserPosition must be implemented");
   }
+
+  /**
+   * 
+   * @param {HTMLDivElement} dom_element 
+   * @param {LngLat} position 
+   */
+  createPopup(dom_element, position) {
+    throw new Error("Abstract function createPopup must be implemented")
+  }
+
+  /**
+   * Removes a popup previously created by createPopup
+   * the popup argument is the result of the createPopup Method
+   * @param {*} popup 
+   */
+  removePopup(popup) {
+    throw new Error("Abstract function removePopup must be implemented")
+  }
 }
diff --git a/app/resources/js/maps/Openlayers.js b/app/resources/js/maps/Openlayers.js
index 788342b..db2a102 100644
--- a/app/resources/js/maps/Openlayers.js
+++ b/app/resources/js/maps/Openlayers.js
@@ -10,9 +10,8 @@ import {
   featureCollection,
   point,
 } from "@turf/turf";
-import * as Polygon from "ol/geom/Polygon";
 import XYZ from "ol/source/XYZ";
-import { Projection, useGeographic } from "ol/proj";
+import { fromLonLat, useGeographic } from "ol/proj";
 import VectorLayer from "ol/layer/Vector";
 import { LngLat, LngLatBounds } from "maplibre-gl";
 import * as Color from "ol/color";
@@ -25,7 +24,6 @@ import Stroke from "ol/style/Stroke";
 import Icon from "ol/style/Icon";
 import Text from "ol/style/Text";
 import Zoom from "ol/control/Zoom";
-import Rotate from "ol/control/Rotate";
 import Attribution from "ol/control/Attribution";
 import { NavigationControl } from "../openlayers/NavigationControl";
 import ScaleLine from "ol/control/ScaleLine";
@@ -34,9 +32,8 @@ import DragPan from "ol/interaction/DragPan";
 import MouseWheelZoom from "ol/interaction/MouseWheelZoom";
 import KeyboardPan from "ol/interaction/KeyboardPan";
 import KeyboardZoom from "ol/interaction/KeyboardZoom";
-import dragRotate from "ol/interaction/DragRotate";
 import DragRotate from "ol/interaction/DragRotate";
-import Source from "ol/source/Source";
+import Popup from "ol-popup";
 
 export class Openlayers extends MetaGerMap {
   /** @type {Map} */
@@ -514,4 +511,30 @@ export class Openlayers extends MetaGerMap {
   hideUserPosition() {
     this.removeFeatures("userposition");
   }
+
+
+  /**
+ * 
+ * @param {HTMLDivElement} dom_element 
+ * @param {LngLat} position 
+ */
+  createPopup(dom_element, position) {
+    dom_element.classList.add("ol-popup");
+
+    let overlay = new Popup();
+    this._map.addOverlay(overlay);
+    overlay.show(fromLonLat([position.lng, position.lat], "EPSG:3857"), dom_element);
+
+
+    return overlay;
+  }
+
+  /**
+   * Removes a popup previously created by createPopup
+   * the popup argument is the result of the createPopup Method
+   * @param {Popup} popup 
+   */
+  removePopup(popup) {
+    this._map.removeOverlay(popup);
+  }
 }
diff --git a/app/resources/js/utils.js b/app/resources/js/utils.js
index d98c3b7..d0c1f7c 100644
--- a/app/resources/js/utils.js
+++ b/app/resources/js/utils.js
@@ -6,7 +6,7 @@
  * @param {int} duration_ms
  * @returns
  */
-function formatDuration(duration_ms) {
+export function formatDuration(duration_ms) {
   let negative = duration_ms < 0 ? true : false;
   duration_ms = Math.abs(duration_ms);
   let duration_minutes = Math.ceil(duration_ms / 1000 / 60);
@@ -27,7 +27,7 @@ function formatDuration(duration_ms) {
  * @param {int} distance_meters
  * @returns
  */
-function formatDistance(distance_meters) {
+export function formatDistance(distance_meters) {
   let distance_kilometers = Math.floor(distance_meters / 1000);
   distance_meters -= distance_kilometers * 1000;
 
@@ -44,7 +44,7 @@ function formatDistance(distance_meters) {
   }
 }
 
-function getBboxFromPoints(points) {
+export function getBboxFromPoints(points) {
   let bbox = [
     [null, null],
     [null, null],
@@ -65,7 +65,7 @@ function getBboxFromPoints(points) {
  * @param {*} bearing_a
  * @param {*} bearing_b
  */
-function compareBearings(bearing_a, bearing_b) {
+export function compareBearings(bearing_a, bearing_b) {
   // Convert all Bearings to radians
   bearing_a = turf.degreesToRadians(turf.bearingToAzimuth(bearing_a));
   bearing_b = turf.degreesToRadians(turf.bearingToAzimuth(bearing_b));
diff --git a/app/resources/less/map.less b/app/resources/less/map.less
index 89c27f8..47a9f35 100644
--- a/app/resources/less/map.less
+++ b/app/resources/less/map.less
@@ -1,3 +1,4 @@
 @import "../../node_modules/maplibre-gl/dist/maplibre-gl.css";
 @import "../../node_modules/ol/ol.css";
+@import "../../node_modules/ol-popup/src/ol-popup.css";
 @import "./openlayers/controls.less";
\ No newline at end of file
diff --git a/app/resources/less/openlayers/controls.less b/app/resources/less/openlayers/controls.less
index d0d6649..088e554 100644
--- a/app/resources/less/openlayers/controls.less
+++ b/app/resources/less/openlayers/controls.less
@@ -149,4 +149,8 @@
     &.control-navbar {
         font-size: 3rem;
     }
+}
+
+.ol-popup {
+    position: absolute;
 }
\ No newline at end of file
diff --git a/app/yarn.lock b/app/yarn.lock
index c1fac38..ec0949f 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -5393,7 +5393,12 @@ obuf@^1.0.0, obuf@^1.1.2:
   resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz"
   integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
 
-ol@^8.2.0:
+ol-popup@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.npmjs.org/ol-popup/-/ol-popup-5.1.0.tgz"
+  integrity sha512-vCxtOQQUI1LZoXjwN+Ic/A7BXii/WtDCmdvRhyaEMJSNpbk2k77yNkIvucTEG3+0gS1zKgYCBMaXOKbKBXpY4A==
+
+ol@^8.2.0, ol@>=5.0.0:
   version "8.2.0"
   resolved "https://registry.npmjs.org/ol/-/ol-8.2.0.tgz"
   integrity sha512-/m1ddd7Jsp4Kbg+l7+ozR5aKHAZNQOBAoNZ5pM9Jvh4Etkf0WGkXr9qXd7PnhmwiC1Hnc2Toz9XjCzBBvexfXw==
-- 
GitLab