More robust functions in table elements
This commit is contained in:
+43
-5
@@ -388,14 +388,31 @@ 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', _ => {
|
||||
function createInputFunction(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)
|
||||
);
|
||||
|
||||
|
||||
var value = distanceInput.value;
|
||||
var valueFloat = parseFloat(value);
|
||||
if (value != '' && isNaN(valueFloat)) {
|
||||
@@ -406,23 +423,44 @@ function populateDistancesTable() {
|
||||
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)
|
||||
);
|
||||
|
||||
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);
|
||||
} else if (lockedDistance) {
|
||||
lockedDistance[2] = distance;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
distanceInput.addEventListener('blur', createBlurFunction(distance, wasUnchanged, firstLatLng, secondLatLng));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user