399 lines
15 KiB
TypeScript
399 lines
15 KiB
TypeScript
import './style.css';
|
|
import './modules/maphandler/maphandler'
|
|
import MapHandler from './modules/maphandler/maphandler';
|
|
import type { Message, PathSegment, Polygon } from './interfaces';
|
|
import { downloadGpxFile } from './modules/gpx/exporter';
|
|
|
|
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 lastRoute: PathSegment[] | undefined;
|
|
|
|
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 exportButton = document.getElementById('export-route-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) {
|
|
let settings = getSettings();
|
|
mapHandler?.drawPath(message.returnFullPath.pathSegments, settings.minimumSpeed, settings.maximumSpeed);
|
|
setRoute(message.returnFullPath.pathSegments);
|
|
} 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);
|
|
setRoute(undefined);
|
|
});
|
|
mapHandler.addClickedEndpointListener((startNodeId, endNodeId) => {
|
|
let settings = getSettings();
|
|
let message: Message = {getFullPath: {startNodeId: startNodeId, endNodeId: endNodeId, minimumSpeed: settings.minimumSpeed, dragCoefficient: settings.dragCoefficient}};
|
|
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'});
|
|
|
|
function setRoute(route?: PathSegment[]) {
|
|
lastRoute = route;
|
|
if (exportButton && exportButton instanceof HTMLButtonElement) {
|
|
if (lastRoute == null) {
|
|
exportButton.disabled = true;
|
|
} else {
|
|
exportButton.disabled = false;
|
|
}
|
|
}
|
|
};
|
|
setRoute(undefined);
|
|
|
|
// 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'});
|
|
}
|
|
});
|
|
|
|
exportButton?.addEventListener('click', _ => {
|
|
if (lastRoute != null) {
|
|
downloadGpxFile(lastRoute);
|
|
}
|
|
})
|
|
|
|
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]);
|
|
}); |