Compare commits

..

No commits in common. "main" and "deployment-stuff" have entirely different histories.

36 changed files with 3654 additions and 2945 deletions

21
.eslintrc.js Normal file
View 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
View file

@ -1,3 +1 @@
node_modules/
.cache/
public/
node_modules/

5
.prettierignore Normal file
View file

@ -0,0 +1,5 @@
.cache
package.json
package-lock.json
public
.vscode/

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": false,
"tabWidth": 2
}

View file

@ -1,15 +1,12 @@
when:
- branch: main
event: push
- event: tag
steps:
- name: deploy
- name: techtransthai-simple-deploy
image: node
commands:
- npm i
- npm run build
- rm -rf /mnt/caddy-sites/tsib.techtransthai.org/*
- cp -r public/* /mnt/caddy-sites/tsib.techtransthai.org/
volumes:
- /media/core/Data1/Apps/caddy/sites:/mnt/caddy-sites

View file

@ -1,6 +1,3 @@
services:
nodejs-app:
build: .
volumes:
- .:/usr/src/app
command: sh -c "npm install && npx nodemon"
build: .

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -39,8 +39,6 @@
"devDependencies": {
"eslint": "^6.8.0",
"eslint-plugin-react": "^7.23.2",
"nodemon": "^3.1.9",
"serve": "^14.2.4",
"prettier": "^1.19.1"
},
"keywords": [
@ -49,10 +47,10 @@
"license": "MIT",
"scripts": {
"build": "gatsby build",
"develop": "npx gatsby develop --host=0.0.0.0",
"develop": "gatsby develop",
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
"start": "npm run develop",
"serve": "gatsby serve --host=0.0.0.0",
"serve": "gatsby serve",
"clean": "gatsby clean"
},
"repository": {

View file

@ -60,7 +60,6 @@ export const MapPlot = React.forwardRef((props, ref) => {
};
const onDefinedPoint = ({ lngLat }) => {
console.log(plotPoints.map(x => x.position));
dispatch(actions.addDefinedPoint(lngLat));
};
const layers = useMemo(() => [

View file

@ -102,6 +102,14 @@ export const MenuSolverControls = ({
{alg.friendlyName}
</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>
{algorithms
.filter(alg => alg.type === "exhaustive")

View 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];
};
```

View 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]
}
}
}
//
// .....
//
}
```

View 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();
}
};
```

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

View 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);
};
```

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

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

View 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);
```

View 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);
}
}
}
}
};
```

View file

@ -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]];
}
}
}
}
};
```

View file

@ -12,6 +12,7 @@ export const useAlgorithmInfo = () => {
in: [
"exhaustive"
"heuristic-construction"
"heuristic-improvement"
]
}
}

View 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);

View 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);

View 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);

View file

@ -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);

View 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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View 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);

View file

@ -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);

View file

@ -1,7 +1,31 @@
import random from "./exhaustive/random.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 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 {
random,
depthFirstSearch,
branchAndBoundOnCost,
branchAndBoundOnCostAndCross,
nearestNeighbor,
arbitraryInsertion,
furthestInsertion,
nearestInsertion,
convexHull,
simulatedAnnealing,
twoOptInversion,
twoOptReciprocalExchange
};

View file

@ -1,28 +1,47 @@
import * as actions from "./actions";
const usTop12 = [
[100.47966551545956, 13.763377348238809],
// [95.99777586767772, 20.659057411016644], //myanmar
// [102.61447417504604, 18.354304323036597], //laos
// [104.82860225771425, 12.260559642146927], //cambodia
[105.61972922924626, 21.337690594700124],
[101.61025278140067, 3.852198947284515],
[106.83275037018893, -6.209465092032497],
[103.81336272752452, 1.3468280345835395],
[114.92250057366596, 4.945297365972065],
[121.00856338015852, 14.602156304250775]
[-73.85835427500902, 40.56507951957753],
[-77.54976052500858, 38.772432514145194],
[-78.91206521250587, 42.66742768420476],
[-70.95796365000933, 42.66742768420476],
[-80.27436990000314, 26.176558881220437],
[-84.4052292750001, 34.108547937473524],
[-82.55952615000031, 28.24770207922181],
[-84.66890115000008, 30.089457425014395],
[-89.89839333750201, 29.746655988569763],
[-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 = {
latitude: 8.880258536666247,
longitude: 113.01211067669622,
latitude: 39.8097343,
longitude: -98.5556199,
zoom: 4
};
const initialState = {
points: usTop12,
points: usTop12.sort(() => Math.random() + 0.5),
viewport: initialViewport,
algorithm: "depthFirstSearch",
algorithm: "convexHull",
delay: 100,
evaluatingDetailLevel: 2,
maxEvaluatingDetailLevel: 2,

View file

@ -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-----

View file

@ -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-----