Compare commits
No commits in common. "main" and "deployment-stuff" have entirely different histories.
main
...
deployment
38 changed files with 3655 additions and 2979 deletions
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: "react-app",
|
||||||
|
globals: {
|
||||||
|
Atomics: "readonly",
|
||||||
|
SharedArrayBuffer: "readonly",
|
||||||
|
__PATH_PREFIX__: true
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
},
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
sourceType: "module"
|
||||||
|
},
|
||||||
|
plugins: ["react"]
|
||||||
|
};
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
.cache/
|
|
||||||
public/
|
|
5
.prettierignore
Normal file
5
.prettierignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.cache
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
public
|
||||||
|
.vscode/
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
|
@ -1,15 +1,12 @@
|
||||||
when:
|
when:
|
||||||
- branch: main
|
- branch: main
|
||||||
event: push
|
|
||||||
- event: tag
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: deploy
|
- name: techtransthai-simple-deploy
|
||||||
image: node
|
image: node
|
||||||
commands:
|
commands:
|
||||||
- npm i
|
- npm i
|
||||||
- npm run build
|
- npm run build
|
||||||
- rm -rf /mnt/caddy-sites/tsib.techtransthai.org/*
|
|
||||||
- cp -r public/* /mnt/caddy-sites/tsib.techtransthai.org/
|
- cp -r public/* /mnt/caddy-sites/tsib.techtransthai.org/
|
||||||
volumes:
|
volumes:
|
||||||
- /media/core/Data1/Apps/caddy/sites:/mnt/caddy-sites
|
- /media/core/Data1/Apps/caddy/sites:/mnt/caddy-sites
|
|
@ -1,9 +1,3 @@
|
||||||
services:
|
services:
|
||||||
nodejs-app:
|
nodejs-app:
|
||||||
image: node:latest
|
build: .
|
||||||
working_dir: /usr/src/app
|
|
||||||
volumes:
|
|
||||||
- .:/usr/src/app
|
|
||||||
command: sh -c "npm install && npx nodemon"
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"watch": ["src"],
|
|
||||||
"ext": "js,css,html,json,jsx",
|
|
||||||
"exec": "npm run build && serve --ssl-cert tsp.test.pem --ssl-key tsp.test-key.pem -s public"
|
|
||||||
}
|
|
||||||
|
|
4766
package-lock.json
generated
4766
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -39,8 +39,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-plugin-react": "^7.23.2",
|
"eslint-plugin-react": "^7.23.2",
|
||||||
"nodemon": "^3.1.9",
|
|
||||||
"serve": "^14.2.4",
|
|
||||||
"prettier": "^1.19.1"
|
"prettier": "^1.19.1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -49,10 +47,10 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "gatsby build",
|
"build": "gatsby build",
|
||||||
"develop": "npx gatsby develop --host=0.0.0.0",
|
"develop": "gatsby develop",
|
||||||
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
|
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
|
||||||
"start": "npm run develop",
|
"start": "npm run develop",
|
||||||
"serve": "gatsby serve --host=0.0.0.0",
|
"serve": "gatsby serve",
|
||||||
"clean": "gatsby clean"
|
"clean": "gatsby clean"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -60,7 +60,6 @@ export const MapPlot = React.forwardRef((props, ref) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDefinedPoint = ({ lngLat }) => {
|
const onDefinedPoint = ({ lngLat }) => {
|
||||||
console.log(plotPoints.map(x => x.position));
|
|
||||||
dispatch(actions.addDefinedPoint(lngLat));
|
dispatch(actions.addDefinedPoint(lngLat));
|
||||||
};
|
};
|
||||||
const layers = useMemo(() => [
|
const layers = useMemo(() => [
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Typography } from "@material-ui/core";
|
|
||||||
|
|
||||||
export const MemoryUsage = () => {
|
|
||||||
const [memoryUsage, setMemoryUsage] = useState("Loading...");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function updateMemoryUsage() {
|
|
||||||
if (performance.memory) {
|
|
||||||
const memoryInfo = performance.memory;
|
|
||||||
const usedMB = (memoryInfo.usedJSHeapSize / 1024 / 1024).toFixed(2);
|
|
||||||
setMemoryUsage(`Used: ${usedMB} MB`);
|
|
||||||
} else {
|
|
||||||
setMemoryUsage("Memory info not supported in this browser.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = setInterval(updateMemoryUsage, 1000);
|
|
||||||
return () => clearInterval(interval); // Cleanup on unmount
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography color="textPrimary" component="div">
|
|
||||||
<h2>Client-Side Memory Usage</h2>
|
|
||||||
<p style={{ fontSize: "1.2em", fontWeight: "bold" }}>{memoryUsage}</p>
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -102,6 +102,14 @@ export const MenuSolverControls = ({
|
||||||
{alg.friendlyName}
|
{alg.friendlyName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
<ListSubheader>Heuristic Improvement</ListSubheader>
|
||||||
|
{algorithms
|
||||||
|
.filter(alg => alg.type === "heuristic-improvement")
|
||||||
|
.map(alg => (
|
||||||
|
<SelectItem value={alg.solverKey} key={alg.solverKey}>
|
||||||
|
{alg.friendlyName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
<ListSubheader>Exhaustive</ListSubheader>
|
<ListSubheader>Exhaustive</ListSubheader>
|
||||||
{algorithms
|
{algorithms
|
||||||
.filter(alg => alg.type === "exhaustive")
|
.filter(alg => alg.type === "exhaustive")
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { Grid, Typography } from "@material-ui/core";
|
||||||
import { MenuItem } from "./MenuItem";
|
import { MenuItem } from "./MenuItem";
|
||||||
import { MenuSection } from "./MenuSection";
|
import { MenuSection } from "./MenuSection";
|
||||||
import { ThemeToggle } from "./ThemeToggle";
|
import { ThemeToggle } from "./ThemeToggle";
|
||||||
import { MemoryUsage } from "./MemoryUsage";
|
|
||||||
|
|
||||||
export const OtherControls = props => {
|
export const OtherControls = props => {
|
||||||
return (
|
return (
|
||||||
|
@ -18,7 +17,6 @@ export const OtherControls = props => {
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</Grid>
|
</Grid>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MemoryUsage />
|
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
99
src/content/exhaustive/branchAndBoundOnCost.md
Normal file
99
src/content/exhaustive/branchAndBoundOnCost.md
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
---
|
||||||
|
type: exhaustive
|
||||||
|
order: 3
|
||||||
|
solverKey: branchAndBoundOnCost
|
||||||
|
friendlyName: Branch and Bound (Cost)
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 2
|
||||||
|
maxEvaluatingDetailLevel: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Branch and Bound on Cost
|
||||||
|
|
||||||
|
This is a recursive algorithm, similar to depth first search, that is guaranteed to find the optimal solution.
|
||||||
|
|
||||||
|
The candidate solution space is generated by systematically traversing possible paths, and discarding large subsets of fruitless candidates by comparing the current solution to an upper and lower bound. In this case, the upper bound is the best path found so far.
|
||||||
|
|
||||||
|
While evaluating paths, if at any point the current solution is already more expensive (longer) than the best complete path discovered, there is no point continuing.
|
||||||
|
|
||||||
|
For example, imagine:
|
||||||
|
|
||||||
|
1. A -> B -> C -> D -> E -> A was already found with a cost of 100.
|
||||||
|
2. We are evaluating A -> C -> E, which has a cost of 110. There is **no point** evaluating the remaining solutions.
|
||||||
|
3. Instead of continuing to evaluate all of the child solutions from here, we can go down a different path, eliminating candidates not worth evaluating:
|
||||||
|
- `A -> C -> E -> D -> B -> A`
|
||||||
|
- `A -> C -> E -> B -> D -> A`
|
||||||
|
|
||||||
|
Implementation is very similar to depth first search, with the exception that we cut paths that are already longer than the current best.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const branchAndBoundOnCost = async (
|
||||||
|
points,
|
||||||
|
path = [],
|
||||||
|
visited = null,
|
||||||
|
overallBest = Infinity
|
||||||
|
) => {
|
||||||
|
if (visited === null) {
|
||||||
|
// initial call
|
||||||
|
path = [points.shift()];
|
||||||
|
points = new Set(points);
|
||||||
|
visited = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out which points are left
|
||||||
|
const available = setDifference(points, visited);
|
||||||
|
|
||||||
|
// calculate the cost, from here, to go home
|
||||||
|
const backToStart = [...path, path[0]];
|
||||||
|
const cost = pathCost(backToStart);
|
||||||
|
|
||||||
|
if (cost > overallBest) {
|
||||||
|
// we may not be done, but have already traveled further than the best path
|
||||||
|
// no reason to continue
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
// still cheaper than the best, keep going deeper, and deeper, and deeper...
|
||||||
|
|
||||||
|
if (available.size === 0) {
|
||||||
|
// at the end of the path, return where we're at
|
||||||
|
return [cost, backToStart];
|
||||||
|
}
|
||||||
|
|
||||||
|
let [bestCost, bestPath] = [null, null];
|
||||||
|
|
||||||
|
// for every point yet to be visited along this path
|
||||||
|
for (const p of available) {
|
||||||
|
// go to that point
|
||||||
|
visited.add(p);
|
||||||
|
path.push(p);
|
||||||
|
|
||||||
|
// RECURSE - go through all the possible points from that point
|
||||||
|
const [curCost, curPath] = await branchAndBoundOnCost(
|
||||||
|
points,
|
||||||
|
path,
|
||||||
|
visited,
|
||||||
|
overallBest
|
||||||
|
);
|
||||||
|
|
||||||
|
// if that path is better and complete, keep it
|
||||||
|
if (curCost && (!bestCost || curCost < bestCost)) {
|
||||||
|
[bestCost, bestPath] = [curCost, curPath];
|
||||||
|
|
||||||
|
if (!overallBest || bestCost < overallBest) {
|
||||||
|
// found a new best complete path
|
||||||
|
overallBest = bestCost;
|
||||||
|
self.setBestPath(bestPath, bestCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go back up and make that point available again
|
||||||
|
visited.delete(p);
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [bestCost, bestPath];
|
||||||
|
};
|
||||||
|
```
|
56
src/content/exhaustive/branchAndBoundOnCostAndCross.md
Normal file
56
src/content/exhaustive/branchAndBoundOnCostAndCross.md
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
type: exhaustive
|
||||||
|
order: 4
|
||||||
|
solverKey: branchAndBoundOnCostAndCross
|
||||||
|
friendlyName: Branch and Bound (Cost, Crossings)
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 2
|
||||||
|
maxEvaluatingDetailLevel: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Branch and Bound (Cost, Intersections)
|
||||||
|
|
||||||
|
This is the same as branch and bound on cost, with an additional heuristic added to further minimize the search space.
|
||||||
|
|
||||||
|
While traversing paths, if at any point the path intersects (crosses over) itself, than backtrack and try the next way. It's been proven that an optimal path will never contain crossings.
|
||||||
|
|
||||||
|
Implementation is almost identical to branch and bound on cost only, with the added heuristic below:
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
const counterClockWise = (p, q, r) => {
|
||||||
|
return (q[0] - p[0]) * (r[1] - q[1]) <
|
||||||
|
(q[1] - p[1]) * (r[0] - q[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersects = (a, b, c, d) => {
|
||||||
|
return counterClockWise(a, c, d) !== counterClockWise(b, c, d) &&
|
||||||
|
counterClockWise(a, b, c) !== counterClockWise(a, b, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchAndBoundOnCostAndCross = async (...) => {
|
||||||
|
//
|
||||||
|
// .....
|
||||||
|
//
|
||||||
|
|
||||||
|
if (path.length > 3) {
|
||||||
|
// if this newly added edge crosses over the existing path,
|
||||||
|
// don't continue. It's been proven that an optimal path will
|
||||||
|
// not cross itself.
|
||||||
|
const newSegment = [
|
||||||
|
path[path.length-2], path[path.length-1]
|
||||||
|
]
|
||||||
|
for (let i=1; i<path.length-2; i++) {
|
||||||
|
if (intersects(path[i], path[i-1], ...newSegment)) {
|
||||||
|
return [null, null]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// .....
|
||||||
|
//
|
||||||
|
}
|
||||||
|
```
|
53
src/content/exhaustive/random.md
Normal file
53
src/content/exhaustive/random.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
type: exhaustive
|
||||||
|
order: 2
|
||||||
|
solverKey: random
|
||||||
|
friendlyName: Random
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Random
|
||||||
|
|
||||||
|
This is an impractical, albeit exhaustive algorithm. It is here only for demonstration purposes, but will not find a reasonable path for traveling salesman problems above 7 or 8 points.
|
||||||
|
|
||||||
|
I consider it exhaustive because if it runs for infinity, eventually it will encounter every possible path.
|
||||||
|
|
||||||
|
1. From the starting path
|
||||||
|
2. Randomly shuffle the path
|
||||||
|
3. If it's better, keep it
|
||||||
|
4. If not, ditch it and keep going
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const random = async points => {
|
||||||
|
let best = Infinity;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// save off the starting point
|
||||||
|
const start = points.shift();
|
||||||
|
|
||||||
|
// sort the remaining points
|
||||||
|
const path = points.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
// put the starting point back
|
||||||
|
path.unshift(start);
|
||||||
|
|
||||||
|
// return to the starting point
|
||||||
|
path.push(start);
|
||||||
|
|
||||||
|
// calculate the new cost
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
if (cost < best) {
|
||||||
|
// we found a better path
|
||||||
|
best = cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get rid of starting point at the end
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
61
src/content/heurisitc-construction/arbitraryInsertion.md
Normal file
61
src/content/heurisitc-construction/arbitraryInsertion.md
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
type: heuristic-construction
|
||||||
|
order: 2
|
||||||
|
solverKey: arbitraryInsertion
|
||||||
|
friendlyName: Arbitrary Insertion
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arbitrary Insertion
|
||||||
|
|
||||||
|
This is a heuristic construction algorithm. It select a random point, and then figures out where the best place to put it will be.
|
||||||
|
|
||||||
|
1. From the starting point
|
||||||
|
2. First, go to the closest point
|
||||||
|
3. Choose a random point to go to
|
||||||
|
4. Find the cheapest place to add it in the path
|
||||||
|
5. Chosen point is no longer an "available point"
|
||||||
|
6. Continue from #3 until there are no available points, and then return to the start.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const arbitraryInsertion = async points => {
|
||||||
|
// from the starting point
|
||||||
|
const path = [points.shift()];
|
||||||
|
|
||||||
|
//
|
||||||
|
// INITIALIZATION - go to the nearest point
|
||||||
|
//
|
||||||
|
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||||
|
path.push(points.pop());
|
||||||
|
|
||||||
|
// randomly sort points - this is the order they will be added
|
||||||
|
// to the path
|
||||||
|
points.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
//
|
||||||
|
// SELECTION - choose a next point randomly
|
||||||
|
//
|
||||||
|
const nextPoint = points.pop();
|
||||||
|
|
||||||
|
//
|
||||||
|
// INSERTION -find the insertion spot that minimizes distance
|
||||||
|
//
|
||||||
|
let [bestCost, bestIdx] = [Infinity, null];
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||||
|
if (insertionCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [insertionCost, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.splice(bestIdx, 0, nextPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return to start after visiting all other points
|
||||||
|
path.push(path[0]);
|
||||||
|
};
|
||||||
|
```
|
111
src/content/heurisitc-construction/convexHull.md
Normal file
111
src/content/heurisitc-construction/convexHull.md
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
---
|
||||||
|
type: heuristic-construction
|
||||||
|
order: 5
|
||||||
|
solverKey: convexHull
|
||||||
|
friendlyName: Convex Hull
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 2
|
||||||
|
maxEvaluatingDetailLevel: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Convex Hull
|
||||||
|
|
||||||
|
This is a heuristic construction algorithm. It starts by building the [convex hull](https://en.wikipedia.org/wiki/Convex_hull), and adding interior points from there. This implmentation uses another heuristic for insertion based on the ratio of the cost of adding the new point to the overall length of the segment, however any insertion algorithm could be applied after building the hull.
|
||||||
|
|
||||||
|
There are a number of algorithms to determine the convex hull. This implementation uses the [gift wrapping algorithm](https://en.wikipedia.org/wiki/Gift_wrapping_algorithm).
|
||||||
|
|
||||||
|
In essence, the steps are:
|
||||||
|
|
||||||
|
1. Determine the leftmost point
|
||||||
|
2. Continually add the most counterclockwise point until the convex hull is formed
|
||||||
|
3. For each remaining point p, find the segment i => j in the hull that minimizes cost(i -> p) + cost(p -> j) - cost(i -> j)
|
||||||
|
4. Of those, choose p that minimizes cost(i -> p -> j) / cost(i -> j)
|
||||||
|
5. Add p to the path between i and j
|
||||||
|
6. Repeat from #3 until there are no remaining points
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const convexHull = async points => {
|
||||||
|
const sp = points[0];
|
||||||
|
|
||||||
|
// Find the "left most point"
|
||||||
|
let leftmost = points[0];
|
||||||
|
for (const p of points) {
|
||||||
|
if (p[1] < leftmost[1]) {
|
||||||
|
leftmost = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = [leftmost];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const curPoint = path[path.length - 1];
|
||||||
|
let [selectedIdx, selectedPoint] = [0, null];
|
||||||
|
|
||||||
|
// find the "most counterclockwise" point
|
||||||
|
for (let [idx, p] of points.entries()) {
|
||||||
|
if (!selectedPoint || orientation(curPoint, p, selectedPoint) === 2) {
|
||||||
|
// this point is counterclockwise with respect to the current hull
|
||||||
|
// and selected point (e.g. more counterclockwise)
|
||||||
|
[selectedIdx, selectedPoint] = [idx, p];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// adding this to the hull so it's no longer available
|
||||||
|
points.splice(selectedIdx, 1);
|
||||||
|
|
||||||
|
// back to the furthest left point, formed a cycle, break
|
||||||
|
if (selectedPoint === leftmost) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to hull
|
||||||
|
path.push(selectedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
|
||||||
|
|
||||||
|
for (let [freeIdx, freePoint] of points.entries()) {
|
||||||
|
// for every free point, find the point in the current path
|
||||||
|
// that minimizes the cost of adding the point minus the cost of
|
||||||
|
// the original segment
|
||||||
|
let [bestCost, bestIdx] = [Infinity, 0];
|
||||||
|
for (let [pathIdx, pathPoint] of path.entries()) {
|
||||||
|
const nextPathPoint = path[(pathIdx + 1) % path.length];
|
||||||
|
|
||||||
|
// the new cost minus the old cost
|
||||||
|
const evalCost =
|
||||||
|
pathCost([pathPoint, freePoint, nextPathPoint]) -
|
||||||
|
pathCost([pathPoint, nextPathPoint]);
|
||||||
|
|
||||||
|
if (evalCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [evalCost, pathIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out how "much" more expensive this is with respect to the
|
||||||
|
// overall length of the segment
|
||||||
|
const nextPoint = path[(bestIdx + 1) % path.length];
|
||||||
|
const prevCost = pathCost([path[bestIdx], nextPoint]);
|
||||||
|
const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
|
||||||
|
const ratio = newCost / prevCost;
|
||||||
|
|
||||||
|
if (ratio < bestRatio) {
|
||||||
|
[bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [nextPoint] = points.splice(bestPointIdx, 1);
|
||||||
|
path.splice(insertIdx, 0, nextPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate the array so that starting point is back first
|
||||||
|
const startIdx = path.findIndex(p => p === sp);
|
||||||
|
path.unshift(...path.splice(startIdx, path.length));
|
||||||
|
|
||||||
|
// go back home
|
||||||
|
path.push(sp);
|
||||||
|
};
|
||||||
|
```
|
73
src/content/heurisitc-construction/furthestInsertion.md
Normal file
73
src/content/heurisitc-construction/furthestInsertion.md
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
type: heuristic-construction
|
||||||
|
order: 4
|
||||||
|
solverKey: furthestInsertion
|
||||||
|
friendlyName: Furthest Insertion
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Furthest Insertion
|
||||||
|
|
||||||
|
This is a heuristic construction algorithm. It selects the furthest point from the path, and then figures out where the best place to put it will be.
|
||||||
|
|
||||||
|
1. From the starting point
|
||||||
|
2. First, go to the closest point
|
||||||
|
3. Choose the point that is furthest from any of the points on the path
|
||||||
|
4. Find the cheapest place to add it in the path
|
||||||
|
5. Chosen point is no longer an "available point"
|
||||||
|
6. Continue from #3 until there are no available points, and then return to the start.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const furthestInsertion = async points => {
|
||||||
|
// from the starting point
|
||||||
|
const path = [points.shift()];
|
||||||
|
|
||||||
|
//
|
||||||
|
// INITIALIZATION - go to the nearest point first
|
||||||
|
//
|
||||||
|
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||||
|
path.push(points.pop());
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
//
|
||||||
|
// SELECTION - furthest point from the path
|
||||||
|
//
|
||||||
|
let [selectedDistance, selectedIdx] = [0, null];
|
||||||
|
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||||
|
// find the minimum distance to the path for freePoint
|
||||||
|
let [bestCostToPath, costToPathIdx] = [Infinity, null];
|
||||||
|
for (const pathPoint of path) {
|
||||||
|
const dist = distance(freePoint, pathPoint);
|
||||||
|
if (dist < bestCostToPath) {
|
||||||
|
[bestCostToPath, costToPathIdx] = [dist, freePointIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this point is further from the path than the currently selected
|
||||||
|
if (bestCostToPath > selectedDistance) {
|
||||||
|
[selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||||
|
|
||||||
|
//
|
||||||
|
// INSERTION - find the insertion spot that minimizes distance
|
||||||
|
//
|
||||||
|
let [bestCost, bestIdx] = [Infinity, null];
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||||
|
if (insertionCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [insertionCost, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.splice(bestIdx, 0, nextPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return to start after visiting all other points
|
||||||
|
path.push(path[0]);
|
||||||
|
};
|
||||||
|
```
|
68
src/content/heurisitc-construction/nearestInsertion.md
Normal file
68
src/content/heurisitc-construction/nearestInsertion.md
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
type: heuristic-construction
|
||||||
|
order: 3
|
||||||
|
solverKey: nearestInsertion
|
||||||
|
friendlyName: Nearest Insertion
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Furthest Insertion
|
||||||
|
|
||||||
|
This is a heuristic construction algorithm. It selects the closest point to the path, and then figures out where the best place to put it will be.
|
||||||
|
|
||||||
|
1. From the starting point
|
||||||
|
2. First, go to the closest point
|
||||||
|
3. Choose the point that is **nearest** to the current path
|
||||||
|
4. Find the cheapest place to add it in the path
|
||||||
|
5. Chosen point is no longer an "available point"
|
||||||
|
6. Continue from #3 until there are no available points, and then return to the start.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const nearestInsertion = async points => {
|
||||||
|
// from the starting point
|
||||||
|
const path = [points.shift()];
|
||||||
|
|
||||||
|
//
|
||||||
|
// INITIALIZATION - go to the nearest point first
|
||||||
|
//
|
||||||
|
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||||
|
path.push(points.pop());
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
//
|
||||||
|
// SELECTION - nearest point to the path
|
||||||
|
//
|
||||||
|
let [selectedDistance, selectedIdx] = [Infinity, null];
|
||||||
|
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||||
|
for (const pathPoint of path) {
|
||||||
|
const dist = distance(freePoint, pathPoint);
|
||||||
|
if (dist < selectedDistance) {
|
||||||
|
[selectedDistance, selectedIdx] = [dist, freePointIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the next point to add
|
||||||
|
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||||
|
|
||||||
|
//
|
||||||
|
// INSERTION - find the insertion spot that minimizes distance
|
||||||
|
//
|
||||||
|
let [bestCost, bestIdx] = [Infinity, null];
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||||
|
if (insertionCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [insertionCost, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.splice(bestIdx, 0, nextPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return to start after visiting all other points
|
||||||
|
path.push(path[0]);
|
||||||
|
};
|
||||||
|
```
|
105
src/content/heurisitc-construction/simulatedAnnealing.md
Normal file
105
src/content/heurisitc-construction/simulatedAnnealing.md
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
---
|
||||||
|
type: heuristic-construction
|
||||||
|
order: 6
|
||||||
|
solverKey: simulatedAnnealing
|
||||||
|
friendlyName: Simulated Annealing
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Simulated Annealing
|
||||||
|
|
||||||
|
Simulated annealing (SA) is a probabilistic technique for approximating the global optimum of a given function. Specifically, it is a metaheuristic to approximate global optimization in a large search space for an optimization problem.
|
||||||
|
|
||||||
|
For problems where finding an approximate global optimum is more important than finding a precise local optimum in a fixed amount of time, simulated annealing may be preferable to exact algorithms
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const simulatedAnnealing = async points => {
|
||||||
|
const sp = points[0];
|
||||||
|
const path = points;
|
||||||
|
|
||||||
|
const tempCoeff =
|
||||||
|
path.length < 10
|
||||||
|
? 1 - 1e-4
|
||||||
|
: path.length < 15
|
||||||
|
? 1 - 1e-5
|
||||||
|
: path.length < 25
|
||||||
|
? 1 - 1e-6
|
||||||
|
: 1 - 5e-7;
|
||||||
|
|
||||||
|
const deltaDistance = (aIdx, bIdx) => {
|
||||||
|
const aPrev = (aIdx - 1 + path.length) % path.length;
|
||||||
|
const aNext = (aIdx + 1 + path.length) % path.length;
|
||||||
|
const bPrev = (bIdx - 1 + path.length) % path.length;
|
||||||
|
const bNext = (bIdx + 1 + path.length) % path.length;
|
||||||
|
let diff =
|
||||||
|
distance(path[bPrev], path[aIdx]) +
|
||||||
|
distance(path[aIdx], path[bNext]) +
|
||||||
|
distance(path[aPrev], path[bIdx]) +
|
||||||
|
distance(path[bIdx], path[aNext]) -
|
||||||
|
distance(path[aPrev], path[aIdx]) -
|
||||||
|
distance(path[aIdx], path[aNext]) -
|
||||||
|
distance(path[bPrev], path[bIdx]) -
|
||||||
|
distance(path[bIdx], path[bNext]);
|
||||||
|
|
||||||
|
if (bPrev === aIdx || bNext === aIdx) {
|
||||||
|
diff += 2 * distance(path[aIdx], path[bIdx]);
|
||||||
|
}
|
||||||
|
return diff;
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePath = temperature => {
|
||||||
|
// 2 random points
|
||||||
|
const a = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||||
|
const b = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||||
|
|
||||||
|
const delta = deltaDistance(a, b);
|
||||||
|
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
|
||||||
|
// swap points
|
||||||
|
[path[a], path[b]] = [path[b], path[a]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialTemp = 100 * distance(path[0], path[1]);
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let temperature = initialTemp;
|
||||||
|
temperature > 1e-6;
|
||||||
|
temperature *= tempCoeff
|
||||||
|
) {
|
||||||
|
changePath(temperature);
|
||||||
|
if (i % 10000 == 0) {
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path, color: EVALUATING_PATH_COLOR }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
if (i % 100000 == 0) {
|
||||||
|
path.push(sp);
|
||||||
|
self.setBestPath(path, pathCost(path));
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate the array so that starting point is back first
|
||||||
|
rotateToStartingPoint(path, sp);
|
||||||
|
|
||||||
|
// go back home
|
||||||
|
path.push(sp);
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(simulatedAnnealing);
|
||||||
|
```
|
60
src/content/heuristic-improvement/twoOptInversion.md
Normal file
60
src/content/heuristic-improvement/twoOptInversion.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
type: heuristic-improvement
|
||||||
|
order: 1
|
||||||
|
solverKey: twoOptInversion
|
||||||
|
friendlyName: Two Opt Inversion
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Two-Opt inversion
|
||||||
|
|
||||||
|
This algorithm is also known as 2-opt, 2-opt mutation, and cross-aversion. The general goal is to find places where the path crosses over itself, and then "undo" that crossing. It repeats until there are no crossings. A characteristic of this algorithm is that afterwards the path is guaranteed to have no crossings.
|
||||||
|
|
||||||
|
1. While a better path has not been found.
|
||||||
|
2. For each pair of points:
|
||||||
|
3. Reverse the path between the selected points.
|
||||||
|
4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
|
||||||
|
5. If not, revert the path and continue searching.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const twoOptInversion = async path => {
|
||||||
|
path.push(path[0]);
|
||||||
|
let best = pathCost(path);
|
||||||
|
let swapped = true;
|
||||||
|
|
||||||
|
while (swapped) {
|
||||||
|
swapped = false;
|
||||||
|
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||||
|
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||||
|
// section of the path to reverse
|
||||||
|
const section = path.slice(pt1, pt2 + 1);
|
||||||
|
|
||||||
|
// reverse section in place
|
||||||
|
section.reverse();
|
||||||
|
|
||||||
|
// replace section of path with reversed section in place
|
||||||
|
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||||
|
|
||||||
|
// calculate new cost
|
||||||
|
const newPath = path;
|
||||||
|
const cost = pathCost(newPath);
|
||||||
|
|
||||||
|
if (cost < best) {
|
||||||
|
// found a better path after the swap, keep it
|
||||||
|
swapped = true;
|
||||||
|
best = cost;
|
||||||
|
self.setBestPath(newPath, best);
|
||||||
|
} else {
|
||||||
|
// un-reverse the section
|
||||||
|
section.reverse();
|
||||||
|
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
type: heuristic-improvement
|
||||||
|
order: 2
|
||||||
|
solverKey: twoOptReciprocalExchange
|
||||||
|
friendlyName: Two Opt Reciprocal Exchange
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Two-Opt Reciprocal Exchange
|
||||||
|
|
||||||
|
This algorithm is similar to the 2-opt mutation or inversion algorithm, although generally will find a less optimal path. However, the computational cost of calculating new solutions is less intensive.
|
||||||
|
|
||||||
|
The big difference with 2-opt mutation is not reversing the path between the 2 points. This algorithm is **not** always going to find a path that doesn't cross itself.
|
||||||
|
|
||||||
|
It could be worthwhile to try this algorithm prior to 2-opt inversion because of the cheaper cost of calculation, but probably not.
|
||||||
|
|
||||||
|
1. While a better path has not been found.
|
||||||
|
2. For each pair of points:
|
||||||
|
3. Swap the points in the path. That is, go to point B before point A, continue along the same path, and go to point A where point B was.
|
||||||
|
4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
|
||||||
|
5. If not, revert the path and continue searching.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const twoOptReciprocalExchange = async path => {
|
||||||
|
path.push(path[0]);
|
||||||
|
let best = pathCost(path);
|
||||||
|
let swapped = true;
|
||||||
|
|
||||||
|
self.setBestPath(path, best);
|
||||||
|
|
||||||
|
while (swapped) {
|
||||||
|
swapped = false;
|
||||||
|
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||||
|
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||||
|
// swap current pair of points
|
||||||
|
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||||
|
|
||||||
|
// calculate new cost
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
if (cost < best) {
|
||||||
|
// found a better path after the swap, keep it
|
||||||
|
swapped = true;
|
||||||
|
best = cost;
|
||||||
|
} else {
|
||||||
|
// swap back - this one's worse
|
||||||
|
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
|
@ -12,6 +12,7 @@ export const useAlgorithmInfo = () => {
|
||||||
in: [
|
in: [
|
||||||
"exhaustive"
|
"exhaustive"
|
||||||
"heuristic-construction"
|
"heuristic-construction"
|
||||||
|
"heuristic-improvement"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
131
src/solvers/exhaustive/branchAndBoundOnCost.worker.js
Normal file
131
src/solvers/exhaustive/branchAndBoundOnCost.worker.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, setDifference } from "../cost";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EVALUATING_PATH_COLOR,
|
||||||
|
EVALUATING_ERROR_COLOR,
|
||||||
|
EVALUATING_SEGMENT_COLOR
|
||||||
|
} from "../../constants";
|
||||||
|
|
||||||
|
const branchAndBoundOnCost = async (
|
||||||
|
points,
|
||||||
|
path = [],
|
||||||
|
visited = null,
|
||||||
|
overallBest = Infinity
|
||||||
|
) => {
|
||||||
|
if (visited === null) {
|
||||||
|
// initial call
|
||||||
|
path = [points.shift()];
|
||||||
|
points = new Set(points);
|
||||||
|
visited = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out which points are left
|
||||||
|
const available = setDifference(points, visited);
|
||||||
|
|
||||||
|
// calculate the cost, from here, to go home
|
||||||
|
const backToStart = [...path, path[0]];
|
||||||
|
const cost = pathCost(backToStart);
|
||||||
|
|
||||||
|
if (cost > overallBest) {
|
||||||
|
// we may not be done, but have already traveled further than the best path
|
||||||
|
// no reason to continue
|
||||||
|
self.setEvaluatingPaths(
|
||||||
|
() => ({
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
path: path.slice(0, path.length - 1),
|
||||||
|
color: EVALUATING_SEGMENT_COLOR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: path.slice(path.length - 2, path.length + 1),
|
||||||
|
color: EVALUATING_ERROR_COLOR
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cost
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
// still cheaper than the best, keep going deeper, and deeper, and deeper...
|
||||||
|
else {
|
||||||
|
self.setEvaluatingPaths(
|
||||||
|
() => ({
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
path: path.slice(0, path.length - 1),
|
||||||
|
color: EVALUATING_SEGMENT_COLOR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: path.slice(path.length - 2, path.length + 1),
|
||||||
|
color: EVALUATING_PATH_COLOR
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cost
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
if (available.size === 0) {
|
||||||
|
// at the end of the path, return where we're at
|
||||||
|
self.setEvaluatingPath(() => ({
|
||||||
|
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [cost, backToStart];
|
||||||
|
}
|
||||||
|
|
||||||
|
let [bestCost, bestPath] = [null, null];
|
||||||
|
|
||||||
|
// for every point yet to be visited along this path
|
||||||
|
for (const p of available) {
|
||||||
|
// go to that point
|
||||||
|
visited.add(p);
|
||||||
|
path.push(p);
|
||||||
|
|
||||||
|
// RECURSE - go through all the possible points from that point
|
||||||
|
const [curCost, curPath] = await branchAndBoundOnCost(
|
||||||
|
points,
|
||||||
|
path,
|
||||||
|
visited,
|
||||||
|
overallBest
|
||||||
|
);
|
||||||
|
|
||||||
|
// if that path is better and complete, keep it
|
||||||
|
if (curCost && (!bestCost || curCost < bestCost)) {
|
||||||
|
[bestCost, bestPath] = [curCost, curPath];
|
||||||
|
|
||||||
|
if (!overallBest || bestCost < overallBest) {
|
||||||
|
// found a new best complete path
|
||||||
|
overallBest = bestCost;
|
||||||
|
self.setBestPath(bestPath, bestCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go back up and make that point available again
|
||||||
|
visited.delete(p);
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
self.setEvaluatingPath(
|
||||||
|
() => ({
|
||||||
|
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
return [bestCost, bestPath];
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(branchAndBoundOnCost);
|
159
src/solvers/exhaustive/branchAndBoundOnCostAndCross.worker.js
Normal file
159
src/solvers/exhaustive/branchAndBoundOnCostAndCross.worker.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, setDifference, intersects } from "../cost";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EVALUATING_PATH_COLOR,
|
||||||
|
EVALUATING_ERROR_COLOR,
|
||||||
|
EVALUATING_SEGMENT_COLOR
|
||||||
|
} from "../../constants";
|
||||||
|
|
||||||
|
const branchAndBoundOnCostAndCross = async (
|
||||||
|
points,
|
||||||
|
path = [],
|
||||||
|
visited = null,
|
||||||
|
overallBest = Infinity
|
||||||
|
) => {
|
||||||
|
if (visited === null) {
|
||||||
|
// initial call
|
||||||
|
path = [points.shift()];
|
||||||
|
points = new Set(points);
|
||||||
|
visited = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out which points are left
|
||||||
|
const available = setDifference(points, visited);
|
||||||
|
|
||||||
|
// calculate the cost, from here, to go home
|
||||||
|
const backToStart = [...path, path[0]];
|
||||||
|
const cost = pathCost(backToStart);
|
||||||
|
|
||||||
|
if (path.length > 3) {
|
||||||
|
// if this newly added edge crosses over the existing path,
|
||||||
|
// don't continue. It's been proven that an optimal path will
|
||||||
|
// not cross itself.
|
||||||
|
const newSegment = [path[path.length - 2], path[path.length - 1]];
|
||||||
|
for (let i = 1; i < path.length - 2; i++) {
|
||||||
|
if (intersects(path[i], path[i - 1], ...newSegment)) {
|
||||||
|
self.setEvaluatingPaths(
|
||||||
|
() => ({
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
path: path.slice(0, path.length - 1),
|
||||||
|
color: EVALUATING_SEGMENT_COLOR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: path.slice(path.length - 2, path.length + 1),
|
||||||
|
color: EVALUATING_ERROR_COLOR
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cost
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cost > overallBest) {
|
||||||
|
// we may not be done, but have already traveled further than the best path
|
||||||
|
// no reason to continue
|
||||||
|
self.setEvaluatingPaths(
|
||||||
|
() => ({
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
path: path.slice(0, path.length - 1),
|
||||||
|
color: EVALUATING_SEGMENT_COLOR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: path.slice(path.length - 2, path.length + 1),
|
||||||
|
color: EVALUATING_ERROR_COLOR
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cost
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
// still cheaper than the best, keep going deeper, and deeper, and deeper...
|
||||||
|
self.setEvaluatingPaths(
|
||||||
|
() => ({
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
path: path.slice(0, path.length - 1),
|
||||||
|
color: EVALUATING_SEGMENT_COLOR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: path.slice(path.length - 2, path.length + 1),
|
||||||
|
color: EVALUATING_PATH_COLOR
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cost
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
if (available.size === 0) {
|
||||||
|
// at the end of the path, return where we're at
|
||||||
|
self.setEvaluatingPath(() => ({
|
||||||
|
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
return [cost, backToStart];
|
||||||
|
}
|
||||||
|
|
||||||
|
let [bestCost, bestPath] = [null, null];
|
||||||
|
|
||||||
|
// for every point yet to be visited along this path
|
||||||
|
for (const p of available) {
|
||||||
|
// go to that point
|
||||||
|
visited.add(p);
|
||||||
|
path.push(p);
|
||||||
|
|
||||||
|
// RECURSE - go through all the possible points from that point
|
||||||
|
const [curCost, curPath] = await branchAndBoundOnCostAndCross(
|
||||||
|
points,
|
||||||
|
path,
|
||||||
|
visited,
|
||||||
|
overallBest
|
||||||
|
);
|
||||||
|
|
||||||
|
// if that path is better and complete, keep it
|
||||||
|
if (curCost && (!bestCost || curCost < bestCost)) {
|
||||||
|
[bestCost, bestPath] = [curCost, curPath];
|
||||||
|
|
||||||
|
if (!overallBest || bestCost < overallBest) {
|
||||||
|
// found a new best complete path
|
||||||
|
overallBest = bestCost;
|
||||||
|
self.setBestPath(bestPath, bestCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// go back up and make that point available again
|
||||||
|
visited.delete(p);
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
self.setEvaluatingPath(
|
||||||
|
() => ({
|
||||||
|
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
return [bestCost, bestPath];
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(branchAndBoundOnCostAndCross);
|
41
src/solvers/exhaustive/random.worker.js
Normal file
41
src/solvers/exhaustive/random.worker.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost } from "../cost";
|
||||||
|
|
||||||
|
const random = async points => {
|
||||||
|
let best = Infinity;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// save off the starting point
|
||||||
|
const start = points.shift();
|
||||||
|
|
||||||
|
// sort the remaining points
|
||||||
|
const path = points.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
// put the starting point back
|
||||||
|
path.unshift(start);
|
||||||
|
|
||||||
|
// return to the starting point
|
||||||
|
path.push(start);
|
||||||
|
|
||||||
|
// calculate the new cost
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
if (cost < best) {
|
||||||
|
// we found a better path
|
||||||
|
best = cost;
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
|
||||||
|
// get rid of starting point at the end
|
||||||
|
path.pop();
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(random);
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, distance } from "../cost";
|
||||||
|
|
||||||
|
const arbitraryInsertion = async points => {
|
||||||
|
// from the starting point
|
||||||
|
const path = [points.shift()];
|
||||||
|
|
||||||
|
//
|
||||||
|
// INITIALIZATION - go to the nearest point first
|
||||||
|
//
|
||||||
|
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||||
|
path.push(points.pop());
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// randomly sort points - this is the order they will be added
|
||||||
|
// to the path
|
||||||
|
points.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
//
|
||||||
|
// SELECTION - choose a next point randomly
|
||||||
|
//
|
||||||
|
const nextPoint = points.pop();
|
||||||
|
|
||||||
|
//
|
||||||
|
// INSERTION - find the insertion spot that minimizes distance
|
||||||
|
//
|
||||||
|
let [bestCost, bestIdx] = [Infinity, null];
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||||
|
if (insertionCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [insertionCost, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.splice(bestIdx, 0, nextPoint);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// return to start after visiting all other points
|
||||||
|
path.push(path[0]);
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(arbitraryInsertion);
|
125
src/solvers/heuristic-construction/convexHull.worker.js
Normal file
125
src/solvers/heuristic-construction/convexHull.worker.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, counterClockWise, rotateToStartingPoint } from "../cost";
|
||||||
|
import {
|
||||||
|
EVALUATING_PATH_COLOR,
|
||||||
|
EVALUATING_SEGMENT_COLOR
|
||||||
|
} from "../../constants";
|
||||||
|
|
||||||
|
const convexHull = async points => {
|
||||||
|
const sp = points[0];
|
||||||
|
|
||||||
|
// Find the "left most point"
|
||||||
|
let leftmost = points[0];
|
||||||
|
for (const p of points) {
|
||||||
|
if (p[1] < leftmost[1]) {
|
||||||
|
leftmost = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = [leftmost];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const curPoint = path[path.length - 1];
|
||||||
|
let [selectedIdx, selectedPoint] = [0, null];
|
||||||
|
|
||||||
|
// find the "most counterclockwise" point
|
||||||
|
for (let [idx, p] of points.entries()) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
self.setEvaluatingPaths(
|
||||||
|
() => ({
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
path: [...path, selectedPoint || curPoint],
|
||||||
|
color: EVALUATING_SEGMENT_COLOR
|
||||||
|
},
|
||||||
|
{ path: [curPoint, p], color: EVALUATING_PATH_COLOR }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
if (!selectedPoint || counterClockWise(curPoint, p, selectedPoint)) {
|
||||||
|
// this point is counterclockwise with respect to the current hull
|
||||||
|
// and selected point (e.g. more counterclockwise)
|
||||||
|
[selectedIdx, selectedPoint] = [idx, p];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// adding this to the hull so it's no longer available
|
||||||
|
points.splice(selectedIdx, 1);
|
||||||
|
|
||||||
|
// back to the furthest left point, formed a cycle, break
|
||||||
|
if (selectedPoint === leftmost) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to hull
|
||||||
|
path.push(selectedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path, color: EVALUATING_PATH_COLOR }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
|
||||||
|
|
||||||
|
for (let [freeIdx, freePoint] of points.entries()) {
|
||||||
|
// for every free point, find the point in the current path
|
||||||
|
// that minimizes the cost of adding the path minus the cost of
|
||||||
|
// the original segment
|
||||||
|
let [bestCost, bestIdx] = [Infinity, 0];
|
||||||
|
for (let [pathIdx, pathPoint] of path.entries()) {
|
||||||
|
const nextPathPoint = path[(pathIdx + 1) % path.length];
|
||||||
|
|
||||||
|
// the new cost minus the old cost
|
||||||
|
const evalCost =
|
||||||
|
pathCost([pathPoint, freePoint, nextPathPoint]) -
|
||||||
|
pathCost([pathPoint, nextPathPoint]);
|
||||||
|
|
||||||
|
if (evalCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [evalCost, pathIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out how "much" more expensive this is with respect to the
|
||||||
|
// overall length of the segment
|
||||||
|
const nextPoint = path[(bestIdx + 1) % path.length];
|
||||||
|
const prevCost = pathCost([path[bestIdx], nextPoint]);
|
||||||
|
const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
|
||||||
|
const ratio = newCost / prevCost;
|
||||||
|
|
||||||
|
if (ratio < bestRatio) {
|
||||||
|
[bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [nextPoint] = points.splice(bestPointIdx, 1);
|
||||||
|
path.splice(insertIdx, 0, nextPoint);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate the array so that starting point is back first
|
||||||
|
rotateToStartingPoint(path, sp);
|
||||||
|
|
||||||
|
// go back home
|
||||||
|
path.push(sp);
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(convexHull);
|
|
@ -0,0 +1,79 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, distance } from "../cost";
|
||||||
|
|
||||||
|
const furthestInsertion = async points => {
|
||||||
|
// from the starting point
|
||||||
|
const path = [points.shift()];
|
||||||
|
|
||||||
|
//
|
||||||
|
// INITIALIZATION - go to the nearest point first
|
||||||
|
//
|
||||||
|
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||||
|
path.push(points.pop());
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
//
|
||||||
|
// SELECTION - furthest point from the path
|
||||||
|
//
|
||||||
|
let [selectedDistance, selectedIdx] = [0, null];
|
||||||
|
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||||
|
// find the minimum distance to the path for freePoint
|
||||||
|
let [bestCostToPath, costToPathIdx] = [Infinity, null];
|
||||||
|
for (const pathPoint of path) {
|
||||||
|
const dist = distance(freePoint, pathPoint);
|
||||||
|
if (dist < bestCostToPath) {
|
||||||
|
[bestCostToPath, costToPathIdx] = [dist, freePointIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this point is further from the path than the currently selected
|
||||||
|
if (bestCostToPath > selectedDistance) {
|
||||||
|
[selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the next point to add
|
||||||
|
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||||
|
|
||||||
|
//
|
||||||
|
// INSERTION - find the insertion spot that minimizes distance
|
||||||
|
//
|
||||||
|
let [bestCost, bestIdx] = [Infinity, null];
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||||
|
if (insertionCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [insertionCost, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.splice(bestIdx, 0, nextPoint);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// return to start after visiting all other points
|
||||||
|
path.push(path[0]);
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(furthestInsertion);
|
|
@ -0,0 +1,72 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, distance } from "../cost";
|
||||||
|
|
||||||
|
const nearestInsertion = async points => {
|
||||||
|
// from the starting point
|
||||||
|
const path = [points.shift()];
|
||||||
|
|
||||||
|
//
|
||||||
|
// INITIALIZATION - go to the nearest point first
|
||||||
|
//
|
||||||
|
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||||
|
path.push(points.pop());
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
//
|
||||||
|
// SELECTION - nearest point to the path
|
||||||
|
//
|
||||||
|
let [selectedDistance, selectedIdx] = [Infinity, null];
|
||||||
|
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||||
|
for (const pathPoint of path) {
|
||||||
|
const dist = distance(freePoint, pathPoint);
|
||||||
|
if (dist < selectedDistance) {
|
||||||
|
[selectedDistance, selectedIdx] = [dist, freePointIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the next point to add
|
||||||
|
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||||
|
|
||||||
|
//
|
||||||
|
// INSERTION - find the insertion spot that minimizes distance
|
||||||
|
//
|
||||||
|
let [bestCost, bestIdx] = [Infinity, null];
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||||
|
if (insertionCost < bestCost) {
|
||||||
|
[bestCost, bestIdx] = [insertionCost, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.splice(bestIdx, 0, nextPoint);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// return to start after visiting all other points
|
||||||
|
path.push(path[0]);
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(nearestInsertion);
|
|
@ -0,0 +1,90 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, distance, rotateToStartingPoint } from "../cost";
|
||||||
|
import { EVALUATING_PATH_COLOR } from "../../constants";
|
||||||
|
|
||||||
|
const simulatedAnnealing = async points => {
|
||||||
|
const sp = points[0];
|
||||||
|
const path = points;
|
||||||
|
|
||||||
|
const tempCoeff =
|
||||||
|
path.length < 10
|
||||||
|
? 1 - 1e-4
|
||||||
|
: path.length < 15
|
||||||
|
? 1 - 1e-5
|
||||||
|
: path.length < 30
|
||||||
|
? 1 - 1e-6
|
||||||
|
: 1 - 5e-7;
|
||||||
|
|
||||||
|
const deltaDistance = (aIdx, bIdx) => {
|
||||||
|
const aPrev = (aIdx - 1 + path.length) % path.length;
|
||||||
|
const aNext = (aIdx + 1 + path.length) % path.length;
|
||||||
|
const bPrev = (bIdx - 1 + path.length) % path.length;
|
||||||
|
const bNext = (bIdx + 1 + path.length) % path.length;
|
||||||
|
let diff =
|
||||||
|
distance(path[bPrev], path[aIdx]) +
|
||||||
|
distance(path[aIdx], path[bNext]) +
|
||||||
|
distance(path[aPrev], path[bIdx]) +
|
||||||
|
distance(path[bIdx], path[aNext]) -
|
||||||
|
distance(path[aPrev], path[aIdx]) -
|
||||||
|
distance(path[aIdx], path[aNext]) -
|
||||||
|
distance(path[bPrev], path[bIdx]) -
|
||||||
|
distance(path[bIdx], path[bNext]);
|
||||||
|
|
||||||
|
if (bPrev === aIdx || bNext === aIdx) {
|
||||||
|
diff += 2 * distance(path[aIdx], path[bIdx]);
|
||||||
|
}
|
||||||
|
return diff;
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePath = temperature => {
|
||||||
|
// 2 random points
|
||||||
|
const a = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||||
|
const b = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||||
|
|
||||||
|
const delta = deltaDistance(a, b);
|
||||||
|
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
|
||||||
|
// swap points
|
||||||
|
[path[a], path[b]] = [path[b], path[a]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialTemp = 100 * distance(path[0], path[1]);
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (
|
||||||
|
let temperature = initialTemp;
|
||||||
|
temperature > 1e-6;
|
||||||
|
temperature *= tempCoeff
|
||||||
|
) {
|
||||||
|
changePath(temperature);
|
||||||
|
if (i % 10000 == 0) {
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path, color: EVALUATING_PATH_COLOR }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
if (i % 100000 == 0) {
|
||||||
|
path.push(sp);
|
||||||
|
self.setBestPath(path, pathCost(path));
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate the array so that starting point is back first
|
||||||
|
rotateToStartingPoint(path, sp);
|
||||||
|
|
||||||
|
// go back home
|
||||||
|
path.push(sp);
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(simulatedAnnealing);
|
72
src/solvers/heuristic-improvement/twoOptInversion.worker.js
Normal file
72
src/solvers/heuristic-improvement/twoOptInversion.worker.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost } from "../cost";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EVALUATING_PATH_COLOR,
|
||||||
|
EVALUATING_SEGMENT_COLOR
|
||||||
|
} from "../../constants";
|
||||||
|
|
||||||
|
const twoOptInversion = async path => {
|
||||||
|
path.push(path[0]);
|
||||||
|
let best = pathCost(path);
|
||||||
|
let swapped = true;
|
||||||
|
|
||||||
|
self.setBestPath(path, best);
|
||||||
|
|
||||||
|
while (swapped) {
|
||||||
|
swapped = false;
|
||||||
|
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||||
|
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||||
|
// section of the path to reverse
|
||||||
|
const section = path.slice(pt1, pt2 + 1);
|
||||||
|
|
||||||
|
// reverse section in place
|
||||||
|
section.reverse();
|
||||||
|
|
||||||
|
// replace section of path with reversed section in place
|
||||||
|
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||||
|
|
||||||
|
// calculate new cost
|
||||||
|
const newPath = path;
|
||||||
|
const cost = pathCost(newPath);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [
|
||||||
|
{ path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
{ path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
{ path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
{
|
||||||
|
path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
|
||||||
|
color: EVALUATING_PATH_COLOR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
|
||||||
|
color: EVALUATING_PATH_COLOR
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
if (cost < best) {
|
||||||
|
// found a better path after the swap, keep it
|
||||||
|
swapped = true;
|
||||||
|
best = cost;
|
||||||
|
self.setBestPath(newPath, best);
|
||||||
|
} else {
|
||||||
|
// un-reverse the section
|
||||||
|
section.reverse();
|
||||||
|
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path, color: EVALUATING_SEGMENT_COLOR }]
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(twoOptInversion);
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost } from "../cost";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EVALUATING_PATH_COLOR,
|
||||||
|
EVALUATING_SEGMENT_COLOR
|
||||||
|
} from "../../constants";
|
||||||
|
|
||||||
|
const twoOptReciprocalExchange = async path => {
|
||||||
|
path.push(path[0]);
|
||||||
|
let best = pathCost(path);
|
||||||
|
let swapped = true;
|
||||||
|
|
||||||
|
self.setBestPath(path, best);
|
||||||
|
|
||||||
|
while (swapped) {
|
||||||
|
swapped = false;
|
||||||
|
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||||
|
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||||
|
// swap current pair of points
|
||||||
|
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||||
|
|
||||||
|
// calculate new cost
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [
|
||||||
|
{ path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
{ path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
{ path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
|
||||||
|
{
|
||||||
|
path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
|
||||||
|
color: EVALUATING_PATH_COLOR
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
|
||||||
|
color: EVALUATING_PATH_COLOR
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
if (cost < best) {
|
||||||
|
// found a better path after the swap, keep it
|
||||||
|
swapped = true;
|
||||||
|
best = cost;
|
||||||
|
self.setBestPath(path, best);
|
||||||
|
} else {
|
||||||
|
// swap back - this one's worse
|
||||||
|
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setEvaluatingPath(() => ({
|
||||||
|
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(twoOptReciprocalExchange);
|
|
@ -1,7 +1,31 @@
|
||||||
|
import random from "./exhaustive/random.worker";
|
||||||
import depthFirstSearch from "./exhaustive/depthFirstSearch.worker";
|
import depthFirstSearch from "./exhaustive/depthFirstSearch.worker";
|
||||||
|
import branchAndBoundOnCost from "./exhaustive/branchAndBoundOnCost.worker";
|
||||||
|
import branchAndBoundOnCostAndCross from "./exhaustive/branchAndBoundOnCostAndCross.worker";
|
||||||
|
|
||||||
import nearestNeighbor from "./heuristic-construction/nearestNeighbor.worker";
|
import nearestNeighbor from "./heuristic-construction/nearestNeighbor.worker";
|
||||||
|
import arbitraryInsertion from "./heuristic-construction/arbitraryInsertion.worker";
|
||||||
|
import nearestInsertion from "./heuristic-construction/nearestInsertion.worker";
|
||||||
|
import furthestInsertion from "./heuristic-construction/furthestInsertion.worker";
|
||||||
|
import convexHull from "./heuristic-construction/convexHull.worker";
|
||||||
|
import simulatedAnnealing from "./heuristic-construction/simulatedAnnealing.worker";
|
||||||
|
|
||||||
|
import twoOptInversion from "./heuristic-improvement/twoOptInversion.worker";
|
||||||
|
import twoOptReciprocalExchange from "./heuristic-improvement/twoOptReciprocalExchange.worker";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
random,
|
||||||
depthFirstSearch,
|
depthFirstSearch,
|
||||||
|
branchAndBoundOnCost,
|
||||||
|
branchAndBoundOnCostAndCross,
|
||||||
|
|
||||||
nearestNeighbor,
|
nearestNeighbor,
|
||||||
|
arbitraryInsertion,
|
||||||
|
furthestInsertion,
|
||||||
|
nearestInsertion,
|
||||||
|
convexHull,
|
||||||
|
simulatedAnnealing,
|
||||||
|
|
||||||
|
twoOptInversion,
|
||||||
|
twoOptReciprocalExchange
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,47 @@
|
||||||
import * as actions from "./actions";
|
import * as actions from "./actions";
|
||||||
|
|
||||||
const usTop12 = [
|
const usTop12 = [
|
||||||
[100.47966551545956, 13.763377348238809],
|
[-73.85835427500902, 40.56507951957753],
|
||||||
// [95.99777586767772, 20.659057411016644], //myanmar
|
[-77.54976052500858, 38.772432514145194],
|
||||||
// [102.61447417504604, 18.354304323036597], //laos
|
[-78.91206521250587, 42.66742768420476],
|
||||||
// [104.82860225771425, 12.260559642146927], //cambodia
|
[-70.95796365000933, 42.66742768420476],
|
||||||
[105.61972922924626, 21.337690594700124],
|
[-80.27436990000314, 26.176558881220437],
|
||||||
[101.61025278140067, 3.852198947284515],
|
[-84.4052292750001, 34.108547937473524],
|
||||||
[106.83275037018893, -6.209465092032497],
|
[-82.55952615000031, 28.24770207922181],
|
||||||
[103.81336272752452, 1.3468280345835395],
|
[-84.66890115000008, 30.089457425014395],
|
||||||
[114.92250057366596, 4.945297365972065],
|
[-89.89839333750201, 29.746655988569763],
|
||||||
[121.00856338015852, 14.602156304250775]
|
[-96.62202615000125, 32.640688397241334],
|
||||||
|
[-95.3036667750014, 29.287759374472813],
|
||||||
|
[-97.76460427500368, 30.089457425014395],
|
||||||
|
[-101.89546365000065, 34.97727964358472],
|
||||||
|
[-112.22261208749687, 33.23080293029681],
|
||||||
|
[-111.38765114999953, 35.01327961148759],
|
||||||
|
[-115.56245583750162, 36.08588188690158],
|
||||||
|
[-118.63862771249869, 33.999320468363095],
|
||||||
|
[-117.2323777124963, 32.97311239658548],
|
||||||
|
[-123.12104958749816, 38.222145234071036],
|
||||||
|
[-124.26362771250061, 41.13019627380825],
|
||||||
|
[-120.13276833749595, 39.72528830651809],
|
||||||
|
[-111.82710427499693, 41.13019627380825],
|
||||||
|
[-105.2353073999977, 39.961475963760066],
|
||||||
|
[-87.43745583749975, 41.69048709677229],
|
||||||
|
[-93.1064011499991, 45.29144400095841],
|
||||||
|
[-90.20601052499944, 38.772432514145194],
|
||||||
|
[-117.27632302500142, 47.50341272285311],
|
||||||
|
[-122.72554177499823, 45.8757982618686],
|
||||||
|
[-122.81343240000076, 48.152468818056875]
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialViewport = {
|
const initialViewport = {
|
||||||
latitude: 8.880258536666247,
|
latitude: 39.8097343,
|
||||||
longitude: 113.01211067669622,
|
longitude: -98.5556199,
|
||||||
zoom: 4
|
zoom: 4
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
points: usTop12,
|
points: usTop12.sort(() => Math.random() + 0.5),
|
||||||
viewport: initialViewport,
|
viewport: initialViewport,
|
||||||
algorithm: "depthFirstSearch",
|
algorithm: "convexHull",
|
||||||
delay: 100,
|
delay: 100,
|
||||||
evaluatingDetailLevel: 2,
|
evaluatingDetailLevel: 2,
|
||||||
maxEvaluatingDetailLevel: 2,
|
maxEvaluatingDetailLevel: 2,
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDKNFnAZsFXsOg4
|
|
||||||
UFXFSIvG/9P60FQLqoeUwmf87f2dWUSwbeX36alE+SjJt4MFipdnuMe5OzGrTdvq
|
|
||||||
6PGwt70aLI8Ox6QPtk3/2AL7EmCXesgJ5V1kpf0WLZX8j2vE7q7FK+lG/MYXvnJ9
|
|
||||||
5FDGWMuzyPoyu5LU/dQwwTYAPMZRUm/bis/yYh/310Hr7JYoWRVGd7HiSRIKMOKZ
|
|
||||||
CZ5IHReAwpXYPjvvbuULnxDbjULquMfDbQUsuWJexMEoEHIof2/ZXfxSH6PfMRaX
|
|
||||||
zKz5lxsW8BjV5RoejUjSDyxSsOlM0vh3eVMBZFQDCbEzlbw0D9aSIVEbNwUbUYj5
|
|
||||||
pBPTWLtTAgMBAAECggEBAI+Arb21bzu7ymLE6Mo1XTXis9+J4EvTP5uciM5hXJ9C
|
|
||||||
DlSj+hSCmOXhakgWW/8fx6oN4nicAPkRLaU+ouCG1cbwnqqfltiryhlrhVoIRdLb
|
|
||||||
iYI0bJ6UitQlkA+I/bPqrNA0BL+jfza0q26bDZRmylKSrLY6ls9gQSpExP1QJHLr
|
|
||||||
KobE0xAkR2T/ShOdngwXhYZphnNimnDoJvBbl9ptEtfDevLr39Z5HJkGbguT+6Sf
|
|
||||||
eNu9Z9LHOMjjpvqa59XdSCsRRzOvuQsoPXJmkechbM1D+eZ/oAiOScRjytLbkUYH
|
|
||||||
YBla7zmdMah6A9FOEylZAQT9tZjwsuRKrxaJpoB3TnECgYEA31UmTDJE5r5mpiOl
|
|
||||||
FIS6COsFGc58wopvjlpxopgrE9JwKckyy9ds6YWGlzJJpxBopZ/NA0f5dzzTOUz1
|
|
||||||
pGEoGdE70hefINQBF01x5MQ/JW0SjY9d4uLkrC8RHad+HoiSOM7pL61V/AtQk1Oi
|
|
||||||
9jhfdImBM9i2e2Txb3WLO1+VCGkCgYEA58gLFds1deTum32729/h29x2fd94dmHs
|
|
||||||
46Da4OYbGc7ngGTkh+hyg07+gbJ0q8AvPm3Ot6Xoz4dvqxSeDptfvC96yNnDRx2y
|
|
||||||
NZRAker3GWtx5mMoUSw5M73nfUqjZjFg7ichCPoxt4DybdNBq/UjY3hkhiF+FuzL
|
|
||||||
5+gUewaBDlsCgYEAnfVqxf/UDePjVGTnsJCDyCT6EZujUDF735KGxvqblUSFAnkE
|
|
||||||
zXoL1UsUu8HcqCYJ7gMNjOGOR1ClEOUm5GG3bDM5/Umpyh1IvEORZ72J8B2qPqeF
|
|
||||||
PyE9na8YiwHZSR1NVpK6CXeu1jrmfZ1tKHsMwK80zAfeYX4u6aeYl6DuFukCgYAO
|
|
||||||
IehrIL6Vvau+11/I/FGtMjgXXLTfowDqsDgoVl94p2D+NyioEMhKsVpbViI/Bqza
|
|
||||||
xZ9BG2CipsNsTwmEIn0n4E1ASebaQzlGgw+c1hLS/fYn8gvXRzcFrKKcxRxJcuFS
|
|
||||||
JBijj33QjpA5mhP7BCtwOTsH4qrpgu676S62gaME2QKBgQCanh8rBB2j7bCVdEoL
|
|
||||||
BXCs6OBJllbEPDZtgdECAcMYpaskwWrsHT5638uxLAVQsH42+bCzKfYLwcQqZK8r
|
|
||||||
FIJESgR+Ud6/M5J3s6IGxWGSJ49VYWQDOYd9SiWtGxX1JykzGx55J7P4yba3rNO/
|
|
||||||
ZnMxIBIcFEcgkUWSDzUjw6Gnvg==
|
|
||||||
-----END PRIVATE KEY-----
|
|
24
tsp.test.pem
24
tsp.test.pem
|
@ -1,24 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIID8zCCAlugAwIBAgIQeylEAT5Jp8jAylxRtHypdzANBgkqhkiG9w0BAQsFADBP
|
|
||||||
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExEjAQBgNVBAsMCXJvb3RA
|
|
||||||
a2FsaTEZMBcGA1UEAwwQbWtjZXJ0IHJvb3RAa2FsaTAeFw0yNTAyMjgxNzE0MjJa
|
|
||||||
Fw0yNzA1MjgxNjE0MjJaMD0xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj
|
|
||||||
ZXJ0aWZpY2F0ZTESMBAGA1UECwwJcm9vdEBrYWxpMIIBIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAQ8AMIIBCgKCAQEAyjRZwGbBV7DoOFBVxUiLxv/T+tBUC6qHlMJn/O39nVlE
|
|
||||||
sG3l9+mpRPkoybeDBYqXZ7jHuTsxq03b6ujxsLe9GiyPDsekD7ZN/9gC+xJgl3rI
|
|
||||||
CeVdZKX9Fi2V/I9rxO6uxSvpRvzGF75yfeRQxljLs8j6MruS1P3UMME2ADzGUVJv
|
|
||||||
24rP8mIf99dB6+yWKFkVRnex4kkSCjDimQmeSB0XgMKV2D47727lC58Q241C6rjH
|
|
||||||
w20FLLliXsTBKBByKH9v2V38Uh+j3zEWl8ys+ZcbFvAY1eUaHo1I0g8sUrDpTNL4
|
|
||||||
d3lTAWRUAwmxM5W8NA/WkiFRGzcFG1GI+aQT01i7UwIDAQABo10wWzAOBgNVHQ8B
|
|
||||||
Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAU9Dqj2NAN
|
|
||||||
hBbYm6GrIOwnbF6U01owEwYDVR0RBAwwCoIIdHNwLnRlc3QwDQYJKoZIhvcNAQEL
|
|
||||||
BQADggGBAFGlI1vrNiqDJDY00C5QUU9Yi6Vz+IUfbSfwAK4jgV0l1My4/S8jMZn0
|
|
||||||
MTTYHCRatsDfBgwgoTbZnC9tHI5aU+rXXcgERG7bhQNuyTWIFUu+ZGBxXQDIMC/w
|
|
||||||
a++4ZpZsOAdHS5gwTL0qBZxER7bwfjhvsmweA8/2RizRQg4+r/byfIwryyPh9AAT
|
|
||||||
KnH96gymaboWpVJgf+5BEHihI8i/hQzDf2NLo3GTYrsV/dOS/xmkfnr1O7uacFCo
|
|
||||||
k5/Or1LIkogKVGSnjtCQci5fGJkfRiVrlbDYVppjHPBsqArdugKAUBblcAV/2Qq4
|
|
||||||
kjwupnrkCqNolN7lZ6Pj6Nu1aOkZCuLcByfoE6QSwS4uecFN6rjSFjnZHNYvlorV
|
|
||||||
6qeRRGjfYD9QrsergpnZ0Iln/U4+ixEkQHcqLcP3xXtH8IOane1eJgy3kJXMCTYT
|
|
||||||
2XfnnAb4FoenEZ3sGItrDmxsjR0NO+Q76I1tGKvaQ/8QBhMzcZ62fkaqOJwAW8EJ
|
|
||||||
SDKxtQ2McA==
|
|
||||||
-----END CERTIFICATE-----
|
|
Loading…
Add table
Reference in a new issue