From 1042c6f2409cb7d04ee805aedaa35001c8f86ffb Mon Sep 17 00:00:00 2001 From: Martin Asprusten Date: Thu, 25 Dec 2025 02:56:24 +0100 Subject: [PATCH] More robust functions in table elements --- src/main.ts | 90 ++++-- src/main.ts.save | 796 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 860 insertions(+), 26 deletions(-) create mode 100644 src/main.ts.save diff --git a/src/main.ts b/src/main.ts index 1d5ff8a..cdc4a86 100644 --- a/src/main.ts +++ b/src/main.ts @@ -388,41 +388,79 @@ function populateDistancesTable() { } distanceInput.value = distance.toFixed(3); - distanceInput.addEventListener('focus', _ => { + distanceInput.addEventListener('focus', e => { + let distanceInput = e.target; + if (!(distanceInput instanceof HTMLInputElement)) { + return; + } + if (distanceInput.classList.contains('unchanged')) { distanceInput.classList.remove('unchanged'); distanceInput.value = ''; } }); - distanceInput.addEventListener('input', _ => { - var value = distanceInput.value; - var valueFloat = parseFloat(value); - if (value != '' && isNaN(valueFloat)) { - if (!distanceInput.classList.contains('wrong')) { - distanceInput.classList.add('wrong'); + function createInputFunction(firstLatLng: LatLng | undefined, secondLatLng: LatLng | undefined) { + return function (event: Event) { + let distanceInput = event.target; + if (!(distanceInput instanceof HTMLInputElement)) { + return; } - } else { - if (distanceInput.classList.contains('wrong')) { - distanceInput.classList.remove('wrong'); - } - if (!lockedDistance) { - lockedDistance = [firstLatLng!, secondLatLng!, 0.0]; - state.distances.push(lockedDistance); - } - lockedDistance[2] = valueFloat; - } - }); - distanceInput.addEventListener('blur', _ => { - if (distanceInput.value == '') { - distanceInput.value = distance.toFixed(3); - if (wasUnchanged) { - distanceInput.classList.add('unchanged'); - state.distances = state.distances.filter(tuple => tuple != lockedDistance); + let lockedDistance = state.distances.find( + d => (d[0] == firstLatLng && d[1] == secondLatLng) + || (d[0] == secondLatLng && d[1] == firstLatLng) + ); + + + var value = distanceInput.value; + var valueFloat = parseFloat(value); + if (value != '' && isNaN(valueFloat)) { + if (!distanceInput.classList.contains('wrong')) { + distanceInput.classList.add('wrong'); + } + } else { + if (distanceInput.classList.contains('wrong')) { + distanceInput.classList.remove('wrong'); + } + if (!isNaN(valueFloat)) { + if (!lockedDistance) { + lockedDistance = [firstLatLng!, secondLatLng!, 0.0]; + state.distances.push(lockedDistance); + } + lockedDistance[2] = valueFloat; + } } - } - }); + }; + }; + + distanceInput.addEventListener('input', createInputFunction(firstLatLng, secondLatLng)); + + function createBlurFunction(distance: number, wasUnchanged: boolean, firstLatLng: LatLng | undefined, secondLatLng: LatLng | undefined) { + return function (event: Event) { + let distanceInput = event.target; + if (!(distanceInput instanceof HTMLInputElement)) { + return; + } + + let lockedDistance = state.distances.find( + d => (d[0] == firstLatLng && d[1] == secondLatLng) + || (d[0] == secondLatLng && d[1] == firstLatLng) + ); + + if (distanceInput.value == '') { + distanceInput.value = distance.toFixed(3); + if (wasUnchanged) { + distanceInput.classList.add('unchanged'); + state.distances = state.distances.filter(tuple => tuple != lockedDistance); + } else if (lockedDistance) { + lockedDistance[2] = distance; + } + } + }; + }; + + distanceInput.addEventListener('blur', createBlurFunction(distance, wasUnchanged, firstLatLng, secondLatLng)); } } } diff --git a/src/main.ts.save b/src/main.ts.save new file mode 100644 index 0000000..7958699 --- /dev/null +++ b/src/main.ts.save @@ -0,0 +1,796 @@ +import MapHandler from './map/maphandler'; +import Module, { type MainModule } from '../native/salesman.js'; +import haversine from 'haversine-distance'; +import './style.css'; +import { LatLng } from 'leaflet'; +import Openrouteservice from 'openrouteservice-js'; +import { createUrl, parseUrl } from './url/UrlHandler.js'; +import { wktToGeoJSON } from '@terraformer/wkt'; +import PPENHandler from './files/ppenhandler.js'; +import type { GeoJsonObject } from 'geojson'; + +// Outside influences +let mapHandler = new MapHandler(); +let module: MainModule = await Module(); + + + +// HTTP elements to be edited +let clearMapButton = document.getElementById('clear-map'); +let editDistanceButton = document.getElementById('edit-distances'); +let shareLinkButton = document.getElementById('share-link'); + +let calculateRouteButton = document.getElementById('calculate-route'); +let orienteeringButton = document.getElementById('orienteering'); +let uploadButton = document.getElementById('upload'); + +let distancePopup = document.getElementById('distance-popup'); +let distanceTable = document.getElementById('distance-table'); + +let retrieveButton = document.getElementById('retrieve-distances-button'); +let routeTypeForm = document.getElementById('route-type-form') as HTMLFormElement; +let retrieveRoutesButton = document.getElementById('retrieve-routes-button'); + +let middlePopup = document.getElementById('middle-popup'); +let uploadDiv = document.getElementById('upload-div'); +let orienteeringDiv = document.getElementById('orienteering-div'); + +let uploadTextArea = document.getElementById('input-text-area') as HTMLTextAreaElement; +let uploadTextButton = document.getElementById('upload-button'); + +let ppenUploader = document.getElementById('upload-ppen-input') as HTMLInputElement; +let xmlUploader = document.getElementById('upload-xml-input') as HTMLInputElement; +let downloadPpenButton = document.getElementById('download-ppen-route-button') as HTMLButtonElement; +let ppenExplanation = document.getElementById('ppen-explanation') as HTMLParagraphElement; + +let closePopupButton = document.getElementById('close-popup-button'); + +let distanceTextParagraph = document.getElementById('distance-text') as HTMLParagraphElement; + + + +// We have some asynchronous actions later depending on the user clicking something in the map. +// Set that up here +let awaitingResolutions: ((latLng: LatLng) => void)[] = []; +mapHandler.addMarkerClickedListener((latLng) => { + awaitingResolutions.forEach(awaiting => { + awaiting(latLng); + }); + awaitingResolutions = []; +}) + + + +// The overall state of our web browser window +interface State { + points: LatLng[] | null, + distances: [LatLng, LatLng, number][], + foundPath: number[] | null, + complexPath: GeoJsonObject | null, + + ppenHandler: PPENHandler | null, + ppenString: string | null, + xmlString: string | null, + + current: "Editor" | "Distance" | "SpecificDistance" | "Orienteering" | "Downloading" | "Upload", + currentDistance: [LatLng, LatLng] | null, + + distancesTablePopulated: boolean +} + +let state: State = { + points: null, + distances: [], + foundPath: [], + complexPath: null, + + ppenHandler: null, + ppenString: null, + xmlString: null, + + current: "Editor", + currentDistance: null, + + distancesTablePopulated: false +} + +// Make a kembo function that draws the interface based on the current state +function updateState() { + // Decide which buttons to show + var allowClearMap = true; + var allowEditDistances = true; + var allowShareLink = true; + var allowCalculateRoute = true; + var allowOrienteering = true; + var allowUpload = true; + + // If there are no points, don't show the clear map button + if (state.points == null || state.points.length < 1) { + allowClearMap = false; + } + + // If there are less than two points, don't show the edit distances button + if (state.points == null || state.points.length < 2) { + allowEditDistances = false; + } + // However, show it if we are in the editing distances state, so we are able to actually close + // the popup window + if (state.current == "Distance" || state.current == "SpecificDistance") { + allowEditDistances = true; + } + + // Always just allow the share link button, do nothing here + + // Only allow the calculate route button when we have more than three points, and don't already have a route + if (state.points == null || state.points.length < 3 || state.foundPath != null) { + allowCalculateRoute = false; + } + + // Always allow the orienteering and upload buttons, do nothing here + + // However, final override: If we are in the downloading orienteering map state, disallow all buttons until done + if (state.current == "Downloading") { + allowClearMap = false; + allowEditDistances = false; + allowShareLink = false; + allowCalculateRoute = false; + allowOrienteering = false; + allowUpload = false; + } + + let buttonArray: [HTMLElement | null, boolean][] = Array.of( + [clearMapButton, allowClearMap], + [editDistanceButton, allowEditDistances], + [shareLinkButton, allowShareLink], + [calculateRouteButton, allowCalculateRoute], + [orienteeringButton, allowOrienteering], + [uploadButton, allowUpload] + ); + buttonArray.forEach( + buttonData => { + var button = buttonData[0]; + var allow = buttonData[1]; + if (allow) { + button?.classList.add('clickable'); + button?.classList.remove('unclickable'); + } else { + button?.classList.remove('clickable'); + button?.classList.add('unclickable'); + } + } + ) + + // Next, update which popups are showing based on the state + if (state.current == "Editor") { + distancePopup?.style.setProperty('display', 'none'); + middlePopup?.style.setProperty('display', 'none'); + } else if (state.current == "Distance" || state.current == "SpecificDistance") { + distancePopup?.style.setProperty('display', 'block'); + middlePopup?.style.setProperty('display', 'none'); + if (!state.distancesTablePopulated) { + populateDistancesTable(); + state.distancesTablePopulated = true; + } + } else if (state.current == 'Orienteering' || state.current == 'Downloading') { + distancePopup?.style.setProperty('display', 'none'); + orienteeringDiv?.style.setProperty('display', 'block'); + uploadDiv?.style.setProperty('display', 'none'); + middlePopup?.style.setProperty('display', 'block'); + + if (state.current == 'Downloading') { + closePopupButton?.setAttribute('disabled', 'true'); + ppenUploader?.setAttribute('disabled', 'true'); + xmlUploader?.setAttribute('disabled', 'true'); + downloadPpenButton?.setAttribute('disabled', 'true'); + } else { + closePopupButton?.removeAttribute('disabled'); + ppenUploader?.removeAttribute('disabled'); + xmlUploader?.removeAttribute('disabled'); + downloadPpenButton?.removeAttribute('disabled'); + } + + if (state.foundPath == null || state.foundPath.length == 0) { + downloadPpenButton?.setAttribute('disabled', 'true'); + } + } else if (state.current == 'Upload') { + distancePopup?.style.setProperty('display', 'none'); + orienteeringDiv?.style.setProperty('display', 'none'); + uploadDiv?.style.setProperty('display', 'block'); + middlePopup?.style.setProperty('display', 'block'); + } + + // Decide whether the map should be locked or not + var locked = false; + if (state.current == 'Distance' || state.current == 'SpecificDistance') { + locked = true; + } + + if (state.ppenHandler != null) { + locked = true; + } + + if (locked) { + mapHandler.lock(); + } else { + mapHandler.unlock(); + } + + // Decide whether index popups should be shown over points + if (state.current == 'Distance' || state.current == 'SpecificDistance') { + mapHandler.showIdPopups(); + } else { + mapHandler.hideIdPopups(); + } + + // Decide which polyline to draw + let polyLine: LatLng[] | null = null; + var showingDistanceSegment = false; + var distanceText: string | null = null; + if (state.current == 'SpecificDistance' && state.currentDistance && state.currentDistance.length > 1) { + polyLine = state.currentDistance; + showingDistanceSegment = true; + } else if (state.foundPath && state.foundPath.length > 0 && state.points && state.points.length > 0) { + polyLine = state.foundPath + .filter(index => index < (state.points?.length ?? 0)) + .map(index => state.points?.at(index) ?? new LatLng(0, 0)); + + var totalDistance = 0.0; + for (var i = 1; i < state.foundPath.length; i++) { + let currentIndex = state.foundPath[i]; + let previousIndex = state.foundPath[i-1]; + + let currentLatLng = state.points[currentIndex]; + let previousLatLng = state.points[previousIndex]; + + let lockedDistance = state.distances.find(d => + (d[0].distanceTo(currentLatLng) < 2 && d[1].distanceTo(previousLatLng) < 2) + || (d[0].distanceTo(previousLatLng) < 2 && d[1].distanceTo(currentLatLng) < 2) + ); + + if (lockedDistance) { + totalDistance += lockedDistance[2]; + } else { + totalDistance += haversine(currentLatLng, previousLatLng) / 1000.0; + } + } + // Calculate distance + distanceText = "Avstand: " + totalDistance.toFixed(3) + " km"; + } + + mapHandler.clearPolyLine(); + if (polyLine) { + if (!showingDistanceSegment && state.complexPath) { + mapHandler.addPolyLineGeojson(state.complexPath); + } else { + mapHandler.addPolyLine(polyLine); + } + if (showingDistanceSegment) { + mapHandler.zoomToLatLongs(polyLine); + } + } + + if (distanceText && distanceTextParagraph) { + distanceTextParagraph.textContent = distanceText; + distanceTextParagraph.style.setProperty('display', 'block'); + } else { + distanceTextParagraph?.style.setProperty('display', 'none'); + } + + if (state.foundPath && !state.complexPath) { + retrieveRoutesButton?.style.removeProperty('disabled'); + } else { + retrieveButton?.style.setProperty('disabled', 'true'); + } +} + +function calculateRoute(): number[] { + if (!state.points) { + return []; + } + + let points: LatLng[] = state.points; + + let weights = new module.Weights(); + for (var i = 0; i < points.length; i++) { + let weightsRow = new module.WeightsRow(); + for (var j = 0; j < points.length; j++) { + var distance: number; + if (i == j) { + distance = 1e100; + } else { + // Check if this distance has been locked in + let firstLatLng = points[i]; + let secondLatLng = points[j]; + let lockedDistance = state.distances.find( + d => (d[0].distanceTo(firstLatLng) < 2 && d[1].distanceTo(secondLatLng) < 2) + || (d[0].distanceTo(secondLatLng) < 2 && d[1].distanceTo(firstLatLng) < 2) + ); + if (lockedDistance) { + distance = lockedDistance[2] * 1000.0; + } else { + distance = haversine(firstLatLng, secondLatLng); + } + } + + weightsRow.push_back(distance); + } + weights.push_back(weightsRow); + } + + let path = module.findShortestPath(weights); + var foundPath = []; + for (var i = 0; i < path.size(); i++) { + let pointIndex = path.get(i); + if (typeof pointIndex != 'undefined') { + foundPath.push(pointIndex); + } + } + return foundPath; +} + +function populateDistancesTable() { + if (distanceTable && distanceTable instanceof HTMLTableElement) { + // Delete the rows that might have already been there + while (distanceTable.rows.length > 1) { + distanceTable.deleteRow(1); + } + + // Add rows for each pair of coordinates + let coordinates = state.points; + if (!coordinates) { + coordinates = []; + } + + for (var i = 0; i < coordinates.length; i++) { + for (var j = i+1; j < coordinates.length; j++) { + let firstIndex = i+1; + let secondIndex = j+1; + + let tableRow = document.createElement('tr'); + tableRow.classList.add('distance-row'); + let pairCell = document.createElement('td'); + pairCell.appendChild(document.createTextNode(firstIndex.toString() + '-' + secondIndex.toString())); + let distanceCell = document.createElement('td'); + tableRow.appendChild(pairCell); + tableRow.appendChild(distanceCell); + distanceTable.appendChild(tableRow); + + let firstLatLng = coordinates.at(i); + let secondLatLng = coordinates.at(j); + + tableRow.addEventListener('click', () => { + if (typeof firstLatLng != 'undefined' && typeof secondLatLng != 'undefined') { + state.current = 'SpecificDistance'; + state.currentDistance = [firstLatLng, secondLatLng]; + updateState(); + } + }); + + // Add the input for distances + let distanceInput = document.createElement('input') as HTMLInputElement; + distanceCell.appendChild(distanceInput); + distanceCell.appendChild(document.createTextNode(' km')); + + let lockedDistance = state.distances.find( + d => (d[0] == firstLatLng && d[1] == secondLatLng) + || (d[0] == secondLatLng && d[1] == firstLatLng) + ); + + var distance: number; + var wasUnchanged: boolean = true; + + if (lockedDistance) { + distance = lockedDistance[2]; + wasUnchanged = false; + } else { + distance = haversine(firstLatLng!, secondLatLng!) / 1000.0; + distanceInput.classList.add('unchanged'); + } + distanceInput.value = distance.toFixed(3); + + distanceInput.addEventListener('focus', _ => { + if (distanceInput.classList.contains('unchanged')) { + distanceInput.classList.remove('unchanged'); + distanceInput.value = ''; + } + }); + + distanceInput.addEventListener('input', _ => { + var value = distanceInput.value; + var valueFloat = parseFloat(value); + if (value != '' && isNaN(valueFloat)) { + if (!distanceInput.classList.contains('wrong')) { + distanceInput.classList.add('wrong'); + } + } else { + if (distanceInput.classList.contains('wrong')) { + distanceInput.classList.remove('wrong'); + } + if (!lockedDistance) { + lockedDistance = [firstLatLng!, secondLatLng!, 0.0]; + state.distances.push(lockedDistance); + } + lockedDistance[2] = valueFloat; + } + }); + + distanceInput.addEventListener('blur', ()_ => { + if (distanceInput.value == '') { + distanceInput.value = distance.toFixed(3); + if (wasUnchanged) { + distanceInput.classList.add('unchanged'); + state.distances = state.distances.filter(tuple => tuple != lockedDistance); + } + } + }); + } + } + } +} + +retrieveButton?.addEventListener('click', async _ => { + let matrix = new Openrouteservice.Matrix({host: 'https://routeserviceproxy.martinserver.no'}); + let clickedPoints = state.points; + if (!clickedPoints) { + return; + } + + let locations = clickedPoints.map(point => [point.lng, point.lat]); + + let profile = new FormData(routeTypeForm)?.get("route-type") ?? "driving-car" + + let response = await matrix.calculate({ + locations: locations, + profile: profile, + sources: ['all'], + destinations: ['all'], + metrics: ['distance', 'duration'] + }); + + if (response && response.distances && response.distances.length == locations.length) { + state.distances = []; + for (var i = 0; i < clickedPoints.length; i++) { + for (var j = i+1; j < clickedPoints.length; j++) { + if (i == j) { + continue; + } + + let firstLatLng = clickedPoints[i]; + let secondLatLng = clickedPoints[j]; + let distance = response.distances[i][j]; + + state.distances.push([firstLatLng, secondLatLng, distance / 1000.0]); + } + } + + populateDistancesTable(); + } +}); + +uploadTextButton?.addEventListener('click', _ => { + let uploadedText = uploadTextArea?.value; + + // First, try to parse as WKT + var parsed = null; + try { + parsed = wktToGeoJSON(uploadedText); + } catch(error) { + // Do nothing + } + + // Next, try to parse input as geojson + if (!parsed) { + try { + parsed = JSON.parse(uploadedText); + } catch(error) { + // Do nothing + } + } + + // Parsed should now be a GeoJSON object. Try to read all points from it, and add them to the map + if (parsed) { + // We don't really care what type of object or anything it is. We'll just search through it for + // any child element called coordinates, and extract all coordinates and make them into points + + function readCoordinates(coordinates: any): LatLng[] { + var parsedCoordinates: LatLng[] = []; + if ((coordinates instanceof Array) && coordinates.length > 0) { + let firstChild = coordinates[0]; + if (firstChild instanceof Array) { + coordinates.forEach(child => { + let childResult = readCoordinates(child); + childResult.forEach(latlng => { + parsedCoordinates.push(latlng); + }) + }) + } else if (coordinates.length > 1) { + let lng = parseFloat(coordinates[0]); + let lat = parseFloat(coordinates[1]); + if (!isNaN(lat) && !isNaN(lng)) { + parsedCoordinates.push(new LatLng(lat, lng)); + } + } + } + return parsedCoordinates; + } + + function findCoordinates(object: Object): LatLng[] { + var coordinates: LatLng[] = []; + + if (object instanceof Array) { + object.forEach(child => { + let childCoordinates = findCoordinates(child); + childCoordinates.forEach(coordinate => coordinates.push(coordinate)); + }); + + return coordinates; + } + + + if (!(object instanceof Object)) { + return coordinates; + } + + for(const [key, value] of Object.entries(object)) { + if (key == 'coordinates') { + let currentCoordinates = readCoordinates(value); + currentCoordinates.forEach(coordinate => coordinates.push(coordinate)); + } else { + findCoordinates(value).forEach(coordinate => coordinates.push(coordinate)); + } + } + + return coordinates; + } + + let newCoordinates = findCoordinates(parsed); + if (newCoordinates.length > 0) { + mapHandler.clearAll(); + mapHandler.addPoints(newCoordinates); + state.points = newCoordinates; + state.foundPath = null; + state.complexPath = null; + updateState(); + } + } +}); + +function loadPpen(): void { + if (state.ppenString != null) { + state.ppenHandler = new PPENHandler(state.ppenString, state.xmlString); + mapHandler.clearAll() + mapHandler.addPoints(state.ppenHandler.getControls()); + state.points = state.ppenHandler.getControls(); + state.foundPath = null; + state.complexPath = null; + updateState(); + } +} + +ppenUploader?.addEventListener('change', chooseFileEvent => { + let eventTarget = chooseFileEvent.target; + if (!(eventTarget instanceof HTMLInputElement) || eventTarget.files == null || eventTarget.files.length == 0) { + return; + } + + let reader = new FileReader(); + reader.addEventListener('load', e => { + let data = e.target?.result; + if (data != null && typeof data == 'string') { + state.ppenString = data; + loadPpen(); + } + }); + reader.readAsText(eventTarget.files[0]); +}) + +xmlUploader?.addEventListener('change', chooseFileEvent => { + let eventTarget = chooseFileEvent.target; + if (!(eventTarget instanceof HTMLInputElement) || eventTarget.files == null || eventTarget.files.length == 0) { + return; + } + + let reader = new FileReader(); + reader.addEventListener('load', e => { + let data = e.target?.result; + if (data != null && typeof data == 'string') { + state.xmlString = data; + loadPpen(); + } + }); + reader.readAsText(eventTarget.files[0]); +}); + +downloadPpenButton?.addEventListener('click', async _ => { + if (state.points && state.points.length > 1 && state.foundPath != null) { + state.current == 'Downloading'; + updateState(); + + if (ppenExplanation) { + ppenExplanation.textContent = 'Trykk på posten du vil starte runden fra'; + } + await new Promise((resolve) => { + awaitingResolutions.push(resolve); + }).then(async start => { + if (ppenExplanation) { + ppenExplanation.textContent = 'Trykk på neste post, for å bestemme retningen på ruten.' + await new Promise((resolve) => { + awaitingResolutions.push(resolve); + }).then(next => { + let firstIndex = state.foundPath?.findIndex(nodeIndex => state.points?.at(nodeIndex) == start); + let nextIndex = state.foundPath?.findIndex(nodeIndex => state.points?.at(nodeIndex) == next); + let currentPath = state.foundPath; + + if (firstIndex !== null && typeof firstIndex != 'undefined' && nextIndex !== null && typeof nextIndex != 'undefined' && currentPath) { + let withNewStart = currentPath.slice(firstIndex).concat(currentPath.slice(0, firstIndex)); + + let difference = (nextIndex - firstIndex) % withNewStart.length; + if (difference < 0) { + difference += withNewStart.length; + } + + // If the difference is more than half, reverse the path + let finalPath; + if (difference > withNewStart.length / 2) { + finalPath = withNewStart.reverse(); + } else { + finalPath = withNewStart; + } + + state.ppenHandler?.createRoute(finalPath); + let newPpenFile = state.ppenHandler?.getPpenFile(); + if (newPpenFile) { + var blob = new Blob([newPpenFile], {type: "application/xml"}); + var url = window.URL.createObjectURL(blob); + var hiddenLink = document.createElement('a'); + var fileName = ppenUploader?.files?.item(0)?.name; + if (fileName) { + if (fileName.endsWith('.ppen')) { + let nameLength = fileName.length; + fileName = fileName.substring(0, nameLength - 5) + '_med_utsetting.ppen'; + } + } else { + fileName = 'newFile.ppen'; + } + hiddenLink.download = fileName; + hiddenLink.href = url; + hiddenLink.style.display = 'none'; + + document.body.appendChild(hiddenLink); + hiddenLink.click(); + document.body.removeChild(hiddenLink); + } + } + + state.current = 'Orienteering'; + updateState(); + if (ppenExplanation) { + ppenExplanation.textContent = ''; + } + }); + } + }); + } +}); + +// Load URL data and update state +let [points, distances, path] = parseUrl(window.location.href); +if (points) { + mapHandler.clearAll(); + mapHandler.addPoints(points); + state.points = points; +} + +if (distances) { + state.distances = distances; +} + +if (path) { + state.foundPath = path; +} + +updateState(); + +// Finally, add button listeners and map listeners +mapHandler.addAddedListener(_ => { + state.points = mapHandler.getClickedPoints(); + state.foundPath = null; + state.complexPath = null; + state.distancesTablePopulated = false; + updateState(); +}) + +mapHandler.addRemovedListener(_ => { + state.points = mapHandler.getClickedPoints(); + state.foundPath = null; + state.complexPath = null; + state.distancesTablePopulated = false; + updateState(); +}); + +mapHandler.addDraggedListener(_ => { + state.points = mapHandler.getClickedPoints(); + state.foundPath = null; + state.complexPath = null; + state.distancesTablePopulated = false; + updateState(); +}); + +clearMapButton?.addEventListener('click', _ => { + if (clearMapButton.classList.contains('clickable')) { + state.foundPath = null; + state.complexPath = null; + state.ppenHandler = null; + state.distancesTablePopulated = false; + state.current = 'Editor'; + mapHandler.clearAll(); + state.points = null; + updateState(); + } +}); + +editDistanceButton?.addEventListener('click', _ => { + if (editDistanceButton?.classList.contains('clickable')) { + if (state.current == 'Distance' || state.current == 'SpecificDistance') { + state.current = 'Editor'; + updateState(); + } else { + state.current = 'Distance'; + updateState(); + } + } +}); + +shareLinkButton?.addEventListener('click', async _ => { + if (shareLinkButton.classList.contains('clickable')) { + const linkPath = window.location.origin + createUrl(state.points, state.distances, state.foundPath); + await navigator.clipboard.writeText(linkPath); + } +}); + +calculateRouteButton?.addEventListener('click', _ => { + if (calculateRouteButton.classList.contains('clickable')) { + let path = calculateRoute(); + state.foundPath = path; + updateState(); + } +}); + +orienteeringButton?.addEventListener('click', _ => { + if (orienteeringButton.classList.contains('clickable')) { + state.current = 'Orienteering'; + updateState(); + } +}); + +uploadButton?.addEventListener('click', _ => { + if (uploadButton.classList.contains('clickable')) { + state.current = 'Upload'; + updateState(); + } +}); + +closePopupButton?.addEventListener('click', _ => { + state.current = 'Editor'; + updateState(); +}); + +retrieveRoutesButton?.addEventListener('click', async _ => { + if (state.foundPath && state.points && !state.complexPath) { + let directions = new Openrouteservice.Directions({host: 'https://routeserviceproxy.martinserver.no'}); + let coordinates = state.foundPath + .map(pointIndex => state.points?.at(pointIndex) ?? new LatLng(0,0)) + .map(latLng => [latLng.lng, latLng.lat]); + + let profile = new FormData(routeTypeForm)?.get("route-type") ?? "driving-car" + + let response = await directions.calculate({ + coordinates: coordinates, + profile: profile, + format: 'geojson' + }); + + state.complexPath = response; + updateState(); + } +})