initial commit
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
|||||||
|
.vscode/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
.output
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Sveltekit cache directory
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
**/.vitepress/dist
|
||||||
|
|
||||||
|
# vitepress cache directory
|
||||||
|
**/.vitepress/cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Firebase cache directory
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Vite files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# My personal shell script to upload to server
|
||||||
|
build_and_upload_to_server.sh
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="no">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Orientering</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
<h1 class="big-screen">Korteste vei</h1>
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="navbar-button" id="clear-map">Rydd kart</div>
|
||||||
|
<div class="navbar-button" id="edit-distances">Rediger avstander</div>
|
||||||
|
<div class="navbar-button" id="share-link">Del lenke</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div id="distance-popup">
|
||||||
|
<p>Avstander</p>
|
||||||
|
<button id="retrieve-distances-button">Hent fra OpenRouteService</button>
|
||||||
|
<form id="route-type-form">
|
||||||
|
<label for="driving-radio">Bilruter:</label>
|
||||||
|
<input type="radio" name="route-type" value="driving-car" id="driving-radio" checked />
|
||||||
|
<br />
|
||||||
|
<label for="biking-radio">Sykkelruter:</label>
|
||||||
|
<input type="radio" name="route-type" value="cycling-regular" id="biking-radio" />
|
||||||
|
<br />
|
||||||
|
<label for="walking-radio">Gåruter:</label>
|
||||||
|
<input type="radio" name="route-type" value="foot-hiking" id="walking-radio" />
|
||||||
|
</form>
|
||||||
|
<table id="distance-table">
|
||||||
|
<tr><th></th><th>Avstand</th></tr>
|
||||||
|
</table>
|
||||||
|
<button id="retrieve-routes-button">Hent rute</button>
|
||||||
|
</div>
|
||||||
|
<div id="map"></div>
|
||||||
|
</div>
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="navbar-button" id="calculate-route">Beregn rute</div>
|
||||||
|
<div class="navbar-button" id="orienteering">Orientering</div>
|
||||||
|
<div class="navbar-button" id="upload">Last inn</div>
|
||||||
|
</div>
|
||||||
|
<div id="middle-popup">
|
||||||
|
<div id="upload-div">
|
||||||
|
<h2>Last inn</h2>
|
||||||
|
<label for="input-text-area">Lim inn GeoJSON- eller WKT-punkter</label>
|
||||||
|
<br />
|
||||||
|
<textarea rows="30" id="input-text-area"></textarea>
|
||||||
|
<br />
|
||||||
|
<button id="upload-button">Last inn</button>
|
||||||
|
</div>
|
||||||
|
<div id="orienteering-div">
|
||||||
|
<h2>Orientering</h2>
|
||||||
|
<label for="upload-ppen-input">Last opp PPEN-fil: </label>
|
||||||
|
<input type="file" id="upload-ppen-input" />
|
||||||
|
<br />
|
||||||
|
<label for="upload-xml-input">Last opp XML-fil med koordinater (valgfritt): </label>
|
||||||
|
<input type="file" id="upload-xml-input" />
|
||||||
|
<br />
|
||||||
|
<button id="download-ppen-route-button">Last ned ny PPEN-fil</button>
|
||||||
|
<p id="ppen-explanation"> </p>
|
||||||
|
</div>
|
||||||
|
<button id="close-popup-button">Lukk</button>
|
||||||
|
</div>
|
||||||
|
<div id="distance-text-div">
|
||||||
|
<p id="distance-text"></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten/bind.h>
|
||||||
|
#include <emscripten/val.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <queue>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct City {
|
||||||
|
double x;
|
||||||
|
double y;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct State{
|
||||||
|
double** weights;
|
||||||
|
int** adjacencyMatrix;
|
||||||
|
double lowerBound;
|
||||||
|
bool finished = false;
|
||||||
|
std::vector<std::pair<int, int>> paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SplitState {
|
||||||
|
bool wasSplittable = true;
|
||||||
|
State firstState;
|
||||||
|
State secondState;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
double reduceWeights(double** weights, int numberOfCities) {
|
||||||
|
double totalReduced = 0;
|
||||||
|
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
double smallestOnRow = 1e100;
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
if (weights[row][column] < smallestOnRow) {
|
||||||
|
smallestOnRow = weights[row][column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smallestOnRow > 1e50) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalReduced += smallestOnRow;
|
||||||
|
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
weights[row][column] -= smallestOnRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
double smallestOnColumn = 1e100;
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
if (weights[row][column] < smallestOnColumn) {
|
||||||
|
smallestOnColumn = weights[row][column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smallestOnColumn > 1e50) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalReduced += smallestOnColumn;
|
||||||
|
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
weights[row][column] -= smallestOnColumn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalReduced;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, int> findPivotPoint(double** weights, int numberOfCities) {
|
||||||
|
int bestRow = -1;
|
||||||
|
int bestColumn = -1;
|
||||||
|
double bestIncrease = -1;
|
||||||
|
|
||||||
|
for (int testRow = 0; testRow < numberOfCities; testRow++) {
|
||||||
|
for (int testColumn = 0; testColumn < numberOfCities; testColumn++) {
|
||||||
|
// Only look for pivot cells that have a value of zero
|
||||||
|
if (weights[testRow][testColumn] > 0.0001) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double smallestOnRow = 1e100;
|
||||||
|
double smallestOnColumn = 1e100;
|
||||||
|
|
||||||
|
for (int otherColumn = 0; otherColumn < numberOfCities; otherColumn++) {
|
||||||
|
if (otherColumn == testColumn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weights[testRow][otherColumn] < smallestOnRow) {
|
||||||
|
smallestOnRow = weights[testRow][otherColumn];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int otherRow = 0; otherRow < numberOfCities; otherRow++) {
|
||||||
|
if (otherRow == testRow) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weights[otherRow][testColumn] < smallestOnColumn) {
|
||||||
|
smallestOnColumn = weights[otherRow][testColumn];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalIncrease = smallestOnRow + smallestOnColumn;
|
||||||
|
if (totalIncrease > bestIncrease) {
|
||||||
|
bestIncrease = totalIncrease;
|
||||||
|
bestRow = testRow;
|
||||||
|
bestColumn = testColumn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::pair<int, int>(bestRow, bestColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getDegreeOfNode(int nodeNumber, int** adjacencyMatrix, int numberOfCities) {
|
||||||
|
int degree = 0;
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
degree += adjacencyMatrix[nodeNumber][column];
|
||||||
|
}
|
||||||
|
return degree;
|
||||||
|
}
|
||||||
|
|
||||||
|
void multiplyAdjacencyMatrices(int** matrix, int** multiplier, int size) {
|
||||||
|
int** temporary = new int*[size];
|
||||||
|
for (int row = 0; row < size; row++) {
|
||||||
|
temporary[row] = new int[size];
|
||||||
|
for (int column = 0; column < size; column++) {
|
||||||
|
int result = 0;
|
||||||
|
for (int k = 0; k < size; k++) {
|
||||||
|
result += matrix[row][k] * multiplier[k][column];
|
||||||
|
}
|
||||||
|
temporary[row][column] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy into original matrix
|
||||||
|
for (int row = 0; row < size; row++) {
|
||||||
|
for (int column = 0; column < size; column++) {
|
||||||
|
matrix[row][column] = temporary[row][column];
|
||||||
|
}
|
||||||
|
delete [] temporary[row];
|
||||||
|
}
|
||||||
|
delete [] temporary;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disallowSubloops(State &state, int pivotRow, int pivotColumn, int numberOfCities) {
|
||||||
|
// All points with degree one are endpoint
|
||||||
|
int totalDegree = 0;
|
||||||
|
for (int node = 0; node < numberOfCities; node++) {
|
||||||
|
int degree = getDegreeOfNode(node, state.adjacencyMatrix, numberOfCities);
|
||||||
|
totalDegree += degree;
|
||||||
|
if (degree == 2) {
|
||||||
|
for (int otherNode = 0; otherNode < numberOfCities; otherNode++) {
|
||||||
|
state.weights[node][otherNode] = 1e100;
|
||||||
|
state.weights[otherNode][node] = 1e100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this path to the existing paths
|
||||||
|
std::pair<int, int> path(pivotRow, pivotColumn);
|
||||||
|
auto iterator = state.paths.begin();
|
||||||
|
while (iterator != state.paths.end()) {
|
||||||
|
std::pair<int, int> otherPath = *iterator;
|
||||||
|
|
||||||
|
bool expandsPath = false;
|
||||||
|
if (path.first == otherPath.first) {
|
||||||
|
expandsPath = true;
|
||||||
|
path = std::pair<int,int>(path.second, otherPath.second);
|
||||||
|
} else if (path.first == otherPath.second) {
|
||||||
|
expandsPath = true;
|
||||||
|
path = std::pair<int,int>(path.second, otherPath.first);
|
||||||
|
} else if (path.second == otherPath.first) {
|
||||||
|
expandsPath = true;
|
||||||
|
path = std::pair<int,int>(path.first, otherPath.second);
|
||||||
|
} else if (path.second == otherPath.second) {
|
||||||
|
expandsPath = true;
|
||||||
|
path = std::pair<int,int>(path.first, otherPath.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expandsPath) {
|
||||||
|
iterator = state.paths.erase(iterator);
|
||||||
|
} else {
|
||||||
|
iterator++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.paths.push_back(path);
|
||||||
|
|
||||||
|
// There needs to be n edges in a loop, and so the total degree should be 2*n. If we're getting close to this, don't
|
||||||
|
// block of the ability to finish a loop
|
||||||
|
if (state.paths.size() == 1 && totalDegree == 2*(numberOfCities - 1)) {
|
||||||
|
// Finish the loop
|
||||||
|
int startingNode = state.paths.at(0).first;
|
||||||
|
int endingNode = state.paths.at(0).second;
|
||||||
|
|
||||||
|
state.adjacencyMatrix[startingNode][endingNode] = 1;
|
||||||
|
state.adjacencyMatrix[endingNode][startingNode] = 1;
|
||||||
|
|
||||||
|
state.finished = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::pair<int, int> path : state.paths) {
|
||||||
|
state.weights[path.first][path.second] = 1e100;
|
||||||
|
state.weights[path.second][path.first] = 1e100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
State createNewStateOnPivot(int pivotRow, int pivotColumn, State originalState, int numberOfCities) {
|
||||||
|
int** adjacencyMatrixCopy = new int*[numberOfCities];
|
||||||
|
double** weightsCopy = new double*[numberOfCities];
|
||||||
|
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
adjacencyMatrixCopy[row] = new int[numberOfCities];
|
||||||
|
weightsCopy[row] = new double[numberOfCities];
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
adjacencyMatrixCopy[row][column] = originalState.adjacencyMatrix[row][column];
|
||||||
|
weightsCopy[row][column] = originalState.weights[row][column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjacencyMatrixCopy[pivotRow][pivotColumn] = 1;
|
||||||
|
adjacencyMatrixCopy[pivotColumn][pivotRow] = 1;
|
||||||
|
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
weightsCopy[pivotRow][column] = 1e100;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
weightsCopy[row][pivotColumn] = 1e100;
|
||||||
|
}
|
||||||
|
|
||||||
|
weightsCopy[pivotColumn][pivotRow] = 1e100;
|
||||||
|
|
||||||
|
State pivotedState;
|
||||||
|
pivotedState.adjacencyMatrix = adjacencyMatrixCopy;
|
||||||
|
pivotedState.weights = weightsCopy;
|
||||||
|
pivotedState.lowerBound = originalState.lowerBound;
|
||||||
|
for (std::pair<int, int> path : originalState.paths) {
|
||||||
|
pivotedState.paths.push_back(std::pair<int,int>(path.first, path.second));
|
||||||
|
}
|
||||||
|
|
||||||
|
disallowSubloops(pivotedState, pivotRow, pivotColumn, numberOfCities);
|
||||||
|
|
||||||
|
return pivotedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitState splitState(State state, int numberOfCities) {
|
||||||
|
SplitState splitState;
|
||||||
|
|
||||||
|
double reduction = reduceWeights(state.weights, numberOfCities);
|
||||||
|
state.lowerBound += reduction;
|
||||||
|
|
||||||
|
std::pair<int, int> pivotPoint = findPivotPoint(state.weights, numberOfCities);
|
||||||
|
if (pivotPoint.first == -1 || pivotPoint.second == -1) {
|
||||||
|
splitState.wasSplittable = false;
|
||||||
|
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
delete [] state.adjacencyMatrix[row];
|
||||||
|
delete [] state.weights[row];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete [] state.adjacencyMatrix;
|
||||||
|
delete [] state.weights;
|
||||||
|
|
||||||
|
return splitState;
|
||||||
|
}
|
||||||
|
State pivotedState = createNewStateOnPivot(pivotPoint.first, pivotPoint.second, state, numberOfCities);
|
||||||
|
|
||||||
|
// Disallow the pivot in the original state
|
||||||
|
state.weights[pivotPoint.first][pivotPoint.second] = 1e100;
|
||||||
|
|
||||||
|
splitState.firstState = pivotedState;
|
||||||
|
splitState.secondState = state;
|
||||||
|
return splitState;
|
||||||
|
}
|
||||||
|
|
||||||
|
double getActualWeight(int** adjacencyMatrix, double** weights, int numberOfCities) {
|
||||||
|
double totalWeight = 0;
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
totalWeight += adjacencyMatrix[row][column] * weights[row][column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateComparator {
|
||||||
|
public:
|
||||||
|
bool operator()(State one, State two) {
|
||||||
|
return one.lowerBound > two.lowerBound;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<int> findShortestPath(std::vector<std::vector<double>> jsWeights) {
|
||||||
|
|
||||||
|
int numberOfCities = jsWeights.size();
|
||||||
|
|
||||||
|
double** weights = new double*[numberOfCities];
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
weights[row] = new double[numberOfCities];
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
weights[row][column] = jsWeights.at(row).at(column);
|
||||||
|
if (column == row) {
|
||||||
|
weights[row][column] = 1e100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Create a state
|
||||||
|
State initialState;
|
||||||
|
initialState.weights = new double*[numberOfCities];
|
||||||
|
initialState.adjacencyMatrix = new int*[numberOfCities];
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
initialState.weights[row] = new double[numberOfCities];
|
||||||
|
initialState.adjacencyMatrix[row] = new int[numberOfCities];
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
initialState.adjacencyMatrix[row][column] = 0;
|
||||||
|
initialState.weights[row][column] = weights[row][column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initialState.lowerBound = 0;
|
||||||
|
|
||||||
|
std::priority_queue<State, std::vector<State>, StateComparator> queue;
|
||||||
|
queue.push(initialState);
|
||||||
|
|
||||||
|
double currentBest = 1e100;
|
||||||
|
int** bestAdjacenyMatrix = NULL;
|
||||||
|
|
||||||
|
while (!queue.empty()) {
|
||||||
|
State nextState = queue.top();
|
||||||
|
queue.pop();
|
||||||
|
|
||||||
|
if (nextState.lowerBound > currentBest) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextState.finished) {
|
||||||
|
double actualWeight = getActualWeight(nextState.adjacencyMatrix, weights, numberOfCities);
|
||||||
|
if (actualWeight < currentBest) {
|
||||||
|
currentBest = actualWeight;
|
||||||
|
bestAdjacenyMatrix = nextState.adjacencyMatrix;
|
||||||
|
} else {
|
||||||
|
// Delete and clean up memory
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
delete [] nextState.adjacencyMatrix[row];
|
||||||
|
}
|
||||||
|
delete [] nextState.adjacencyMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
delete [] nextState.weights[row];
|
||||||
|
}
|
||||||
|
delete [] nextState.weights;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not finished, split the state and add the new ones to the queue
|
||||||
|
SplitState split = splitState(nextState, numberOfCities);
|
||||||
|
if (split.wasSplittable) {
|
||||||
|
queue.push(split.firstState);
|
||||||
|
queue.push(split.secondState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear up remaining queue for memory
|
||||||
|
while (!queue.empty()) {
|
||||||
|
State stateToDelete = queue.top();
|
||||||
|
queue.pop();
|
||||||
|
|
||||||
|
for (int row = 0; row < numberOfCities; row++) {
|
||||||
|
delete [] stateToDelete.adjacencyMatrix[row];
|
||||||
|
delete [] stateToDelete.weights[row];
|
||||||
|
}
|
||||||
|
delete [] stateToDelete.adjacencyMatrix;
|
||||||
|
delete [] stateToDelete.weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> path;
|
||||||
|
|
||||||
|
int current = 0;
|
||||||
|
int previous = -1;
|
||||||
|
|
||||||
|
path.push_back(current);
|
||||||
|
|
||||||
|
bool firstTime = true;
|
||||||
|
while (current != 0 || firstTime) {
|
||||||
|
firstTime = false;
|
||||||
|
|
||||||
|
for (int column = 0; column < numberOfCities; column++) {
|
||||||
|
if (bestAdjacenyMatrix[current][column] == 1 && column != previous) {
|
||||||
|
previous = current;
|
||||||
|
current = column;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path.push_back(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
EMSCRIPTEN_BINDINGS(my_module) {
|
||||||
|
emscripten::register_vector<double>("WeightsRow");
|
||||||
|
emscripten::register_vector<std::vector<double>>("Weights");
|
||||||
|
emscripten::register_vector<int>("Path");
|
||||||
|
|
||||||
|
emscripten::function("findShortestPath", &findShortestPath);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Generated
+1150
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "orienteering",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Tries to find the shortest path through a set of given points, i.e. solve the traveling salesman problem",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"build:emscripten": "cd native && emcc -O3 -lembind -sEXPORT_ES6 -sASSERTIONS -sEXPORTED_RUNTIME_METHODS=FS -sALLOW_MEMORY_GROWTH -sMAXIMUM_MEMORY=4294967296 --emit-tsd salesman.d.ts -o salesman.js salesman.cpp",
|
||||||
|
"build:normal": "tsc && vite build",
|
||||||
|
"build": "npm run build:emscripten && npm run build:normal",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"@types/terraformer__wkt": "^2.0.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@terraformer/wkt": "^2.2.1",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"haversine-distance": "^1.2.4",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"openrouteservice-js": "^0.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module 'openrouteservice-js';
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import { LatLng } from "leaflet";
|
||||||
|
|
||||||
|
class PPENHandler {
|
||||||
|
ppenDocument: XMLDocument;
|
||||||
|
|
||||||
|
controlCodes: [string, LatLng][];
|
||||||
|
|
||||||
|
constructor(ppenXml: string, xmlXml: string | null) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
this.ppenDocument = parser.parseFromString(ppenXml, "application/xml");
|
||||||
|
let xmlDocument: Document | null;
|
||||||
|
if (xmlXml != null) {
|
||||||
|
xmlDocument = parser.parseFromString(xmlXml, "application/xml");
|
||||||
|
} else {
|
||||||
|
xmlDocument = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, use the xml Document to get a mapping from local coordinates to global coordinates
|
||||||
|
let localAndGlobalPairs: [[number, number], LatLng][] = []
|
||||||
|
if (xmlDocument != null) {
|
||||||
|
Array.from(xmlDocument.getElementsByTagName("Control")).forEach(control => {
|
||||||
|
let position = control.getElementsByTagName("Position").item(0);
|
||||||
|
let mapPosition = control.getElementsByTagName("MapPosition").item(0);
|
||||||
|
|
||||||
|
if (position && mapPosition) {
|
||||||
|
let mapXString = mapPosition.getAttribute("x");
|
||||||
|
let mapYString = mapPosition.getAttribute("y");
|
||||||
|
let latString = position.getAttribute("lat")
|
||||||
|
let lngString = position.getAttribute("lng");
|
||||||
|
|
||||||
|
if (mapXString && mapYString && latString && lngString) {
|
||||||
|
let mapX = parseFloat(mapXString);
|
||||||
|
let mapY = parseFloat(mapYString);
|
||||||
|
let lat = parseFloat(latString);
|
||||||
|
let lng = parseFloat(lngString);
|
||||||
|
|
||||||
|
if (!isNaN(mapX) && !isNaN(mapY) && !isNaN(lat) && !isNaN(lng)) {
|
||||||
|
localAndGlobalPairs.push([[mapX, mapY], new LatLng(lat, lng)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If there is no XML document, just create some positions around 0 latitude, 0 longitude
|
||||||
|
else {
|
||||||
|
Array.from(this.ppenDocument.getElementsByTagName("control")).forEach(control => {
|
||||||
|
let location = control.getElementsByTagName("location").item(0);
|
||||||
|
if (location) {
|
||||||
|
let xString = location.getAttribute("x");
|
||||||
|
let yString = location.getAttribute("y");
|
||||||
|
|
||||||
|
if (xString && yString) {
|
||||||
|
let x = parseFloat(xString);
|
||||||
|
let y = parseFloat(yString);
|
||||||
|
|
||||||
|
if (!isNaN(x) && !isNaN(y)) {
|
||||||
|
let lng = x * 180.0 / (637813.7 * Math.PI);
|
||||||
|
let lat = y * 180.0 / (637813.7 * Math.PI);
|
||||||
|
|
||||||
|
localAndGlobalPairs.push([[x, y], new LatLng(lat, lng)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all control codes
|
||||||
|
this.controlCodes = [];
|
||||||
|
Array.from(this.ppenDocument.getElementsByTagName("control")).forEach(control => {
|
||||||
|
let id = control.getAttribute("id");
|
||||||
|
let location = control.getElementsByTagName("location").item(0);
|
||||||
|
if (id && location) {
|
||||||
|
let xString = location.getAttribute("x");
|
||||||
|
let yString = location.getAttribute("y");
|
||||||
|
|
||||||
|
if (xString && yString) {
|
||||||
|
let x = parseFloat(xString);
|
||||||
|
let y = parseFloat(yString);
|
||||||
|
|
||||||
|
if (!isNaN(x) && !isNaN(y)) {
|
||||||
|
let closestGlobal = localAndGlobalPairs.find(pair => {
|
||||||
|
let otherX = pair[0][0];
|
||||||
|
let otherY = pair[0][1];
|
||||||
|
|
||||||
|
let distance = Math.sqrt(Math.pow(otherX - x, 2) + Math.pow(otherY - y, 2));
|
||||||
|
return distance < 0.1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (closestGlobal) {
|
||||||
|
this.controlCodes.push([id, closestGlobal[1]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
public getControls(): LatLng[] {
|
||||||
|
return this.controlCodes.map(code => code[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLengthOfCourse(course: Element): number {
|
||||||
|
var distance = 0;
|
||||||
|
|
||||||
|
var previousX: number | null = null;
|
||||||
|
var previousY: number | null = null;
|
||||||
|
|
||||||
|
let currentCourseControl = course.getElementsByTagName("first")?.item(0)?.getAttribute("course-control");
|
||||||
|
while (currentCourseControl) {
|
||||||
|
let courseControlElement = Array.from(
|
||||||
|
this.ppenDocument.getElementsByTagName("course-control")
|
||||||
|
).find(element => element.getAttribute("id") == currentCourseControl);
|
||||||
|
|
||||||
|
let controlId = courseControlElement?.getAttribute("control");
|
||||||
|
let controlElement = Array.from(
|
||||||
|
this.ppenDocument.getElementsByTagName("control")
|
||||||
|
).find(element => element.getAttribute("id") == controlId);
|
||||||
|
|
||||||
|
if (courseControlElement && controlId && controlElement) {
|
||||||
|
let currentX = parseFloat(controlElement.getElementsByTagName("location").item(0)?.getAttribute("x") ?? "null");
|
||||||
|
let currentY = parseFloat(controlElement.getElementsByTagName("location").item(0)?.getAttribute("y") ?? "null");
|
||||||
|
|
||||||
|
if (!isNaN(currentX) && !isNaN(currentY)) {
|
||||||
|
if (previousX != null && previousY != null) {
|
||||||
|
distance += Math.sqrt(Math.pow(currentX - previousX, 2) + Math.pow(currentY - previousY, 2));
|
||||||
|
}
|
||||||
|
previousX = currentX;
|
||||||
|
previousY = currentY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCourseControl = courseControlElement?.getElementsByTagName("next").item(0)?.getAttribute("course-control");
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createRoute(path: number[]): void {
|
||||||
|
let courses = Array.from(this.ppenDocument.getElementsByTagName("course"));
|
||||||
|
let longestCourse = courses.reduce((a, b) => this.getLengthOfCourse(a) > this.getLengthOfCourse(b) ? a : b);
|
||||||
|
let courseIds = courses.map(course => parseInt(course.getAttribute("id") ?? "null")).filter(n => !isNaN(n));
|
||||||
|
let courseOrders = courses.map(course => parseInt(course.getAttribute("order") ?? "null")).filter(n => !isNaN(n));
|
||||||
|
|
||||||
|
let courseControls = Array.from(this.ppenDocument.getElementsByTagName("course-control"));
|
||||||
|
let courseControlIds = courseControls.map(control => parseInt(control.getAttribute("id") ?? "null")).filter(n => !isNaN(n));
|
||||||
|
|
||||||
|
let courseId = Math.max(...courseIds) + 1;
|
||||||
|
let courseOrder = Math.max(...courseOrders) + 1;
|
||||||
|
|
||||||
|
let course = this.ppenDocument.createElement("course");
|
||||||
|
course.setAttribute("id", courseId.toString());
|
||||||
|
course.setAttribute("kind", "normal");
|
||||||
|
course.setAttribute("order", courseOrder.toString());
|
||||||
|
|
||||||
|
var name = "Utsetting";
|
||||||
|
var nameCounter = 1;
|
||||||
|
while (courses.find(course => course.getElementsByTagName("name").item(0)?.textContent == name)) {
|
||||||
|
name = "Utsetting" + nameCounter;
|
||||||
|
nameCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nameElement = this.ppenDocument.createElement("name");
|
||||||
|
course.appendChild(nameElement);
|
||||||
|
nameElement.appendChild(this.ppenDocument.createTextNode("Utsetting"));
|
||||||
|
|
||||||
|
let labels = this.ppenDocument.createElement("labels");
|
||||||
|
labels.setAttribute("label-kind", "code");
|
||||||
|
course.appendChild(labels);
|
||||||
|
|
||||||
|
let newCourseControls: Element[] = [];
|
||||||
|
let firstCourseControl = Math.max(...courseControlIds) + 1;
|
||||||
|
|
||||||
|
var currentCourseControl = firstCourseControl;
|
||||||
|
path.forEach(index => {
|
||||||
|
let controlId = this.controlCodes[index]?.[0];
|
||||||
|
if (controlId) {
|
||||||
|
let courseControlElement = this.ppenDocument.createElement("course-control");
|
||||||
|
courseControlElement.setAttribute("id", currentCourseControl.toString());
|
||||||
|
currentCourseControl++;
|
||||||
|
|
||||||
|
courseControlElement.setAttribute("control", controlId);
|
||||||
|
|
||||||
|
let next = this.ppenDocument.createElement("next");
|
||||||
|
next.setAttribute("course-control", currentCourseControl.toString());
|
||||||
|
courseControlElement.appendChild(next);
|
||||||
|
|
||||||
|
let sameControlElement = courseControls.filter(e => e.getAttribute("control") == controlId).find(e => e.getElementsByTagName("number-location").item(0) != null);
|
||||||
|
if (sameControlElement) {
|
||||||
|
let numberLocation = sameControlElement.getElementsByTagName("number-location").item(0);
|
||||||
|
if (numberLocation) {
|
||||||
|
courseControlElement.appendChild(numberLocation.cloneNode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newCourseControls.push(courseControlElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The last course control element shouldn't have a next element
|
||||||
|
let lastElement = newCourseControls[newCourseControls.length - 1];
|
||||||
|
let lastNext = lastElement?.getElementsByTagName("next").item(0);
|
||||||
|
if (lastElement && lastNext) {
|
||||||
|
lastElement.removeChild(lastNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = this.ppenDocument.createElement("first");
|
||||||
|
first.setAttribute("course-control", firstCourseControl.toString());
|
||||||
|
course.appendChild(first);
|
||||||
|
|
||||||
|
let printArea = longestCourse.getElementsByTagName("print-area").item(0);
|
||||||
|
if (printArea) {
|
||||||
|
course.append(printArea.cloneNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = longestCourse.getElementsByTagName("options").item(0);
|
||||||
|
if (options) {
|
||||||
|
course.append(options.cloneNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
longestCourse.parentNode?.insertBefore(course, longestCourse);
|
||||||
|
|
||||||
|
newCourseControls.forEach(control => {
|
||||||
|
var courseControl = null;
|
||||||
|
var controlCounter = 0;
|
||||||
|
while (courseControl == null) {
|
||||||
|
courseControl = courseControls.at(controlCounter);
|
||||||
|
controlCounter += 1;
|
||||||
|
if (controlCounter >= courseControls.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
courseControl?.parentNode?.insertBefore(control, courseControl);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPpenFile(): string {
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
return serializer.serializeToString(this.ppenDocument);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PPENHandler;
|
||||||
+796
@@ -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();
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import L, { LatLng, Marker, Polyline } from "leaflet";
|
||||||
|
import redUrl from '../../static/marker-icon-2x-red.png';
|
||||||
|
import greyUrl from '../../static/marker-icon-2x-grey.png';
|
||||||
|
import shadowUrl from '../../static/marker-shadow.png';
|
||||||
|
import type { GeoJsonObject } from 'geojson';
|
||||||
|
|
||||||
|
const redIcon = new L.Icon({
|
||||||
|
iconUrl: redUrl,
|
||||||
|
shadowUrl: shadowUrl,
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
shadowSize: [41, 41]
|
||||||
|
});
|
||||||
|
|
||||||
|
const greyIcon = new L.Icon({
|
||||||
|
iconUrl: greyUrl,
|
||||||
|
shadowUrl: shadowUrl,
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
shadowSize: [41, 41]
|
||||||
|
});
|
||||||
|
|
||||||
|
interface AddedListener {
|
||||||
|
(latLng: LatLng): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemovedListener {
|
||||||
|
(latLng: LatLng): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DraggedListener {
|
||||||
|
(latLng: LatLng): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarkerClickedListener {
|
||||||
|
(latLng: LatLng): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapHandler {
|
||||||
|
map;
|
||||||
|
clickedPoints: Marker[];
|
||||||
|
addedListeners: AddedListener[] = [];
|
||||||
|
removedListeners: RemovedListener[] = [];
|
||||||
|
markerClickedListeners: MarkerClickedListener[] = [];
|
||||||
|
draggedListeners: DraggedListener[] = [];
|
||||||
|
polyline: Polyline | L.GeoJSON<any, any> | null;
|
||||||
|
|
||||||
|
markerDebounce: Map<Marker, number> = new Map();
|
||||||
|
|
||||||
|
overrideLocked = false;
|
||||||
|
locked = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.clickedPoints = [];
|
||||||
|
this.polyline = null;
|
||||||
|
// Use OpenStreetMaps and center on Oslo
|
||||||
|
this.map = L.map('map', {
|
||||||
|
center: L.latLng(59.92, 10.74),
|
||||||
|
zoom: 13,
|
||||||
|
zoomControl: false
|
||||||
|
});
|
||||||
|
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
}).addTo(this.map);
|
||||||
|
|
||||||
|
this.map.addEventListener('click', e => {
|
||||||
|
if (!this.locked && !this.overrideLocked) {
|
||||||
|
this.addPoint(e.latlng);
|
||||||
|
this.addedListeners.forEach(listener => listener(e.latlng));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClickedPoints(): LatLng[] {
|
||||||
|
return this.clickedPoints.map(marker => marker.getLatLng());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNumberOfPoints(): number {
|
||||||
|
return this.clickedPoints.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addPoint(position: LatLng): Marker {
|
||||||
|
let marker = L.marker(position, {draggable: true, bubblingMouseEvents: false, icon: redIcon});
|
||||||
|
this.clickedPoints.push(marker);
|
||||||
|
marker.addTo(this.map);
|
||||||
|
this.markerDebounce.set(marker, Date.now());
|
||||||
|
|
||||||
|
marker.addEventListener('moveend', _ => {
|
||||||
|
this.polyline?.removeFrom(this.map);
|
||||||
|
this.polyline = null;
|
||||||
|
this.draggedListeners.forEach(listener => listener(marker.getLatLng()));
|
||||||
|
this.markerDebounce.set(marker, Date.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.addEventListener('click', _ => {
|
||||||
|
if (!this.locked && !this.overrideLocked && (!this.markerDebounce.has(marker) || Date.now() - (this.markerDebounce.get(marker) ?? 0) > 500)) {
|
||||||
|
this.polyline?.removeFrom(this.map);
|
||||||
|
this.polyline = null;
|
||||||
|
|
||||||
|
marker.removeFrom(this.map);
|
||||||
|
this.clickedPoints = this.clickedPoints.filter(m => m != marker);
|
||||||
|
this.removedListeners.forEach(listener => listener(marker.getLatLng()));
|
||||||
|
}
|
||||||
|
this.markerClickedListeners.forEach(listener => listener(marker.getLatLng()));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.polyline?.removeFrom(this.map);
|
||||||
|
this.polyline = null;
|
||||||
|
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addPoints(positions: LatLng[]): void {
|
||||||
|
var markers: Marker[] = [];
|
||||||
|
|
||||||
|
positions.forEach(position => {
|
||||||
|
markers.push(this.addPoint(position));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.zoomToExtent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public addPolyLine(coordinates: LatLng[]): void {
|
||||||
|
if (this.polyline != null) {
|
||||||
|
this.polyline.removeFrom(this.map);
|
||||||
|
}
|
||||||
|
this.polyline = L.polyline(coordinates);
|
||||||
|
this.polyline.addTo(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addPolyLineGeojson(geoJson: GeoJsonObject): void {
|
||||||
|
if (this.polyline != null) {
|
||||||
|
this.polyline.removeFrom(this.map);
|
||||||
|
}
|
||||||
|
this.polyline = L.geoJSON(geoJson);
|
||||||
|
this.polyline.addTo(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearPolyLine(): void {
|
||||||
|
if (this.polyline != null) {
|
||||||
|
this.polyline.removeFrom(this.map);
|
||||||
|
this.polyline = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearAll(): void {
|
||||||
|
this.polyline?.removeFrom(this.map);
|
||||||
|
this.polyline = null;
|
||||||
|
|
||||||
|
this.clickedPoints.forEach(m => m.removeFrom(this.map));
|
||||||
|
this.clickedPoints = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyLock(): void {
|
||||||
|
this.clickedPoints.forEach(marker => {
|
||||||
|
marker.dragging?.disable();
|
||||||
|
marker.setIcon(greyIcon);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyUnlock(): void {
|
||||||
|
this.clickedPoints.forEach(marker => {
|
||||||
|
marker.dragging?.enable();
|
||||||
|
marker.setIcon(redIcon);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public lock(): void {
|
||||||
|
this.locked = true;
|
||||||
|
this.applyLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public unlock(): void {
|
||||||
|
this.locked = false;
|
||||||
|
if (!this.overrideLocked) {
|
||||||
|
this.applyUnlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public overrideLock(): void {
|
||||||
|
this.overrideLocked = true;
|
||||||
|
this.applyLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public overrideUnlock(): void {
|
||||||
|
this.overrideLocked = false;
|
||||||
|
if (!this.locked) {
|
||||||
|
this.applyUnlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public showIdPopups(): void {
|
||||||
|
this.clickedPoints.forEach((marker, index) => {
|
||||||
|
marker.bindTooltip((index+1).toString(), {permanent: true, direction: 'right'})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public hideIdPopups(): void {
|
||||||
|
this.clickedPoints.forEach(marker => {
|
||||||
|
marker.closeTooltip();
|
||||||
|
marker.unbindTooltip();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public invalidateSize(): void {
|
||||||
|
this.map.invalidateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public zoomToExtent(): void {
|
||||||
|
this.map.invalidateSize();
|
||||||
|
var group = L.featureGroup(this.clickedPoints);
|
||||||
|
this.map.fitBounds(group.getBounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
public zoomToLatLongs(latLngs: LatLng[]): void {
|
||||||
|
var south = 1e100;
|
||||||
|
var north = -1e100;
|
||||||
|
var west = 1e100;
|
||||||
|
var east = -1e100;
|
||||||
|
|
||||||
|
latLngs.forEach(latLng => {
|
||||||
|
south = Math.min(south, latLng.lat);
|
||||||
|
north = Math.max(north, latLng.lat);
|
||||||
|
east = Math.max(east, latLng.lng);
|
||||||
|
west = Math.min(west, latLng.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
let bounds = L.latLngBounds([south, west], [north, east]);
|
||||||
|
this.map.fitBounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addAddedListener(addedListener: AddedListener): void {
|
||||||
|
this.addedListeners.push(addedListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addRemovedListener(removedListener: RemovedListener): void {
|
||||||
|
this.removedListeners.push(removedListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addDraggedListener(draggedListener: DraggedListener): void {
|
||||||
|
this.draggedListeners.push(draggedListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addMarkerClickedListener(clickedListener: MarkerClickedListener): void {
|
||||||
|
this.markerClickedListeners.push(clickedListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapHandler;
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
: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;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--header-color);
|
||||||
|
-webkit-text-stroke-width: 2px;
|
||||||
|
-webkit-text-stroke-color: var(--header-stroke-color);
|
||||||
|
font-size: 80px;
|
||||||
|
font-family: 'Helvetica';
|
||||||
|
max-height: 10vh;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #333;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
height:60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-button {
|
||||||
|
float: left;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unclickable {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unclickable:hover {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable:hover {
|
||||||
|
background: #ddd;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#distance-popup {
|
||||||
|
float: left;
|
||||||
|
width:250px;
|
||||||
|
display: none;
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: calc(90vh - 130px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#route-type-form {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 90%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th td {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr input {
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unchanged {
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrong {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
height: calc(90vh - 130px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#middle-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 5vh;
|
||||||
|
left: 10vw;
|
||||||
|
width: 80vw;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: solid 2px black;
|
||||||
|
box-shadow: 10px 10px rgba(0, 0, 0, 0.5);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#distance-text-div {
|
||||||
|
position: fixed;
|
||||||
|
top: 150px;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 900;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#distance-text-div p {
|
||||||
|
background-color: rgba(70, 70, 70, 0.7);
|
||||||
|
color: white;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-height: 1100px) {
|
||||||
|
.big-screen {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
height: calc(100vh - 170px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#distance-popup {
|
||||||
|
height: calc(100vh - 170px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#distance-text {
|
||||||
|
top: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { LatLng } from "leaflet";
|
||||||
|
|
||||||
|
export function parseUrl(url: string): [LatLng[] | null, [LatLng, LatLng, number][] | null, number[] | null] {
|
||||||
|
let parsedUrl = URL.parse(url);
|
||||||
|
|
||||||
|
var points: LatLng[] | null = null;
|
||||||
|
var distances: [LatLng, LatLng, number][] | null = null;
|
||||||
|
var path: number[] | null = null;
|
||||||
|
|
||||||
|
if (parsedUrl?.searchParams.has('points')) {
|
||||||
|
points = [];
|
||||||
|
|
||||||
|
let pointsString = parsedUrl.searchParams.get('points');
|
||||||
|
pointsString?.split('-').forEach(pointString => {
|
||||||
|
let splitString = pointString.split(',');
|
||||||
|
if (splitString && splitString.length == 2) {
|
||||||
|
let lat = parseFloat(splitString[0]);
|
||||||
|
let lng = parseFloat(splitString[1]);
|
||||||
|
|
||||||
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
|
points?.push(new LatLng(lat, lng));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (points.length == 0) {
|
||||||
|
points = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedUrl?.searchParams.has('distances')) {
|
||||||
|
distances = [];
|
||||||
|
|
||||||
|
let distancesString = parsedUrl.searchParams.get('distances');
|
||||||
|
distancesString?.split('-').forEach(distanceString => {
|
||||||
|
let splitString = distanceString.split(',');
|
||||||
|
if (splitString && splitString.length == 3) {
|
||||||
|
let from = parseInt(splitString[0]);
|
||||||
|
let to = parseInt(splitString[1]);
|
||||||
|
let distance = parseFloat(splitString[2]);
|
||||||
|
|
||||||
|
if (!isNaN(from) && !isNaN(to) && !isNaN(distance) && points != null && from >= 0 && from < points.length && to >= 0 && to < points.length) {
|
||||||
|
distances?.push([points[from], points[to], distance]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (distances.length == 0) {
|
||||||
|
distances = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedUrl?.searchParams.has('path')) {
|
||||||
|
path = parsedUrl.searchParams.get('path')?.split(',').map(n => parseInt(n)).filter(n => !isNaN(n)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return [points, distances, path];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createUrl(clickedPoints: LatLng[] | null, definedDistances: [LatLng, LatLng, number][] | null, path: number[] | null): string {
|
||||||
|
if (!clickedPoints && !definedDistances && !path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlStrings = []
|
||||||
|
if(clickedPoints) {
|
||||||
|
let pointStrings = clickedPoints.map(latLng => latLng.lat.toFixed(6) + ',' + latLng.lng.toFixed(6));
|
||||||
|
if (pointStrings.length > 0) {
|
||||||
|
urlStrings.push('points=' + pointStrings.join('-'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definedDistances && clickedPoints) {
|
||||||
|
let distancesStrings = [];
|
||||||
|
for (var i = 0; i < clickedPoints.length; i++) {
|
||||||
|
for (var j = i+1; j < clickedPoints.length; j++) {
|
||||||
|
let firstLatLng = clickedPoints[i];
|
||||||
|
let secondLatLng = clickedPoints[j];
|
||||||
|
|
||||||
|
let distance = definedDistances.find(d =>
|
||||||
|
(d[0] == firstLatLng && d[1] == secondLatLng)
|
||||||
|
|| (d[0] == secondLatLng && d[1] == secondLatLng)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance) {
|
||||||
|
distancesStrings.push(i.toString() + ',' + j.toString() + ',' + distance[2].toFixed(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distancesStrings.length > 0) {
|
||||||
|
urlStrings.push('distances=' + distancesStrings.join('-'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path && clickedPoints) {
|
||||||
|
let pathPoints = path.filter(n => n < clickedPoints.length).map(n => n.toString());
|
||||||
|
if (pathPoints.length > 0) {
|
||||||
|
urlStrings.push('path=' + pathPoints.join(','));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlStrings.length > 0) {
|
||||||
|
return '?' + urlStrings.join('&');
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 608 B |
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user