Initial commit of a working website
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// Messages passed to and from the Web Worker
|
||||
export interface Message {
|
||||
loadData?: LoadData,
|
||||
dataLoaded?: DataLoaded
|
||||
findClosestNode?: FindClosestNode,
|
||||
foundClosestNode?: FoundClosestNode,
|
||||
findPathsFromNode?: FindPathsFromNode,
|
||||
foundPathsFromNode?: FoundPathsFromNode,
|
||||
getFullPath?: GetFullPath,
|
||||
returnFullPath?: ReturnFullPath,
|
||||
excludeAreas?: ExcludeAreas,
|
||||
searchArea?: SearchArea,
|
||||
continueSearch?: ContinueSearch,
|
||||
searchAreaResult?: SearchAreaResult,
|
||||
errorMessage?: ErrorMessage
|
||||
}
|
||||
|
||||
interface LoadData {
|
||||
data: Uint8Array
|
||||
}
|
||||
|
||||
interface DataLoaded {
|
||||
|
||||
}
|
||||
|
||||
interface FindClosestNode {
|
||||
latitude: number,
|
||||
longitude: number
|
||||
}
|
||||
|
||||
interface FoundClosestNode {
|
||||
originalLatitude: number,
|
||||
originalLongitude: number,
|
||||
foundLatitude: number,
|
||||
foundLongitude: number,
|
||||
foundNodeId: number
|
||||
}
|
||||
|
||||
interface FindPathsFromNode {
|
||||
nodeId: number,
|
||||
minimumSpeed: number,
|
||||
maximumSpeed: number,
|
||||
maximumSpeedLimit: number,
|
||||
dragCoefficient: number,
|
||||
allowMotorways: boolean,
|
||||
allowTunnels: boolean,
|
||||
allowAgainstOneway: boolean
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
nodeId: number,
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
distanceFromStart: number
|
||||
}
|
||||
|
||||
interface FoundPathsFromNode {
|
||||
nodeId: number,
|
||||
endpoints: Endpoint[]
|
||||
}
|
||||
|
||||
interface GetFullPath {
|
||||
startNodeId: number,
|
||||
endNodeId: number
|
||||
}
|
||||
|
||||
export interface Coordinate {
|
||||
latitude: number,
|
||||
longitude: number
|
||||
}
|
||||
|
||||
interface ReturnFullPath {
|
||||
coordinates: Coordinate[]
|
||||
}
|
||||
|
||||
export interface Ring {
|
||||
coordinates: Coordinate[]
|
||||
}
|
||||
|
||||
export interface Polygon {
|
||||
rings: Ring[]
|
||||
}
|
||||
|
||||
interface ExcludeAreas {
|
||||
polygons: Polygon[]
|
||||
}
|
||||
|
||||
interface SearchArea {
|
||||
polygons: Polygon[],
|
||||
minimumSpeed: number,
|
||||
maximumSpeed: number,
|
||||
maximumSpeedLimit: number,
|
||||
dragCoefficient: number,
|
||||
allowMotorways: boolean,
|
||||
allowTunnels: boolean,
|
||||
allowAgainstOneway: boolean
|
||||
}
|
||||
|
||||
interface ContinueSearch {
|
||||
|
||||
}
|
||||
|
||||
export interface SearchAreaResultEntry {
|
||||
nodeId: number,
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
longestRoute: number
|
||||
}
|
||||
|
||||
interface SearchAreaResult {
|
||||
remainingNodes: number,
|
||||
searchResults: SearchAreaResultEntry[]
|
||||
}
|
||||
|
||||
interface ErrorMessage {
|
||||
error: string
|
||||
}
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
import './style.css';
|
||||
import './modules/maphandler/maphandler'
|
||||
import MapHandler from './modules/maphandler/maphandler';
|
||||
import type { Message, Polygon } from './interfaces';
|
||||
|
||||
interface WindowState {
|
||||
state: 'DataNotLoaded' | 'DataLoading' | 'Ready' | 'Searching'
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
minimumSpeed: number,
|
||||
maximumSpeed: number,
|
||||
maximumSpeedLimit: number,
|
||||
dragCoefficient: number,
|
||||
allowMotorways: boolean,
|
||||
allowTunnels: boolean,
|
||||
allowAgainstOneway: boolean,
|
||||
cutoffDistance: number
|
||||
}
|
||||
|
||||
// Default settings values
|
||||
const DEFAULT_MINIMUM_SPEED = 1;
|
||||
const DEFAULT_MAXIMUM_SPEED = 40;
|
||||
const DEFAULT_MAXIMUM_SPEED_LIMIT = 80;
|
||||
const DEFAULT_DRAG_COEFFICIENT = 0.005;
|
||||
const DEFAULT_ALLOW_MOTORWAYS = false;
|
||||
const DEFAULT_ALLOW_TUNNELS = false;
|
||||
const DEFAULT_ALLOW_AGAINST_ONE_WAY = false;
|
||||
const DEFAULT_CUTOFF_DISTANCE = 1000;
|
||||
|
||||
// Set up variables
|
||||
let routeWorker = new Worker(new URL('./modules/worker/worker.ts', import.meta.url), {type: 'module'});
|
||||
var mapHandler: MapHandler | null = null;
|
||||
var currentSearchArea: Polygon[] = [];
|
||||
|
||||
var currentState: WindowState = {state: 'DataNotLoaded'};
|
||||
|
||||
var lastSearchUpdate = Date.now();
|
||||
|
||||
let freewheelingHeader = document.getElementById('freewheeling-header');
|
||||
let notLoadedContainer = document.getElementById('notloadedcontainer');
|
||||
let loadingContainer = document.getElementById('loadingcontainer');
|
||||
let mapContainer = document.getElementById('mapcontainer');
|
||||
let searchButton = document.getElementById('search-button');
|
||||
let searchStatusParagraph = document.getElementById('search-status-paragraph');
|
||||
let searchResultsTable = document.getElementById('search-result-table');
|
||||
let searchResultTableBody = document.getElementById('search-result-table-body');
|
||||
let settingsButton = document.getElementById('settings-button');
|
||||
let settingsDiv = document.getElementById('settings-div');
|
||||
|
||||
let minimumSpeedInput = document.getElementById('minimum-speed-input');
|
||||
let maximumSpeedInput = document.getElementById('maximum-speed-input');
|
||||
let maximumSpeedLimitInput = document.getElementById('maximum-speed-limit-input');
|
||||
let dragCoefficientInput = document.getElementById('drag-coefficient-input');
|
||||
let allowMotorwaysInput = document.getElementById('allow-motorways-input');
|
||||
let allowTunnelsInput = document.getElementById('allow-tunnels-input');
|
||||
let allowAgainstOnewayInput = document.getElementById('allow-wrong-way-input');
|
||||
let cutoffDistanceInput = document.getElementById('cutoff-distance-input');
|
||||
|
||||
|
||||
// Set up the web worker and what to do when we get messages from it
|
||||
routeWorker.onmessage = e => {
|
||||
let message = e.data as Message;
|
||||
if (message.dataLoaded != null) {
|
||||
setState({state: 'Ready'})
|
||||
}
|
||||
|
||||
else if (message.foundClosestNode != null) {
|
||||
mapHandler?.drawStartNode(message.foundClosestNode.foundNodeId, message.foundClosestNode.foundLatitude, message.foundClosestNode.foundLongitude);
|
||||
let settings = getSettings();
|
||||
let findPathsMessage: Message = {findPathsFromNode: {
|
||||
nodeId: message.foundClosestNode.foundNodeId,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}};
|
||||
routeWorker.postMessage(findPathsMessage);
|
||||
} else if (message.foundPathsFromNode != null) {
|
||||
mapHandler?.drawEndPoints(message.foundPathsFromNode.endpoints);
|
||||
} else if (message.returnFullPath != null) {
|
||||
mapHandler?.drawPath(message.returnFullPath.coordinates);
|
||||
} else if (message.searchAreaResult != null) {
|
||||
searchStatusParagraph?.setHTMLUnsafe('Searching. ' + message.searchAreaResult.remainingNodes + ' possible starting points remain.');
|
||||
let currentTime = Date.now();
|
||||
let settings = getSettings();
|
||||
if (message.searchAreaResult.searchResults.length > 0 && (currentTime - lastSearchUpdate > 500 || message.searchAreaResult.remainingNodes == 0)) {
|
||||
lastSearchUpdate = currentTime;
|
||||
searchResultsTable?.style.setProperty('display', 'block');
|
||||
searchResultTableBody?.setHTMLUnsafe('');
|
||||
|
||||
message.searchAreaResult.searchResults.forEach(result => {
|
||||
if (result.longestRoute < settings.cutoffDistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tableRow = document.createElement('tr');
|
||||
let distanceCell = document.createElement('td');
|
||||
let latitudeCell = document.createElement('td');
|
||||
let longitudeCell = document.createElement('td')
|
||||
let buttonCell = document.createElement('td');
|
||||
searchResultTableBody?.appendChild(tableRow);
|
||||
|
||||
tableRow.appendChild(distanceCell);
|
||||
tableRow.appendChild(latitudeCell);
|
||||
tableRow.appendChild(longitudeCell);
|
||||
tableRow.appendChild(buttonCell);
|
||||
|
||||
distanceCell.setHTMLUnsafe(result.longestRoute.toFixed(0) + ' m');
|
||||
latitudeCell.setHTMLUnsafe(result.latitude.toFixed(6));
|
||||
longitudeCell.setHTMLUnsafe(result.longitude.toFixed(6));
|
||||
|
||||
let button = document.createElement('button');
|
||||
buttonCell.appendChild(button);
|
||||
|
||||
button.setHTMLUnsafe('Show in map');
|
||||
button.addEventListener('click', _ => {
|
||||
mapHandler?.drawStartNode(result.nodeId, result.latitude, result.longitude);
|
||||
let settings = getSettings();
|
||||
let requestMessage: Message = {
|
||||
findPathsFromNode: {
|
||||
nodeId: result.nodeId,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}
|
||||
};
|
||||
routeWorker.postMessage(requestMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (currentState.state == 'Searching' && message.searchAreaResult.remainingNodes > 0) {
|
||||
let continueMessage: Message = {continueSearch: {}};
|
||||
routeWorker.postMessage(continueMessage);
|
||||
} else {
|
||||
searchStatusParagraph?.setHTMLUnsafe('Finished searching.');
|
||||
setState({state: 'Ready'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routeWorker.onerror = e => {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
function setUpMapHandler() {
|
||||
mapHandler = new MapHandler();
|
||||
// Next, set up what to do when the user clicks different stuff in the map
|
||||
mapHandler.addClickedMapListener((latitude, longitude) => {
|
||||
let message: Message = {findClosestNode: {latitude: latitude, longitude: longitude}};
|
||||
routeWorker.postMessage(message);
|
||||
});
|
||||
mapHandler.addClickedEndpointListener((startNodeId, endNodeId) => {
|
||||
let message: Message = {getFullPath: {startNodeId: startNodeId, endNodeId: endNodeId}};
|
||||
routeWorker.postMessage(message);
|
||||
});
|
||||
mapHandler.addExclusionAreaListener(polygons => {
|
||||
let message: Message = {excludeAreas: { polygons: polygons}};
|
||||
routeWorker.postMessage(message);
|
||||
let currentStartPoint = mapHandler?.getCurrentStartPointId();
|
||||
if (currentStartPoint == null) {
|
||||
return;
|
||||
}
|
||||
if (currentStartPoint >= 0) {
|
||||
let settings = getSettings();
|
||||
let newRoutesMessage: Message = {findPathsFromNode: {
|
||||
nodeId: currentStartPoint,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}};
|
||||
routeWorker.postMessage(newRoutesMessage);
|
||||
}
|
||||
});
|
||||
|
||||
mapHandler.addSearchAreaListener(polygons => {
|
||||
currentSearchArea = polygons;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// What to do when the window state changes
|
||||
function setState(state: WindowState) {
|
||||
currentState = state;
|
||||
if (state.state == 'DataNotLoaded') {
|
||||
freewheelingHeader?.style.setProperty('display', 'block');
|
||||
notLoadedContainer?.style.setProperty('display', 'block');
|
||||
loadingContainer?.style.setProperty('display', 'none');
|
||||
mapContainer?.style.setProperty('display', 'none');
|
||||
} else if (state.state == 'DataLoading') {
|
||||
freewheelingHeader?.style.setProperty('display', 'block');
|
||||
notLoadedContainer?.style.setProperty('display', 'none');
|
||||
loadingContainer?.style.setProperty('display', 'block');
|
||||
mapContainer?.style.setProperty('display', 'none');
|
||||
} else if (state.state == 'Ready') {
|
||||
freewheelingHeader?.style.setProperty('display', 'none');
|
||||
notLoadedContainer?.style.setProperty('display', 'none');
|
||||
loadingContainer?.style.setProperty('display', 'none');
|
||||
mapContainer?.style.setProperty('display', 'block');
|
||||
if (mapHandler == null) {
|
||||
setUpMapHandler();
|
||||
}
|
||||
mapHandler?.enableEditing();
|
||||
enableSettings();
|
||||
searchButton?.setHTMLUnsafe('Start search');
|
||||
} else if (state.state == 'Searching') {
|
||||
freewheelingHeader?.style.setProperty('display', 'none');
|
||||
notLoadedContainer?.style.setProperty('display', 'none');
|
||||
loadingContainer?.style.setProperty('display', 'none');
|
||||
mapContainer?.style.setProperty('display', 'block');
|
||||
if (mapHandler == null) {
|
||||
setUpMapHandler();
|
||||
}
|
||||
mapHandler?.disableEditing();
|
||||
disableSettings();
|
||||
searchButton?.setHTMLUnsafe('Cancel search');
|
||||
}
|
||||
};
|
||||
setState({state: 'DataNotLoaded'});
|
||||
|
||||
// Set up the settings values
|
||||
function setUpNumberInput(element: HTMLElement | null, localStorageKey: string, defaultValue: number): void {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
let numberValue: number = Number(localStorage.getItem(localStorageKey)) || defaultValue;
|
||||
element.value = numberValue.toString();
|
||||
element.addEventListener('change', _ => localStorage.setItem(localStorageKey, element.value));
|
||||
}
|
||||
}
|
||||
|
||||
function setUpBooleanInput(element: HTMLElement | null, localStorageKey: string, defaultValue: boolean): void {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
if (localStorage.getItem(localStorageKey) != null) {
|
||||
element.checked = localStorage.getItem(localStorageKey) === 'true';
|
||||
} else {
|
||||
element.checked = defaultValue;
|
||||
}
|
||||
|
||||
element.addEventListener('change', _ => {localStorage.setItem(localStorageKey, element.checked.toString())});
|
||||
}
|
||||
}
|
||||
|
||||
function getNumberValue(element: HTMLElement | null, defaultValue: number): number {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
return Number(element.value) || defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
function getBooleanValue(element: HTMLElement | null, defaultValue: boolean): boolean {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
return element.checked;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
function enableInput(element: HTMLElement | null) {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
element.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function disableInput(element: HTMLElement | null) {
|
||||
if (element != null && element instanceof HTMLInputElement) {
|
||||
element.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
setUpNumberInput(minimumSpeedInput, 'minimum-speed', DEFAULT_MINIMUM_SPEED);
|
||||
setUpNumberInput(maximumSpeedInput, 'maximum-speed', DEFAULT_MAXIMUM_SPEED);
|
||||
setUpNumberInput(maximumSpeedLimitInput, 'maximum-speed-limit', DEFAULT_MAXIMUM_SPEED_LIMIT);
|
||||
setUpNumberInput(dragCoefficientInput, 'drag-coefficient', DEFAULT_DRAG_COEFFICIENT);
|
||||
setUpBooleanInput(allowMotorwaysInput, 'allow-motorways', DEFAULT_ALLOW_MOTORWAYS);
|
||||
setUpBooleanInput(allowTunnelsInput, 'allow-tunnels', DEFAULT_ALLOW_TUNNELS);
|
||||
setUpBooleanInput(allowAgainstOnewayInput, 'allow-against-one-way', DEFAULT_ALLOW_AGAINST_ONE_WAY);
|
||||
setUpNumberInput(cutoffDistanceInput, 'cutoff-distance', DEFAULT_CUTOFF_DISTANCE);
|
||||
|
||||
function getSettings(): Settings {
|
||||
return {
|
||||
minimumSpeed: getNumberValue(minimumSpeedInput, DEFAULT_MINIMUM_SPEED) / 3.6,
|
||||
maximumSpeed: getNumberValue(maximumSpeedInput, DEFAULT_MAXIMUM_SPEED) / 3.6,
|
||||
maximumSpeedLimit: getNumberValue(maximumSpeedLimitInput, DEFAULT_MAXIMUM_SPEED_LIMIT),
|
||||
dragCoefficient: getNumberValue(dragCoefficientInput, DEFAULT_DRAG_COEFFICIENT),
|
||||
allowMotorways: getBooleanValue(allowMotorwaysInput, DEFAULT_ALLOW_MOTORWAYS),
|
||||
allowTunnels: getBooleanValue(allowTunnelsInput, DEFAULT_ALLOW_TUNNELS),
|
||||
allowAgainstOneway: getBooleanValue(allowAgainstOnewayInput, DEFAULT_ALLOW_AGAINST_ONE_WAY),
|
||||
cutoffDistance: getNumberValue(cutoffDistanceInput, DEFAULT_CUTOFF_DISTANCE)
|
||||
};
|
||||
}
|
||||
|
||||
function enableSettings(): void {
|
||||
enableInput(minimumSpeedInput);
|
||||
enableInput(maximumSpeedInput);
|
||||
enableInput(maximumSpeedLimitInput);
|
||||
enableInput(dragCoefficientInput);
|
||||
enableInput(allowMotorwaysInput);
|
||||
enableInput(allowTunnelsInput);
|
||||
enableInput(allowAgainstOnewayInput);
|
||||
}
|
||||
|
||||
function disableSettings(): void {
|
||||
disableInput(minimumSpeedInput);
|
||||
disableInput(maximumSpeedInput);
|
||||
disableInput(maximumSpeedLimitInput);
|
||||
disableInput(dragCoefficientInput);
|
||||
disableInput(allowMotorwaysInput);
|
||||
disableInput(allowTunnelsInput);
|
||||
disableInput(allowAgainstOnewayInput);
|
||||
}
|
||||
|
||||
|
||||
// Finally, set up various events when clicking things
|
||||
searchButton?.addEventListener('click', _ => {
|
||||
if (currentState.state == 'Ready') {
|
||||
if (currentSearchArea.length > 0) {
|
||||
setState({state: 'Searching'})
|
||||
let settings = getSettings();
|
||||
let message: Message = {searchArea: {
|
||||
polygons: currentSearchArea,
|
||||
minimumSpeed: settings.minimumSpeed,
|
||||
maximumSpeed: settings.maximumSpeed,
|
||||
maximumSpeedLimit: settings.maximumSpeedLimit,
|
||||
dragCoefficient: settings.dragCoefficient,
|
||||
allowMotorways: settings.allowMotorways,
|
||||
allowTunnels: settings.allowTunnels,
|
||||
allowAgainstOneway: settings.allowAgainstOneway
|
||||
}};
|
||||
routeWorker.postMessage(message);
|
||||
}
|
||||
} else if (currentState.state == 'Searching') {
|
||||
setState({state: 'Ready'});
|
||||
}
|
||||
});
|
||||
|
||||
settingsButton?.addEventListener('click', _ => {
|
||||
let settingsDisplay = settingsDiv?.style.getPropertyValue('display');
|
||||
if (settingsDisplay === 'none') {
|
||||
settingsDiv?.style.setProperty('display', 'block');
|
||||
} else {
|
||||
settingsDiv?.style.setProperty('display', 'none');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('data-file-chooser')?.addEventListener('change', chooseEvent => {
|
||||
let eventTarget = chooseEvent.target;
|
||||
if (!(eventTarget instanceof HTMLInputElement) || eventTarget.files == null || eventTarget.files.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({state: 'DataLoading'});
|
||||
let reader = new FileReader();
|
||||
reader.onload = loadEvent => {
|
||||
let data = loadEvent.target?.result;
|
||||
if (data != null && data instanceof ArrayBuffer) {
|
||||
let message: Message = {loadData: {data: new Uint8Array(data)}};
|
||||
routeWorker.postMessage(message);
|
||||
} else {
|
||||
setState({state: 'DataNotLoaded'});
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(eventTarget.files[0]);
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import L from 'leaflet'
|
||||
|
||||
import blueUrl from '../../../static/marker-icon-2x-blue.png'
|
||||
import goldUrl from '../../../static/marker-icon-2x-gold.png'
|
||||
import redUrl from '../../../static/marker-icon-2x-red.png'
|
||||
import greenUrl from '../../../static/marker-icon-2x-green.png'
|
||||
import orangeUrl from '../../../static/marker-icon-2x-orange.png'
|
||||
import yellowUrl from '../../../static/marker-icon-2x-yellow.png'
|
||||
import violetUrl from '../../../static/marker-icon-2x-violet.png'
|
||||
import greyUrl from '../../../static/marker-icon-2x-grey.png'
|
||||
import blackUrl from '../../../static/marker-icon-2x-black.png'
|
||||
import shadowUrl from '../../../static/marker-shadow.png'
|
||||
|
||||
export const blueIcon = new L.Icon({
|
||||
iconUrl: blueUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const goldIcon = new L.Icon({
|
||||
iconUrl: goldUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const redIcon = new L.Icon({
|
||||
iconUrl: redUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const greenIcon = new L.Icon({
|
||||
iconUrl: greenUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const orangeIcon = new L.Icon({
|
||||
iconUrl: orangeUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const yellowIcon = new L.Icon({
|
||||
iconUrl: yellowUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const violetIcon = new L.Icon({
|
||||
iconUrl: violetUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const greyIcon = new L.Icon({
|
||||
iconUrl: greyUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
export const blackIcon = new L.Icon({
|
||||
iconUrl: blackUrl,
|
||||
shadowUrl: shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
.search-icon {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 32l32 0c17.7 0 32 14.3 32 32l0 32L96 96l0-32c0-17.7 14.3-32 32-32zm64 96l0 320c0 17.7-14.3 32-32 32L32 480c-17.7 0-32-14.3-32-32l0-59.1c0-34.6 9.4-68.6 27.2-98.3C40.9 267.8 49.7 242.4 53 216L60.5 156c2-16 15.6-28 31.8-28l99.8 0zm227.8 0c16.1 0 29.8 12 31.8 28L459 216c3.3 26.4 12.1 51.8 25.8 74.6c17.8 29.7 27.2 63.7 27.2 98.3l0 59.1c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-320 99.8 0zM320 64c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 32-96 0 0-32zm-32 64l0 160-64 0 0-160 64 0z"/></svg>');
|
||||
}
|
||||
|
||||
.exclude-icon {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5zm45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5L412.5 367.2zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>');
|
||||
}
|
||||
|
||||
.cancel-icon {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>')
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L, { FeatureGroup, Marker, Polygon, Polyline, type LatLngTuple } from 'leaflet';
|
||||
import '@geoman-io/leaflet-geoman-free'
|
||||
import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css'
|
||||
|
||||
import './maphandler.css'
|
||||
import { greenIcon, redIcon, violetIcon } from './icons.ts'
|
||||
import type { Endpoint, Coordinate, Polygon as InterfacePolygon } from '../../interfaces.ts'
|
||||
import type { Position } from 'geojson';
|
||||
|
||||
interface ClickedMapListener {
|
||||
(latitude: number, longitude: number): void;
|
||||
}
|
||||
|
||||
interface ClickedEndpointListener {
|
||||
(startpointId: number, endpointId: number): void;
|
||||
}
|
||||
|
||||
interface SearchAreaPolygonListener {
|
||||
(searchAreaPolygons: InterfacePolygon[]): void;
|
||||
}
|
||||
|
||||
interface ExclusionPolygonListener {
|
||||
(exclusionPolygons: InterfacePolygon[]): void;
|
||||
}
|
||||
|
||||
interface OnChangeFunction {
|
||||
(): void;
|
||||
}
|
||||
|
||||
class MapHandler {
|
||||
map;
|
||||
currentStartPoint?: number;
|
||||
|
||||
// Whenever the polygons of either the search area or the exclusion area are changed, these listeners are called
|
||||
clickedMapListeners: ClickedMapListener[] = [];
|
||||
clickedEndpointListeners: ClickedEndpointListener[] = [];
|
||||
searchAreaPolygonListeners: SearchAreaPolygonListener[] = [];
|
||||
exclusionPolygonListeners: ExclusionPolygonListener[] = [];
|
||||
|
||||
// We need feature groups to store the polygons that define these areas
|
||||
searchAreaFeatureGroup: FeatureGroup;
|
||||
exclusionAreaFeatureGroup: FeatureGroup;
|
||||
|
||||
startMarker?: Marker;
|
||||
endMarkers: FeatureGroup;
|
||||
path?: Polyline;
|
||||
|
||||
editingPolygons: boolean = false;
|
||||
|
||||
onChangeFunction?: OnChangeFunction
|
||||
|
||||
constructor() {
|
||||
|
||||
// Use OpenStreetMaps and center on Oslo
|
||||
this.map = L.map('map', {
|
||||
center: L.latLng(59.92, 10.74),
|
||||
zoom: 13,
|
||||
});
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(this.map);
|
||||
|
||||
// Make sure that geoman can only control the polygons it itself has created
|
||||
L.PM.setOptIn(true);
|
||||
this.map.on('pm:create', (e) => {
|
||||
e.layer.options.pmIgnore = false;
|
||||
L.PM.reInitLayer(e.layer);
|
||||
|
||||
if (this.onChangeFunction != null) {
|
||||
e.layer.on('pm:edit', this.onChangeFunction);
|
||||
e.layer.on('pm:drag', this.onChangeFunction);
|
||||
e.layer.on('pm:cut', this.onChangeFunction);
|
||||
e.layer.on('pm:remove', this.onChangeFunction);
|
||||
e.layer.on('pm:rotate', this.onChangeFunction);
|
||||
this.onChangeFunction();
|
||||
};
|
||||
})
|
||||
|
||||
// Make sure we don't start route searches points when editing polygons
|
||||
this.map.on('pm:globaldrawmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globalcutmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globaldragmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globaleditmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globalrotatemodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
});
|
||||
|
||||
this.map.on('pm:globalremovalmodetoggled', e => {
|
||||
this.editingPolygons = e.enabled;
|
||||
})
|
||||
|
||||
// Add geoman controls for drawing these polygons
|
||||
this.map.pm.addControls({
|
||||
position: 'bottomleft',
|
||||
drawMarker: false,
|
||||
drawCircle: false,
|
||||
drawCircleMarker: false,
|
||||
drawPolyline: false,
|
||||
drawText: false,
|
||||
drawRectangle: false,
|
||||
drawPolygon: false
|
||||
});
|
||||
|
||||
this.map.pm.Toolbar.copyDrawControl('Polygon', {
|
||||
name: 'searcharea',
|
||||
block: 'draw',
|
||||
title: 'Define the search area for longest route',
|
||||
className: 'search-icon',
|
||||
onClick: () => {this.setFeatureGroup(this.searchAreaFeatureGroup, 'green')}
|
||||
})
|
||||
|
||||
this.map.pm.Toolbar.copyDrawControl('Polygon', {
|
||||
name: 'exclusionarea',
|
||||
block: 'draw',
|
||||
title: 'Draw areas that should not be entered',
|
||||
className: 'exclude-icon',
|
||||
onClick: () => {this.setFeatureGroup(this.exclusionAreaFeatureGroup, 'red')}
|
||||
});
|
||||
|
||||
this.map.pm.Toolbar.createCustomControl({
|
||||
name: 'removePolygons',
|
||||
block: 'draw',
|
||||
title: 'Remove all search and exclusion areas',
|
||||
className: 'cancel-icon',
|
||||
onClick: () => {
|
||||
this.searchAreaFeatureGroup.clearLayers();
|
||||
this.exclusionAreaFeatureGroup.clearLayers();
|
||||
this.onExclusionAreaChange();
|
||||
this.onSearchAreaChange();
|
||||
}
|
||||
});
|
||||
|
||||
this.map.pm.Toolbar.setBlockPosition('draw', 'topleft');
|
||||
|
||||
// In order for the polygons in the feature groups to be displayed, they must be added to the map
|
||||
this.searchAreaFeatureGroup = new L.FeatureGroup().addTo(this.map);
|
||||
this.exclusionAreaFeatureGroup = new L.FeatureGroup().addTo(this.map);
|
||||
this.endMarkers = new L.FeatureGroup().addTo(this.map);
|
||||
|
||||
this.map.addEventListener('click', e => {
|
||||
if (!this.editingPolygons) {
|
||||
this.clickedMapListeners.forEach(routeSearchListener => {
|
||||
routeSearchListener(e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.exclusionAreaFeatureGroup.addEventListener('pm:change', _ => {
|
||||
let polygons: Polygon[] = [];
|
||||
this.exclusionAreaFeatureGroup.eachLayer(layer => {
|
||||
if (layer instanceof Polygon) {
|
||||
polygons.push(layer);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getCurrentStartPointId(): number {
|
||||
if (this.currentStartPoint) {
|
||||
return this.currentStartPoint;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private getPolygonsOfFeatureGroup(featureGroup: FeatureGroup): InterfacePolygon[] {
|
||||
let multiPolygonCoordinates: Position[][][] = [];
|
||||
featureGroup.eachLayer(layer => {
|
||||
if (layer instanceof Polygon) {
|
||||
let geojson = layer.toGeoJSON();
|
||||
if (geojson.geometry.type == 'Polygon') {
|
||||
multiPolygonCoordinates.push(geojson.geometry.coordinates);
|
||||
} else if (geojson.geometry.type == 'MultiPolygon') {
|
||||
geojson.geometry.coordinates.forEach(polygon => {
|
||||
multiPolygonCoordinates.push(polygon);
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return multiPolygonCoordinates.map(polygonCoordinates => {
|
||||
return {rings: polygonCoordinates.map(ringCoordinates => {
|
||||
return {coordinates: ringCoordinates.map(singleCoordinate => {
|
||||
return {
|
||||
latitude: singleCoordinate[1],
|
||||
longitude: singleCoordinate[0]
|
||||
}
|
||||
})}
|
||||
})};
|
||||
});
|
||||
}
|
||||
|
||||
private onExclusionAreaChange(): void {
|
||||
let multiPolygon: InterfacePolygon[] = this.getPolygonsOfFeatureGroup(this.exclusionAreaFeatureGroup);
|
||||
this.exclusionPolygonListeners.forEach(listener => {
|
||||
listener(multiPolygon);
|
||||
})
|
||||
}
|
||||
|
||||
private onSearchAreaChange(): void {
|
||||
let multiPolygon: InterfacePolygon[] = this.getPolygonsOfFeatureGroup(this.searchAreaFeatureGroup);
|
||||
this.searchAreaPolygonListeners.forEach(listener => {
|
||||
listener(multiPolygon);
|
||||
})
|
||||
}
|
||||
|
||||
// This function is called when clicking one of the draw buttons, to make sure the polygons drawn have the right colour and
|
||||
// are added to the right feature group
|
||||
private setFeatureGroup(featureGroup: FeatureGroup, color: string): void {
|
||||
this.map.pm.setGlobalOptions({
|
||||
layerGroup: featureGroup,
|
||||
pathOptions: {color: color, fillOpacity: 0.05},
|
||||
templineStyle: {color: color, radius: 10},
|
||||
hintlineStyle: {color: color, dashArray: [5, 5]}
|
||||
});
|
||||
|
||||
if (featureGroup === this.exclusionAreaFeatureGroup) {
|
||||
this.onChangeFunction = this.onExclusionAreaChange;
|
||||
} else if (featureGroup === this.searchAreaFeatureGroup) {
|
||||
this.onChangeFunction = this.onSearchAreaChange;
|
||||
}
|
||||
}
|
||||
|
||||
public addClickedMapListener(routeSearchListener: ClickedMapListener): void {
|
||||
this.clickedMapListeners.push(routeSearchListener);
|
||||
}
|
||||
|
||||
public addClickedEndpointListener(clickedEndpointListener: ClickedEndpointListener): void {
|
||||
this.clickedEndpointListeners.push(clickedEndpointListener);
|
||||
}
|
||||
|
||||
public addExclusionAreaListener(exclusionAreaListener: ExclusionPolygonListener): void {
|
||||
this.exclusionPolygonListeners.push(exclusionAreaListener);
|
||||
}
|
||||
|
||||
public addSearchAreaListener(searchAreaListener: SearchAreaPolygonListener): void {
|
||||
this.searchAreaPolygonListeners.push(searchAreaListener);
|
||||
}
|
||||
|
||||
public drawStartNode(nodeId: number, latitude: number, longitude: number): void {
|
||||
if (this.path != null) {
|
||||
this.map.removeLayer(this.path);
|
||||
this.path = undefined;
|
||||
}
|
||||
|
||||
if (this.startMarker != null) {
|
||||
this.map.removeLayer(this.startMarker);
|
||||
}
|
||||
|
||||
this.currentStartPoint = nodeId;
|
||||
this.endMarkers.clearLayers();
|
||||
this.startMarker = L.marker([latitude, longitude], {icon: greenIcon}).addTo(this.map);
|
||||
}
|
||||
|
||||
public drawEndPoints(endpoints: Endpoint[]): void {
|
||||
this.endMarkers.clearLayers();
|
||||
if (this.path != null) {
|
||||
this.map.removeLayer(this.path);
|
||||
this.path = undefined;
|
||||
}
|
||||
var firstMarker = true;
|
||||
endpoints.forEach(endpoint => {
|
||||
var settings;
|
||||
if (firstMarker) {
|
||||
settings = {
|
||||
icon: redIcon
|
||||
};
|
||||
} else {
|
||||
settings = {
|
||||
icon: violetIcon,
|
||||
opacity: 0.7
|
||||
};
|
||||
}
|
||||
|
||||
var marker = L.marker([endpoint.latitude, endpoint.longitude], settings).addTo(this.endMarkers);
|
||||
marker.bindTooltip(Math.round(endpoint.distanceFromStart) + 'm');
|
||||
if (firstMarker) {
|
||||
marker.openTooltip();
|
||||
}
|
||||
firstMarker = false;
|
||||
|
||||
marker.addEventListener('click', _ => {
|
||||
this.clickedEndpointListeners.forEach(endpointListener => {
|
||||
if (this.currentStartPoint != null) {
|
||||
endpointListener(this.currentStartPoint, endpoint.nodeId);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public drawPath(coordinates: Coordinate[]): void {
|
||||
if (this.path != null) {
|
||||
this.map.removeLayer(this.path);
|
||||
}
|
||||
|
||||
var leafletCoordinates = coordinates.map(coordinate => {
|
||||
let latLngTuple: LatLngTuple = [coordinate.latitude, coordinate.longitude];
|
||||
return latLngTuple;
|
||||
});
|
||||
|
||||
this.path = L.polyline(leafletCoordinates).addTo(this.map);
|
||||
}
|
||||
|
||||
public enableEditing() {
|
||||
this.map.pm.Toolbar.setButtonDisabled('searcharea', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('exclusionarea', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('editMode', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('dragMode', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('cutPolygon', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('removalMode', false);
|
||||
this.map.pm.Toolbar.setButtonDisabled('rotateMode', false);
|
||||
}
|
||||
|
||||
public disableEditing() {
|
||||
this.map.pm.Toolbar.setButtonDisabled('searcharea', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('exclusionarea', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('editMode', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('dragMode', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('cutPolygon', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('removalMode', true);
|
||||
this.map.pm.Toolbar.setButtonDisabled('rotateMode', true);
|
||||
|
||||
this.map.pm.disableDraw();
|
||||
this.map.pm.disableGlobalEditMode();
|
||||
this.map.pm.disableGlobalDragMode();
|
||||
this.map.pm.disableGlobalRemovalMode();
|
||||
this.map.pm.disableGlobalCutMode();
|
||||
this.map.pm.disableGlobalRotateMode();
|
||||
}
|
||||
}
|
||||
|
||||
export default MapHandler;
|
||||
@@ -0,0 +1,191 @@
|
||||
import Module, { type AreaSearchEntries, type MainModule, type MultiPolygon } from '../../../native/route_search.js';
|
||||
import proj4 from 'proj4';
|
||||
|
||||
import type { Endpoint, Message, Polygon, SearchAreaResultEntry } from '../../interfaces';
|
||||
|
||||
var dataLoaded = false;
|
||||
var module: MainModule | undefined = undefined;
|
||||
|
||||
function sendErrorMessage(error: string): void {
|
||||
let message: Message = {errorMessage: {error: error}};
|
||||
postMessage(message);
|
||||
}
|
||||
|
||||
function createCMultiPolygon(module: MainModule, polygons: Polygon[]): MultiPolygon {
|
||||
let multiPolygon = new module.MultiPolygon();
|
||||
polygons.forEach(polygon => {
|
||||
let cPolygon = new module.Polygon();
|
||||
polygon.rings.forEach(ring => {
|
||||
let cRing = new module.Ring();
|
||||
ring.coordinates.forEach(coordinate => {
|
||||
let polygonCoordinate = new module.PolygonCoordinate();
|
||||
let utmCoordinates = proj4('EPSG:4326', 'EPSG:32633', [coordinate.longitude, coordinate.latitude]);
|
||||
polygonCoordinate.x = utmCoordinates[0];
|
||||
polygonCoordinate.y = utmCoordinates[1];
|
||||
cRing.push_back(polygonCoordinate);
|
||||
});
|
||||
cPolygon.push_back(cRing);
|
||||
});
|
||||
multiPolygon.push_back(cPolygon);
|
||||
});
|
||||
return multiPolygon;
|
||||
}
|
||||
|
||||
function getAreaSearchResults(searchedNodes: AreaSearchEntries): SearchAreaResultEntry[] {
|
||||
let searchResults: SearchAreaResultEntry[] = [];
|
||||
for (var i = 0; i < searchedNodes.size(); i++) {
|
||||
let searchedNode = searchedNodes.get(i);
|
||||
if (searchedNode != null) {
|
||||
let utmCoordinates = [searchedNode.positionX, searchedNode.positionY];
|
||||
let lngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', utmCoordinates);
|
||||
searchResults.push({
|
||||
nodeId: searchedNode.nodeId,
|
||||
latitude: lngLatCoordinates[1],
|
||||
longitude: lngLatCoordinates[0],
|
||||
longestRoute: searchedNode.longestRoute
|
||||
});
|
||||
}
|
||||
}
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
onmessage = async (e) => {
|
||||
if (module == null) {
|
||||
module = await Module();
|
||||
}
|
||||
|
||||
let message = e.data as Message;
|
||||
|
||||
if (message.loadData != null) {
|
||||
module.FS.writeFile('roads.dat', message.loadData.data);
|
||||
module.loadData('roads.dat');
|
||||
dataLoaded = true;
|
||||
|
||||
let returnMessage: Message = {dataLoaded: {}}
|
||||
postMessage(returnMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the data is not loaded, it will not be possible to perform any further operations
|
||||
if (!dataLoaded) {
|
||||
sendErrorMessage('Data not loaded')
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.findClosestNode != null) {
|
||||
let lngLatCoordinates = [message.findClosestNode.longitude, message.findClosestNode.latitude];
|
||||
let utmCoordinates = proj4('EPSG:4326', 'EPSG:32633', lngLatCoordinates);
|
||||
let node = module.findClosestNode(utmCoordinates[0], utmCoordinates[1]);
|
||||
|
||||
let nodeUtmCoordinates = [node.positionX, node.positionY];
|
||||
let nodeLngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', nodeUtmCoordinates);
|
||||
|
||||
let returnMessage: Message = {
|
||||
foundClosestNode: {
|
||||
originalLatitude: message.findClosestNode.latitude,
|
||||
originalLongitude: message.findClosestNode.longitude,
|
||||
foundLatitude: nodeLngLatCoordinates[1],
|
||||
foundLongitude: nodeLngLatCoordinates[0],
|
||||
foundNodeId: node.nodeId
|
||||
}
|
||||
};
|
||||
node.delete();
|
||||
postMessage(returnMessage);
|
||||
};
|
||||
|
||||
if (message.findPathsFromNode != null) {
|
||||
let results = module.findAllPathsFromPoint(
|
||||
message.findPathsFromNode.nodeId,
|
||||
message.findPathsFromNode.minimumSpeed,
|
||||
message.findPathsFromNode.maximumSpeed,
|
||||
message.findPathsFromNode.maximumSpeedLimit,
|
||||
message.findPathsFromNode.dragCoefficient,
|
||||
message.findPathsFromNode.allowMotorways,
|
||||
message.findPathsFromNode.allowTunnels,
|
||||
message.findPathsFromNode.allowAgainstOneway
|
||||
);
|
||||
|
||||
let endpoints: Endpoint[] = [];
|
||||
for (var i = 0; i < results.endPoints.size(); i++) {
|
||||
let nodeData = results.endPoints.get(i);
|
||||
if (!nodeData) {
|
||||
sendErrorMessage('Could not find paths from node ' + message.findPathsFromNode.nodeId);
|
||||
return;
|
||||
}
|
||||
let coordinates = [nodeData.positionX, nodeData.positionY];
|
||||
let lngLatCoordinates = proj4('EPSG:32633', 'EPSG:4326', coordinates);
|
||||
endpoints.push({
|
||||
nodeId: nodeData.nodeId,
|
||||
latitude: lngLatCoordinates[1],
|
||||
longitude: lngLatCoordinates[0],
|
||||
distanceFromStart: nodeData.distanceFromStart
|
||||
});
|
||||
}
|
||||
results.delete();
|
||||
|
||||
let returnMessage: Message = {foundPathsFromNode: {nodeId: message.findPathsFromNode.nodeId, endpoints: endpoints}};
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
|
||||
if (message.getFullPath != null) {
|
||||
let path = module.getPath(message.getFullPath.startNodeId, message.getFullPath.endNodeId);
|
||||
if (!path) {
|
||||
sendErrorMessage('Could not get path');
|
||||
return;
|
||||
}
|
||||
var coordinates = [];
|
||||
for (var i = 0; i < path.size(); i++) {
|
||||
let currentPoint = path.get(i);
|
||||
if (!currentPoint) {
|
||||
continue;
|
||||
}
|
||||
coordinates.push([currentPoint.positionX, currentPoint.positionY]);
|
||||
}
|
||||
path.delete();
|
||||
|
||||
coordinates = coordinates.map(utmCoordinate => {
|
||||
let lngLat = proj4('EPSG:32633', 'EPSG:4326', utmCoordinate);
|
||||
return {latitude: lngLat[1], longitude: lngLat[0]};
|
||||
});
|
||||
|
||||
let returnMessage: Message = {returnFullPath: {coordinates: coordinates}};
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
|
||||
if (message.excludeAreas != null) {
|
||||
module.excludeNodesWithinPolygons(createCMultiPolygon(module, message.excludeAreas.polygons));
|
||||
}
|
||||
|
||||
if (message.searchArea != null) {
|
||||
let settings = message.searchArea;
|
||||
let result = module.startAreaSearch(
|
||||
settings.minimumSpeed,
|
||||
settings.maximumSpeed,
|
||||
settings.maximumSpeedLimit,
|
||||
settings.dragCoefficient,
|
||||
settings.allowMotorways,
|
||||
settings.allowTunnels,
|
||||
settings.allowAgainstOneway,
|
||||
createCMultiPolygon(module, settings.polygons)
|
||||
);
|
||||
|
||||
let returnMessage: Message = { searchAreaResult: {
|
||||
remainingNodes: result.remainingNodes,
|
||||
searchResults: getAreaSearchResults(result.searchedNodes)
|
||||
}};
|
||||
|
||||
result.delete();
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
|
||||
if (message.continueSearch != null) {
|
||||
let result = module.continueAreaSearch();
|
||||
let returnMessage: Message = { searchAreaResult: {
|
||||
remainingNodes: result.remainingNodes,
|
||||
searchResults: getAreaSearchResults(result.searchedNodes)
|
||||
}};
|
||||
|
||||
result.delete();
|
||||
postMessage(returnMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
:root {
|
||||
--background-color: #fff7f7;
|
||||
--header-color: #d46a64;
|
||||
--header-stroke-color: #550000;
|
||||
--text-color: #000000;
|
||||
--collapsible-background-color: #AA3939;
|
||||
--collapsible-active-background-color: #D46A6A;
|
||||
--collapsible-color: #FFAAAA;
|
||||
--collapsible-active-color: #550000;
|
||||
--explanation-background-color: #FFAAAA;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
font-family: 'Courier New';
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--header-color);
|
||||
-webkit-text-stroke-width: 2px;
|
||||
-webkit-text-stroke-color: var(--header-stroke-color);
|
||||
font-size: 80px;
|
||||
font-family: 'Helvetica';
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 80vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#settings-div {
|
||||
width: 40vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
padding: 10px;
|
||||
margin-top:10px;
|
||||
}
|
||||
|
||||
.settings-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
table {
|
||||
width: max(60vw, 800px);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border: solid 1px;
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
th {
|
||||
border: solid 1px;
|
||||
width: max(15vw, 200px);
|
||||
}
|
||||
|
||||
td {
|
||||
border: solid 1px;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user