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]); });