setCoordinates(e.target.value)}
64 | multiline
65 | rows={10}
66 | fullWidth
67 | variant="outlined"
68 | color="secondary"
69 | />
70 |
71 |
77 |
82 |
83 | >
84 | );
85 | };
86 |
87 | export default CoordinateInput;
--------------------------------------------------------------------------------
/src/components/InformationModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/styles";
3 | import {
4 | Dialog,
5 | DialogContent,
6 | IconButton,
7 | useMediaQuery,
8 | useTheme
9 | } from "@material-ui/core";
10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
11 | import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
12 |
13 | const useStyles = makeStyles(theme => ({
14 | closeButton: {
15 | position: "absolute",
16 | right: theme.spacing(1),
17 | top: theme.spacing(1),
18 | color: theme.palette.grey[500]
19 | },
20 | root: {
21 | zIndex: "10000 !important"
22 | }
23 | }));
24 |
25 | export const InformationModal = ({ open, onClose, children }) => {
26 | const classes = useStyles();
27 | const theme = useTheme();
28 | const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
29 |
30 | return (
31 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/IntroductionModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 |
4 | import * as selectors from "../store/selectors";
5 | import * as actions from "../store/actions";
6 | import { useIntroductionInfo } from "../hooks";
7 |
8 | import { InformationModal } from "./InformationModal";
9 |
10 | export const IntroductionModal = props => {
11 | const dispatch = useDispatch();
12 | const introduction = useIntroductionInfo();
13 | const open = useSelector(selectors.selectSiteInfoOpen);
14 |
15 | const onClose = () => {
16 | dispatch(actions.toggleSiteInfoOpen());
17 | };
18 |
19 | return (
20 |
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/styles";
3 |
4 | const useStyles = makeStyles(theme => ({
5 | container: {
6 | height: "100vh",
7 | display: "flex",
8 | overflow: "hidden",
9 | flexDirection: "row"
10 | },
11 | [theme.breakpoints.down("sm")]: {
12 | container: {
13 | justifyContent: "flex-end",
14 | flexDirection: "column-reverse"
15 | }
16 | }
17 | }));
18 |
19 | export const Layout = ({ children }) => {
20 | const classes = useStyles();
21 |
22 | return {children}
;
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/MapPlot.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useImperativeHandle, useEffect, useMemo } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { useMediaQuery } from "@material-ui/core";
4 | import MapGL from "react-map-gl";
5 | import "mapbox-gl/dist/mapbox-gl.css";
6 | import DeckGL from '@deck.gl/react';
7 | import { ScatterplotLayer, PathLayer } from "@deck.gl/layers";
8 | import { LinearProgress } from "@material-ui/core";
9 | import * as actions from "../store/actions";
10 | import * as selectors from "../store/selectors";
11 | import { useThemeContext } from "../context";
12 |
13 | // not secret
14 | const TOKEN =
15 | "pk.eyJ1IjoiaW50cmVwaWRldiIsImEiOiJjazBpa2M5YnowMHcyM21ubzgycW8zZHJmIn0.DCO2aRA6MJweC8HN-d_cgQ";
16 |
17 | export const MapPlot = React.forwardRef((props, ref) => {
18 | const { children } = props;
19 | const { muiTheme, colorMode } = useThemeContext();
20 | const matches = useMediaQuery(muiTheme.breakpoints.down("sm"));
21 | const mapGlRef = useRef();
22 | const plotPoints = useSelector(selectors.selectPointsDisplay);
23 | const plotPaths = useSelector(selectors.selectPlotPaths);
24 | const viewport = useSelector(selectors.selectViewport);
25 | const running = useSelector(selectors.selectRunning);
26 | const definingPoints = useSelector(selectors.selectDefiningPoints);
27 | const mapStyle = useMemo(() =>
28 | colorMode === "dark"
29 | ? "mapbox://styles/mapbox/dark-v8"
30 | : "mapbox://styles/mapbox/light-v8"
31 | );
32 | const dispatch = useDispatch();
33 |
34 | useImperativeHandle(ref, () => ({
35 | getBounds: () => {
36 | const map = mapGlRef.current.getMap();
37 | const { _ne, _sw } = map.getBounds();
38 | return {
39 | top: _ne.lat,
40 | bottom: _sw.lat,
41 | left: _ne.lng,
42 | right: _sw.lng
43 | };
44 | }
45 | }));
46 |
47 | useEffect(() => {
48 | if (matches) {
49 | dispatch(
50 | actions.setViewportState({
51 | ...viewport,
52 | zoom: 2
53 | })
54 | );
55 | }
56 | }, [matches, dispatch]); // eslint-disable-line react-hooks/exhaustive-deps
57 |
58 | useEffect(() => {
59 | if (!viewport.latitude || !viewport.longitude) {
60 | dispatch(actions.setViewportState(initialViewport));
61 | }
62 | }, [viewport, dispatch]);
63 |
64 | const onViewportChanged = viewport => {
65 | dispatch(actions.setViewportState(viewport));
66 | };
67 |
68 | const onDefinedPoint = ({ lngLat }) => {
69 | dispatch(actions.addDefinedPoint(lngLat));
70 | };
71 | const layers = useMemo(() => [
72 | new PathLayer({
73 | id: "path-layer",
74 | data: plotPaths,
75 | getPath: d => d.path,
76 | getColor: d => d.color,
77 | pickable: true,
78 | widthMinPixels: 4,
79 | widthMaxPixels: 8
80 | }),
81 | new ScatterplotLayer({
82 | id: "scatter-layer",
83 | data: plotPoints,
84 | pickable: true,
85 | opacity: 0.8,
86 | getFillColor: p => p.color,
87 | radiusMinPixels: 6,
88 | radiusMaxPixels: 8
89 | })
90 | ], [plotPaths, plotPoints]);
91 |
92 | return (
93 |
94 |
107 | {running && }
108 |
109 |
110 | {children}
111 |
112 | );
113 | });
114 |
--------------------------------------------------------------------------------
/src/components/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/styles";
3 | import { Paper, Divider } from "@material-ui/core";
4 | import { MenuHeader } from "./MenuHeader";
5 | import { MenuSolverControls } from "./MenuSolverControls";
6 | import { MenuMetrics } from "./MenuMetrics";
7 | import { MenuPointControls } from "./MenuPointControls";
8 | import { OtherControls } from "./OtherControls";
9 |
10 | const useStyles = makeStyles(theme => ({
11 | wrapper: {
12 | overflowY: "auto",
13 | flex: "0 0 400px",
14 | padding: theme.spacing(2),
15 | display: "flex",
16 | flexDirection: "column",
17 | flexWrap: "nowrap",
18 | zIndex: 100
19 | },
20 | [theme.breakpoints.down("sm")]: {
21 | width: "100%"
22 | }
23 | }));
24 |
25 | export const Menu = ({
26 | onStart,
27 | onPause,
28 | onUnPause,
29 | onFullSpeed,
30 | onStop,
31 | onStep,
32 | onRandomizePoints
33 | }) => {
34 | const classes = useStyles();
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/MenuHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { Grid, Typography, IconButton, Tooltip } from "@material-ui/core";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import {
6 | faInfoCircle,
7 | faBriefcase,
8 | faDonate
9 | } from "@fortawesome/free-solid-svg-icons";
10 | import { faGithub } from "@fortawesome/free-brands-svg-icons";
11 | import { makeStyles } from "@material-ui/styles";
12 | import { MenuSection } from "./MenuSection";
13 |
14 | import * as actions from "../store/actions";
15 |
16 | const useStyles = makeStyles(theme => ({
17 | root: {
18 | paddingTop: theme.spacing(3),
19 | paddingBottom: theme.spacing(3)
20 | },
21 | title: {
22 | fontSize: "1.2rem"
23 | }
24 | }));
25 |
26 | export const MenuHeader = props => {
27 | const classes = useStyles();
28 | const dispatch = useDispatch();
29 |
30 | const onOpenSiteInfo = () => {
31 | dispatch(actions.toggleSiteInfoOpen());
32 | };
33 |
34 | return (
35 |
36 |
37 |
44 | TSPVIS
45 |
46 |
47 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | Visualize algorithms for the traveling salesman problem. Use the
65 | controls below to plot points, choose an algorithm, and control
66 | execution.
67 |
68 | (Hint: try a construction algorithm followed by an improvement
69 | algorithm)
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/components/MenuItem.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/styles";
3 | import { Grid, Typography } from "@material-ui/core";
4 |
5 | const useStyles = makeStyles(theme => ({
6 | item: {
7 | margin: `${theme.spacing(1.5)}px 0`
8 | }
9 | }));
10 |
11 | export const MenuItem = ({ children, title = "", row = false }) => {
12 | const classes = useStyles();
13 |
14 | return (
15 |
16 |
22 | {title && (
23 |
24 |
30 | {title}
31 |
32 |
33 | )}
34 | {children}
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/MenuMetrics.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useSelector } from "react-redux";
3 | import { Grid, Typography } from "@material-ui/core";
4 | import * as selectors from "../store/selectors";
5 |
6 | import { MenuSection } from "./MenuSection";
7 | import { MenuItem } from "./MenuItem";
8 | import { makeStyles } from "@material-ui/styles";
9 |
10 | const useStyles = makeStyles(theme => ({
11 | grow: {
12 | flexGrow: 1,
13 | paddingRight: theme.spacing(1)
14 | },
15 | unit: {
16 | flexShrink: 0,
17 | width: "2rem"
18 | }
19 | }));
20 |
21 | export const MenuMetrics = props => {
22 | const classes = useStyles();
23 | const best = useSelector(selectors.selectBestCostDisplay);
24 | const evaluating = useSelector(selectors.selectEvaluatingCostDisplay);
25 | const startedRunningAt = useSelector(selectors.selectStartedRunningAt);
26 | const [runningFor, setRunningFor] = useState(0);
27 |
28 | useEffect(() => {
29 | if (startedRunningAt) {
30 | const interval = setInterval(() => {
31 | const elapsed = Date.now() - startedRunningAt;
32 | const seconds = Math.floor(elapsed / 1000);
33 | const milliseconds = Math.floor((elapsed % 1000) / 10); // Get two decimal places
34 | setRunningFor(`${seconds}.${milliseconds.toString().padStart(2, '0')}`);
35 | }, 100);
36 | return () => clearInterval(interval);
37 | }
38 | }, [startedRunningAt]);
39 |
40 | return (
41 |
42 |
123 |
124 | );
125 | };
126 |
--------------------------------------------------------------------------------
/src/components/MenuPointControls.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import {
4 | ButtonGroup,
5 | Button,
6 | Slider,
7 | Grid,
8 | Typography,
9 | makeStyles
10 | } from "@material-ui/core";
11 | import {
12 | faRandom,
13 | faSave,
14 | faMousePointer,
15 | faMapMarked
16 | } from "@fortawesome/free-solid-svg-icons";
17 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
18 |
19 | import { MenuSection } from "./MenuSection";
20 | import { MenuItem } from "./MenuItem";
21 | import * as selectors from "../store/selectors";
22 | import * as actions from "../store/actions";
23 |
24 | const useStyles = makeStyles(theme => ({
25 | grow: {
26 | flexGrow: 1
27 | }
28 | }));
29 |
30 | // [0, 1/2, 1, 3, 12]
31 | let cache = ["1e+0", "1e+0"];
32 | const possRoutes = n => {
33 | if (n <= 2) {
34 | return "1e+0";
35 | }
36 | if (typeof cache[n - 1] !== "undefined") {
37 | return cache[n - 1];
38 | }
39 |
40 | let result = 1;
41 |
42 | for (let i = 1; i <= n; i++) {
43 | result *= i;
44 | cache[i] = (result / 2).toExponential(3);
45 | }
46 |
47 | return cache[n - 1];
48 | };
49 |
50 | export const MenuPointControls = ({ onRandomizePoints }) => {
51 | const classes = useStyles();
52 | const [possiblePaths, setPossiblePaths] = useState("0");
53 | const dispatch = useDispatch();
54 | const pointCount = useSelector(selectors.selectPointCount);
55 | const running = useSelector(selectors.selectRunning);
56 | const definingPoints = useSelector(selectors.selectDefiningPoints);
57 |
58 | const onDefaultMap = () => {
59 | dispatch(actions.setDefaultMap());
60 | };
61 |
62 | const onToggleDefiningPoints = () => {
63 | const action = definingPoints
64 | ? actions.stopDefiningPoints()
65 | : actions.startDefiningPoints();
66 | dispatch(action);
67 | };
68 |
69 | const onPointCountChange = (_, newCount) => {
70 | dispatch(actions.setPointCount(newCount));
71 | };
72 |
73 | useEffect(() => {
74 | setPossiblePaths(possRoutes(pointCount));
75 | }, [pointCount]);
76 |
77 | const [num, exp] = possiblePaths.split("e+");
78 |
79 | return (
80 |
81 |
106 |
107 |
119 |
139 |
140 | );
141 | };
142 |
--------------------------------------------------------------------------------
/src/components/MenuSection.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/styles";
3 | import { Grid } from "@material-ui/core";
4 |
5 | const useStyles = makeStyles(theme => ({
6 | section: {
7 | padding: theme.spacing(2),
8 | // backgroundColor: ({ highlight = false }) =>
9 | // highlight ? theme.palette.grey[100] : theme.palette.paper,
10 | border: ({ highlight = false }) =>
11 | highlight ? `2px solid ${theme.palette.secondary.main}` : "none",
12 | borderRadius: "10px"
13 | }
14 | }));
15 |
16 | export const MenuSection = ({ children, ...rest }) => {
17 | const classes = useStyles(rest);
18 |
19 | return (
20 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/MenuSolverControls.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | ButtonGroup,
4 | Button,
5 | Slider,
6 | Select,
7 | ListSubheader,
8 | MenuItem as SelectItem,
9 | Typography,
10 | Switch,
11 | Grid,
12 | IconButton
13 | } from "@material-ui/core";
14 | import { useDispatch, useSelector } from "react-redux";
15 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
16 | import {
17 | faPlay,
18 | faStop,
19 | faRedo,
20 | faQuestion,
21 | faFastForward,
22 | faPause,
23 | faStepForward
24 | } from "@fortawesome/free-solid-svg-icons";
25 | import { MenuSection } from "./MenuSection";
26 | import { MenuItem } from "./MenuItem";
27 | import { useAlgorithmInfo } from "../hooks";
28 | import * as actions from "../store/actions";
29 | import * as selectors from "../store/selectors";
30 | import CoordinateInput from './CoordinateInput';
31 |
32 | export const MenuSolverControls = ({
33 | onStart,
34 | onPause,
35 | onUnPause,
36 | onFullSpeed,
37 | onStop,
38 | onStep
39 | }) => {
40 | const dispatch = useDispatch();
41 | const algorithms = useAlgorithmInfo();
42 | const selectedAlgorithm = useSelector(selectors.selectAlgorithm);
43 | const delay = useSelector(selectors.selectDelay);
44 | const evaluatingDetailLevel = useSelector(
45 | selectors.selectEvaluatingDetailLevel
46 | );
47 | const maxEvaluatingDetailLevel = useSelector(
48 | selectors.selectMaxEvaluatingDetailLevel
49 | );
50 | const showBestPath = useSelector(selectors.selectShowBestPath);
51 | const running = useSelector(selectors.selectRunning);
52 | const fullSpeed = useSelector(selectors.selectFullSpeed);
53 | const paused = useSelector(selectors.selectPaused);
54 | const definingPoints = useSelector(selectors.selectDefiningPoints);
55 |
56 | const onAlgorithmChange = event => {
57 | event.persist();
58 | onStop();
59 | const solverKey = event.target.value;
60 | const { defaults } = algorithms.find(alg => alg.solverKey === solverKey);
61 | dispatch(actions.setAlgorithm(solverKey, defaults));
62 | };
63 |
64 | const onDelayChange = (_, newDelay) => {
65 | dispatch(actions.setDelay(newDelay));
66 | };
67 |
68 | const onEvaluatingDetailLevelChange = (onLevel, offLevel) => event => {
69 | const level = event.target.checked ? onLevel : offLevel;
70 | dispatch(actions.setEvaluatingDetailLevel(level));
71 | };
72 |
73 | const onShowBestPathChange = event => {
74 | dispatch(actions.setShowBestPath(event.target.checked));
75 | };
76 |
77 | const onReset = () => {
78 | onStop();
79 | dispatch(actions.resetSolverState());
80 | };
81 |
82 | const onShowAlgInfo = () => {
83 | dispatch(actions.toggleAlgInfoOpen());
84 | };
85 |
86 | return (
87 | <>
88 |
89 |
135 |
136 |
162 |
174 |
175 |
176 |
230 |
231 |
232 |
233 |
238 | Add custom list of coordinates
239 |
240 |
241 |
242 |
243 | >
244 | );
245 | };
246 |
--------------------------------------------------------------------------------
/src/components/OtherControls.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Grid, Typography } from "@material-ui/core";
3 | import { MenuItem } from "./MenuItem";
4 | import { MenuSection } from "./MenuSection";
5 | import { ThemeToggle } from "./ThemeToggle";
6 |
7 | export const OtherControls = props => {
8 | return (
9 |
10 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/SEO.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Helmet from "react-helmet";
3 |
4 | const description =
5 | "Interactive solver for the traveling salesman problem to visualize different algorithms. Includes various Heuristic and Exhaustive algorithms.";
6 |
7 | export const SEO = ({ subtitle }) => {
8 | return (
9 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/ThemeToggle.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Switch } from "@material-ui/core";
3 | import { useThemeContext } from "../context";
4 |
5 | export const ThemeToggle = () => {
6 | const { colorMode, toggleColorMode } = useThemeContext();
7 |
8 | return (
9 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export * from "./AlgorithmModals";
2 | export * from "./InformationModal";
3 | export * from "./IntroductionModal";
4 | export * from "./Layout";
5 | export * from "./MapPlot";
6 | export * from "./Menu";
7 | export * from "./MenuHeader";
8 | export * from "./MenuItem";
9 | export * from "./MenuMetrics";
10 | export * from "./MenuPointControls";
11 | export * from "./MenuSection";
12 | export * from "./MenuSolverControls";
13 | export * from "../context/PreSetTheme";
14 | export * from "./SEO";
15 | export * from "./ThemeToggle";
16 | export { default as CoordinateInput } from './CoordinateInput';
17 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | // orangish
2 | export const START_POINT_COLOR = [255, 87, 34];
3 |
4 | // blueish
5 | export const POINT_COLOR = [41, 121, 255];
6 |
7 | // greenish
8 | export const BEST_PATH_COLOR = [76, 175, 80];
9 |
10 | // orangish
11 | export const EVALUATING_PATH_COLOR = [255, 87, 34, 225];
12 |
13 | // reddish
14 | export const EVALUATING_ERROR_COLOR = [255, 25, 25, 240];
15 |
16 | // greyish
17 | export const EVALUATING_SEGMENT_COLOR = [180, 180, 180, 240];
18 |
19 | export const COLOR_MODE_KEY = "color-mode";
20 |
--------------------------------------------------------------------------------
/src/content/exhaustive/branchAndBoundOnCost.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: exhaustive
3 | order: 3
4 | solverKey: branchAndBoundOnCost
5 | friendlyName: Branch and Bound (Cost)
6 | defaults:
7 | evaluatingDetailLevel: 2
8 | maxEvaluatingDetailLevel: 2
9 | ---
10 |
11 | # Branch and Bound on Cost
12 |
13 | This is a recursive algorithm, similar to depth first search, that is guaranteed to find the optimal solution.
14 |
15 | 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.
16 |
17 | 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.
18 |
19 | For example, imagine:
20 |
21 | 1. A -> B -> C -> D -> E -> A was already found with a cost of 100.
22 | 2. We are evaluating A -> C -> E, which has a cost of 110. There is **no point** evaluating the remaining solutions.
23 | 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:
24 | - `A -> C -> E -> D -> B -> A`
25 | - `A -> C -> E -> B -> D -> A`
26 |
27 | Implementation is very similar to depth first search, with the exception that we cut paths that are already longer than the current best.
28 |
29 | ## Implementation
30 |
31 | ```javascript
32 | const branchAndBoundOnCost = async (
33 | points,
34 | path = [],
35 | visited = null,
36 | overallBest = Infinity
37 | ) => {
38 | if (visited === null) {
39 | // initial call
40 | path = [points.shift()];
41 | points = new Set(points);
42 | visited = new Set();
43 | }
44 |
45 | // figure out which points are left
46 | const available = setDifference(points, visited);
47 |
48 | // calculate the cost, from here, to go home
49 | const backToStart = [...path, path[0]];
50 | const cost = pathCost(backToStart);
51 |
52 | if (cost > overallBest) {
53 | // we may not be done, but have already traveled further than the best path
54 | // no reason to continue
55 | return [null, null];
56 | }
57 |
58 | // still cheaper than the best, keep going deeper, and deeper, and deeper...
59 |
60 | if (available.size === 0) {
61 | // at the end of the path, return where we're at
62 | return [cost, backToStart];
63 | }
64 |
65 | let [bestCost, bestPath] = [null, null];
66 |
67 | // for every point yet to be visited along this path
68 | for (const p of available) {
69 | // go to that point
70 | visited.add(p);
71 | path.push(p);
72 |
73 | // RECURSE - go through all the possible points from that point
74 | const [curCost, curPath] = await branchAndBoundOnCost(
75 | points,
76 | path,
77 | visited,
78 | overallBest
79 | );
80 |
81 | // if that path is better and complete, keep it
82 | if (curCost && (!bestCost || curCost < bestCost)) {
83 | [bestCost, bestPath] = [curCost, curPath];
84 |
85 | if (!overallBest || bestCost < overallBest) {
86 | // found a new best complete path
87 | overallBest = bestCost;
88 | self.setBestPath(bestPath, bestCost);
89 | }
90 | }
91 |
92 | // go back up and make that point available again
93 | visited.delete(p);
94 | path.pop();
95 | }
96 |
97 | return [bestCost, bestPath];
98 | };
99 | ```
100 |
--------------------------------------------------------------------------------
/src/content/exhaustive/branchAndBoundOnCostAndCross.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: exhaustive
3 | order: 4
4 | solverKey: branchAndBoundOnCostAndCross
5 | friendlyName: Branch and Bound (Cost, Crossings)
6 | defaults:
7 | evaluatingDetailLevel: 2
8 | maxEvaluatingDetailLevel: 2
9 | ---
10 |
11 | # Branch and Bound (Cost, Intersections)
12 |
13 | This is the same as branch and bound on cost, with an additional heuristic added to further minimize the search space.
14 |
15 | 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.
16 |
17 | Implementation is almost identical to branch and bound on cost only, with the added heuristic below:
18 |
19 | ## Implementation
20 |
21 | ```javascript
22 |
23 | const counterClockWise = (p, q, r) => {
24 | return (q[0] - p[0]) * (r[1] - q[1]) <
25 | (q[1] - p[1]) * (r[0] - q[0])
26 | }
27 |
28 | const intersects = (a, b, c, d) => {
29 | return counterClockWise(a, c, d) !== counterClockWise(b, c, d) &&
30 | counterClockWise(a, b, c) !== counterClockWise(a, b, d)
31 | }
32 |
33 | const branchAndBoundOnCostAndCross = async (...) => {
34 | //
35 | // .....
36 | //
37 |
38 | if (path.length > 3) {
39 | // if this newly added edge crosses over the existing path,
40 | // don't continue. It's been proven that an optimal path will
41 | // not cross itself.
42 | const newSegment = [
43 | path[path.length-2], path[path.length-1]
44 | ]
45 | for (let i=1; i {
39 | if (visited === null) {
40 | // initial call
41 | path = [points.shift()];
42 | points = new Set(points);
43 | visited = new Set();
44 | }
45 |
46 | // figure out what points are left from this point
47 | const available = setDifference(points, visited);
48 |
49 | if (available.size === 0) {
50 | // this must be a complete path
51 | const backToStart = [...path, path[0]];
52 |
53 | // calculate the cost of this path
54 | const cost = pathCost(backToStart);
55 |
56 | // return both the cost and the path where we're at
57 | return [cost, backToStart];
58 | }
59 |
60 | let [bestCost, bestPath] = [null, null];
61 |
62 | // for every point yet to be visited along this path
63 | for (const p of available) {
64 | // go to that point
65 | visited.add(p);
66 | path.push(p);
67 |
68 | // RECURSE - go through all the possible points from that point
69 | const [curCost, curPath] = await dfs(points, path, visited, overallBest);
70 |
71 | // if that path is better, keep it
72 | if (bestCost === null || curCost < bestCost) {
73 | [bestCost, bestPath] = [curCost, curPath];
74 |
75 | if (overallBest === null || bestCost < overallBest) {
76 | // found a new best complete path
77 | overallBest = bestCost;
78 | }
79 | }
80 |
81 | // go back up and make that point available again
82 | visited.delete(p);
83 | path.pop();
84 | }
85 | return [bestCost, bestPath];
86 | };
87 | ```
88 |
--------------------------------------------------------------------------------
/src/content/exhaustive/random.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: exhaustive
3 | order: 2
4 | solverKey: random
5 | friendlyName: Random
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Random
12 |
13 | 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.
14 |
15 | I consider it exhaustive because if it runs for infinity, eventually it will encounter every possible path.
16 |
17 | 1. From the starting path
18 | 2. Randomly shuffle the path
19 | 3. If it's better, keep it
20 | 4. If not, ditch it and keep going
21 |
22 | ## Implementation
23 |
24 | ```javascript
25 | const random = async points => {
26 | let best = Infinity;
27 |
28 | while (true) {
29 | // save off the starting point
30 | const start = points.shift();
31 |
32 | // sort the remaining points
33 | const path = points.sort(() => Math.random() - 0.5);
34 |
35 | // put the starting point back
36 | path.unshift(start);
37 |
38 | // return to the starting point
39 | path.push(start);
40 |
41 | // calculate the new cost
42 | const cost = pathCost(path);
43 |
44 | if (cost < best) {
45 | // we found a better path
46 | best = cost;
47 | }
48 |
49 | // get rid of starting point at the end
50 | path.pop();
51 | }
52 | };
53 | ```
54 |
--------------------------------------------------------------------------------
/src/content/heurisitc-construction/arbitraryInsertion.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-construction
3 | order: 2
4 | solverKey: arbitraryInsertion
5 | friendlyName: Arbitrary Insertion
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Heuristic Construction: Arbitrary Insertion
12 |
13 | This is a heuristic construction algorithm. It select a random point, and then figures out where the best place to put it will be.
14 |
15 | 1. From the starting point
16 | 2. First, go to the closest point
17 | 3. Choose a random point to go to
18 | 4. Find the cheapest place to add it in the path
19 | 5. Chosen point is no longer an "available point"
20 | 6. Continue from #3 until there are no available points, and then return to the start.
21 |
22 | ## Implementation
23 |
24 | ```javascript
25 | const arbitraryInsertion = async points => {
26 | // from the starting point
27 | const path = [points.shift()];
28 |
29 | //
30 | // INITIALIZATION - go to the nearest point
31 | //
32 | points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
33 | path.push(points.pop());
34 |
35 | // randomly sort points - this is the order they will be added
36 | // to the path
37 | points.sort(() => Math.random() - 0.5);
38 |
39 | while (points.length > 0) {
40 | //
41 | // SELECTION - choose a next point randomly
42 | //
43 | const nextPoint = points.pop();
44 |
45 | //
46 | // INSERTION -find the insertion spot that minimizes distance
47 | //
48 | let [bestCost, bestIdx] = [Infinity, null];
49 | for (let i = 1; i < path.length; i++) {
50 | const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
51 | if (insertionCost < bestCost) {
52 | [bestCost, bestIdx] = [insertionCost, i];
53 | }
54 | }
55 | path.splice(bestIdx, 0, nextPoint);
56 | }
57 |
58 | // return to start after visiting all other points
59 | path.push(path[0]);
60 | };
61 | ```
62 |
--------------------------------------------------------------------------------
/src/content/heurisitc-construction/convexHull.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-construction
3 | order: 5
4 | solverKey: convexHull
5 | friendlyName: Convex Hull
6 | defaults:
7 | evaluatingDetailLevel: 2
8 | maxEvaluatingDetailLevel: 2
9 | ---
10 |
11 | # Convex Hull
12 |
13 | 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.
14 |
15 | 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).
16 |
17 | In essence, the steps are:
18 |
19 | 1. Determine the leftmost point
20 | 2. Continually add the most counterclockwise point until the convex hull is formed
21 | 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)
22 | 4. Of those, choose p that minimizes cost(i -> p -> j) / cost(i -> j)
23 | 5. Add p to the path between i and j
24 | 6. Repeat from #3 until there are no remaining points
25 |
26 | ## Implementation
27 |
28 | ```javascript
29 | const convexHull = async points => {
30 | const sp = points[0];
31 |
32 | // Find the "left most point"
33 | let leftmost = points[0];
34 | for (const p of points) {
35 | if (p[1] < leftmost[1]) {
36 | leftmost = p;
37 | }
38 | }
39 |
40 | const path = [leftmost];
41 |
42 | while (true) {
43 | const curPoint = path[path.length - 1];
44 | let [selectedIdx, selectedPoint] = [0, null];
45 |
46 | // find the "most counterclockwise" point
47 | for (let [idx, p] of points.entries()) {
48 | if (!selectedPoint || orientation(curPoint, p, selectedPoint) === 2) {
49 | // this point is counterclockwise with respect to the current hull
50 | // and selected point (e.g. more counterclockwise)
51 | [selectedIdx, selectedPoint] = [idx, p];
52 | }
53 | }
54 |
55 | // adding this to the hull so it's no longer available
56 | points.splice(selectedIdx, 1);
57 |
58 | // back to the furthest left point, formed a cycle, break
59 | if (selectedPoint === leftmost) {
60 | break;
61 | }
62 |
63 | // add to hull
64 | path.push(selectedPoint);
65 | }
66 |
67 | while (points.length > 0) {
68 | let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
69 |
70 | for (let [freeIdx, freePoint] of points.entries()) {
71 | // for every free point, find the point in the current path
72 | // that minimizes the cost of adding the point minus the cost of
73 | // the original segment
74 | let [bestCost, bestIdx] = [Infinity, 0];
75 | for (let [pathIdx, pathPoint] of path.entries()) {
76 | const nextPathPoint = path[(pathIdx + 1) % path.length];
77 |
78 | // the new cost minus the old cost
79 | const evalCost =
80 | pathCost([pathPoint, freePoint, nextPathPoint]) -
81 | pathCost([pathPoint, nextPathPoint]);
82 |
83 | if (evalCost < bestCost) {
84 | [bestCost, bestIdx] = [evalCost, pathIdx];
85 | }
86 | }
87 |
88 | // figure out how "much" more expensive this is with respect to the
89 | // overall length of the segment
90 | const nextPoint = path[(bestIdx + 1) % path.length];
91 | const prevCost = pathCost([path[bestIdx], nextPoint]);
92 | const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
93 | const ratio = newCost / prevCost;
94 |
95 | if (ratio < bestRatio) {
96 | [bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
97 | }
98 | }
99 |
100 | const [nextPoint] = points.splice(bestPointIdx, 1);
101 | path.splice(insertIdx, 0, nextPoint);
102 | }
103 |
104 | // rotate the array so that starting point is back first
105 | const startIdx = path.findIndex(p => p === sp);
106 | path.unshift(...path.splice(startIdx, path.length));
107 |
108 | // go back home
109 | path.push(sp);
110 | };
111 | ```
112 |
--------------------------------------------------------------------------------
/src/content/heurisitc-construction/furthestInsertion.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-construction
3 | order: 4
4 | solverKey: furthestInsertion
5 | friendlyName: Furthest Insertion
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Furthest Insertion
12 |
13 | 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.
14 |
15 | 1. From the starting point
16 | 2. First, go to the closest point
17 | 3. Choose the point that is furthest from any of the points on the path
18 | 4. Find the cheapest place to add it in the path
19 | 5. Chosen point is no longer an "available point"
20 | 6. Continue from #3 until there are no available points, and then return to the start.
21 |
22 | ## Implementation
23 |
24 | ```javascript
25 | const furthestInsertion = async points => {
26 | // from the starting point
27 | const path = [points.shift()];
28 |
29 | //
30 | // INITIALIZATION - go to the nearest point first
31 | //
32 | points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
33 | path.push(points.pop());
34 |
35 | while (points.length > 0) {
36 | //
37 | // SELECTION - furthest point from the path
38 | //
39 | let [selectedDistance, selectedIdx] = [0, null];
40 | for (const [freePointIdx, freePoint] of points.entries()) {
41 | // find the minimum distance to the path for freePoint
42 | let [bestCostToPath, costToPathIdx] = [Infinity, null];
43 | for (const pathPoint of path) {
44 | const dist = distance(freePoint, pathPoint);
45 | if (dist < bestCostToPath) {
46 | [bestCostToPath, costToPathIdx] = [dist, freePointIdx];
47 | }
48 | }
49 |
50 | // if this point is further from the path than the currently selected
51 | if (bestCostToPath > selectedDistance) {
52 | [selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
53 | }
54 | }
55 | const [nextPoint] = points.splice(selectedIdx, 1);
56 |
57 | //
58 | // INSERTION - find the insertion spot that minimizes distance
59 | //
60 | let [bestCost, bestIdx] = [Infinity, null];
61 | for (let i = 1; i < path.length; i++) {
62 | const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
63 | if (insertionCost < bestCost) {
64 | [bestCost, bestIdx] = [insertionCost, i];
65 | }
66 | }
67 | path.splice(bestIdx, 0, nextPoint);
68 | }
69 |
70 | // return to start after visiting all other points
71 | path.push(path[0]);
72 | };
73 | ```
74 |
--------------------------------------------------------------------------------
/src/content/heurisitc-construction/nearestInsertion.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-construction
3 | order: 3
4 | solverKey: nearestInsertion
5 | friendlyName: Nearest Insertion
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Furthest Insertion
12 |
13 | 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.
14 |
15 | 1. From the starting point
16 | 2. First, go to the closest point
17 | 3. Choose the point that is **nearest** to the current path
18 | 4. Find the cheapest place to add it in the path
19 | 5. Chosen point is no longer an "available point"
20 | 6. Continue from #3 until there are no available points, and then return to the start.
21 |
22 | ## Implementation
23 |
24 | ```javascript
25 | const nearestInsertion = async points => {
26 | // from the starting point
27 | const path = [points.shift()];
28 |
29 | //
30 | // INITIALIZATION - go to the nearest point first
31 | //
32 | points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
33 | path.push(points.pop());
34 |
35 | while (points.length > 0) {
36 | //
37 | // SELECTION - nearest point to the path
38 | //
39 | let [selectedDistance, selectedIdx] = [Infinity, null];
40 | for (const [freePointIdx, freePoint] of points.entries()) {
41 | for (const pathPoint of path) {
42 | const dist = distance(freePoint, pathPoint);
43 | if (dist < selectedDistance) {
44 | [selectedDistance, selectedIdx] = [dist, freePointIdx];
45 | }
46 | }
47 | }
48 |
49 | // get the next point to add
50 | const [nextPoint] = points.splice(selectedIdx, 1);
51 |
52 | //
53 | // INSERTION - find the insertion spot that minimizes distance
54 | //
55 | let [bestCost, bestIdx] = [Infinity, null];
56 | for (let i = 1; i < path.length; i++) {
57 | const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
58 | if (insertionCost < bestCost) {
59 | [bestCost, bestIdx] = [insertionCost, i];
60 | }
61 | }
62 | path.splice(bestIdx, 0, nextPoint);
63 | }
64 |
65 | // return to start after visiting all other points
66 | path.push(path[0]);
67 | };
68 | ```
69 |
--------------------------------------------------------------------------------
/src/content/heurisitc-construction/nearestNeighbor.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-construction
3 | order: 1
4 | solverKey: nearestNeighbor
5 | friendlyName: Nearest Neighbor
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Nearest Neighbor
12 |
13 | This is a heuristic, greedy algorithm also known as nearest neighbor. It continually chooses the best looking option from the current state.
14 |
15 | 1. From the starting point
16 | 2. sort the remaining available points based on cost (distance)
17 | 3. Choose the closest point and go there
18 | 4. Chosen point is no longer an "available point"
19 | 5. Continue this way until there are no available points, and then return to the start.
20 |
21 | ## Implementation
22 |
23 | ```javascript
24 | const nearestNeighbor = async points => {
25 | const path = [points.shift()];
26 |
27 | while (points.length > 0) {
28 | // sort remaining points in place by their
29 | // distance from the last point in the current path
30 | points.sort(
31 | (a, b) =>
32 | distance(path[path.length - 1], b) - distance(path[path.length - 1], a)
33 | );
34 |
35 | // go to the closest remaining point
36 | path.push(points.pop());
37 | }
38 |
39 | // return to start after visiting all other points
40 | path.push(path[0]);
41 | const cost = pathCost(path);
42 | };
43 | ```
44 |
--------------------------------------------------------------------------------
/src/content/heurisitc-construction/simulatedAnnealing.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-construction
3 | order: 6
4 | solverKey: simulatedAnnealing
5 | friendlyName: Simulated Annealing
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Simulated Annealing
12 |
13 | 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.
14 |
15 | 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
16 |
17 | ## Implementation
18 |
19 | ```javascript
20 | const simulatedAnnealing = async points => {
21 | const sp = points[0];
22 | const path = points;
23 |
24 | const tempCoeff =
25 | path.length < 10
26 | ? 1 - 1e-4
27 | : path.length < 15
28 | ? 1 - 1e-5
29 | : path.length < 25
30 | ? 1 - 1e-6
31 | : 1 - 5e-7;
32 |
33 | const deltaDistance = (aIdx, bIdx) => {
34 | const aPrev = (aIdx - 1 + path.length) % path.length;
35 | const aNext = (aIdx + 1 + path.length) % path.length;
36 | const bPrev = (bIdx - 1 + path.length) % path.length;
37 | const bNext = (bIdx + 1 + path.length) % path.length;
38 | let diff =
39 | distance(path[bPrev], path[aIdx]) +
40 | distance(path[aIdx], path[bNext]) +
41 | distance(path[aPrev], path[bIdx]) +
42 | distance(path[bIdx], path[aNext]) -
43 | distance(path[aPrev], path[aIdx]) -
44 | distance(path[aIdx], path[aNext]) -
45 | distance(path[bPrev], path[bIdx]) -
46 | distance(path[bIdx], path[bNext]);
47 |
48 | if (bPrev === aIdx || bNext === aIdx) {
49 | diff += 2 * distance(path[aIdx], path[bIdx]);
50 | }
51 | return diff;
52 | };
53 |
54 | const changePath = temperature => {
55 | // 2 random points
56 | const a = 1 + Math.floor(Math.random() * (path.length - 1));
57 | const b = 1 + Math.floor(Math.random() * (path.length - 1));
58 |
59 | const delta = deltaDistance(a, b);
60 | if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
61 | // swap points
62 | [path[a], path[b]] = [path[b], path[a]];
63 | }
64 | };
65 |
66 | const initialTemp = 100 * distance(path[0], path[1]);
67 | let i = 0;
68 |
69 | for (
70 | let temperature = initialTemp;
71 | temperature > 1e-6;
72 | temperature *= tempCoeff
73 | ) {
74 | changePath(temperature);
75 | if (i % 10000 == 0) {
76 | self.setEvaluatingPaths(() => ({
77 | paths: [{ path, color: EVALUATING_PATH_COLOR }],
78 | cost: pathCost(path)
79 | }));
80 | await self.sleep();
81 | }
82 | if (i % 100000 == 0) {
83 | path.push(sp);
84 | self.setBestPath(path, pathCost(path));
85 | path.pop();
86 | }
87 | i++;
88 | }
89 |
90 | // rotate the array so that starting point is back first
91 | rotateToStartingPoint(path, sp);
92 |
93 | // go back home
94 | path.push(sp);
95 | const cost = pathCost(path);
96 |
97 | self.setEvaluatingPaths(() => ({
98 | paths: [{ path }],
99 | cost
100 | }));
101 | self.setBestPath(path, cost);
102 | };
103 |
104 | makeSolver(simulatedAnnealing);
105 | ```
106 |
--------------------------------------------------------------------------------
/src/content/heuristic-improvement/twoOptInversion.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-improvement
3 | order: 1
4 | solverKey: twoOptInversion
5 | friendlyName: Two Opt Inversion
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Two-Opt inversion
12 |
13 | 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.
14 |
15 | 1. While a better path has not been found.
16 | 2. For each pair of points:
17 | 3. Reverse the path between the selected points.
18 | 4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
19 | 5. If not, revert the path and continue searching.
20 |
21 | ## Implementation
22 |
23 | ```javascript
24 | const twoOptInversion = async path => {
25 | path.push(path[0]);
26 | let best = pathCost(path);
27 | let swapped = true;
28 |
29 | while (swapped) {
30 | swapped = false;
31 | for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
32 | for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
33 | // section of the path to reverse
34 | const section = path.slice(pt1, pt2 + 1);
35 |
36 | // reverse section in place
37 | section.reverse();
38 |
39 | // replace section of path with reversed section in place
40 | path.splice(pt1, pt2 + 1 - pt1, ...section);
41 |
42 | // calculate new cost
43 | const newPath = path;
44 | const cost = pathCost(newPath);
45 |
46 | if (cost < best) {
47 | // found a better path after the swap, keep it
48 | swapped = true;
49 | best = cost;
50 | self.setBestPath(newPath, best);
51 | } else {
52 | // un-reverse the section
53 | section.reverse();
54 | path.splice(pt1, pt2 + 1 - pt1, ...section);
55 | }
56 | }
57 | }
58 | }
59 | };
60 | ```
61 |
--------------------------------------------------------------------------------
/src/content/heuristic-improvement/twoOptReciprocalExchange.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: heuristic-improvement
3 | order: 2
4 | solverKey: twoOptReciprocalExchange
5 | friendlyName: Two Opt Reciprocal Exchange
6 | defaults:
7 | evaluatingDetailLevel: 1
8 | maxEvaluatingDetailLevel: 1
9 | ---
10 |
11 | # Two-Opt Reciprocal Exchange
12 |
13 | 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.
14 |
15 | 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.
16 |
17 | It could be worthwhile to try this algorithm prior to 2-opt inversion because of the cheaper cost of calculation, but probably not.
18 |
19 | 1. While a better path has not been found.
20 | 2. For each pair of points:
21 | 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.
22 | 4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
23 | 5. If not, revert the path and continue searching.
24 |
25 | ## Implementation
26 |
27 | ```javascript
28 | const twoOptReciprocalExchange = async path => {
29 | path.push(path[0]);
30 | let best = pathCost(path);
31 | let swapped = true;
32 |
33 | self.setBestPath(path, best);
34 |
35 | while (swapped) {
36 | swapped = false;
37 | for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
38 | for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
39 | // swap current pair of points
40 | [path[pt1], path[pt2]] = [path[pt2], path[pt1]];
41 |
42 | // calculate new cost
43 | const cost = pathCost(path);
44 |
45 | if (cost < best) {
46 | // found a better path after the swap, keep it
47 | swapped = true;
48 | best = cost;
49 | } else {
50 | // swap back - this one's worse
51 | [path[pt1], path[pt2]] = [path[pt2], path[pt1]];
52 | }
53 | }
54 | }
55 | }
56 | };
57 | ```
58 |
--------------------------------------------------------------------------------
/src/content/introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: introduction
3 | ---
4 |
5 | # Traveling Salesman Problem
6 |
7 | The traveling salesman problem (TSP) asks the question, "Given a list of cities and the distances between each pair of cities, what is the shortest possible route that visits each city and returns to the origin city?".
8 |
9 | ### This project
10 |
11 | - The goal of this site is to be an **educational** resource to help visualize, learn, and develop different algorithms for the traveling salesman problem in a way that's easily accessible
12 | - As you apply different algorithms, the current best path is saved and used as input to whatever you run next. (e.g. shortest path first -> branch and bound). The order in which you apply different algorithms to the problem is sometimes referred to the meta-heuristic strategy.
13 |
14 | ### Heuristic algorithms
15 |
16 | Heuristic algorithms attempt to find a good approximation of the optimal path within a more _reasonable_ amount of time.
17 |
18 | **Construction** - Build a path (e.g. shortest path)
19 |
20 | - Shortest Path
21 | - Arbitrary Insertion
22 | - Furthest Insertion
23 | - Nearest Insertion
24 | - Convex Hull Insertion\*
25 | - Simulated Annealing\*
26 |
27 | **Improvement** - Attempt to take an existing constructed path and improve on it
28 |
29 | - 2-Opt Inversion
30 | - 2-Opt Reciprcal Exchange\*
31 |
32 | ### Exhaustive algorithms
33 |
34 | Exhaustive algorithms will always find the best possible solution by evaluating every possible path. These algorithms are typically significantly more expensive then the heuristic algorithms discussed next. The exhaustive algorithms implemented so far include:
35 |
36 | - Random Paths
37 | - Depth First Search (Brute Force)
38 | - Branch and Bound (Cost)
39 | - Branch and Bound (Cost, Intersections)\*
40 |
41 | ## Dependencies
42 |
43 | These are the main tools used to build this site:
44 |
45 | - [gatsbyjs](https://www.gatsbyjs.org)
46 | - [reactjs](https://reactjs.org)
47 | - [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
48 | - [material-ui](https://material-ui.com/)
49 | - [deck.gl](https://deck.gl/#/)
50 | - [mapbox](https://www.mapbox.com/)
51 |
52 | ## Contributing
53 |
54 | Pull requests are always welcome! Also, feel free to raise any ideas, suggestions, or bugs as an issue.
55 |
--------------------------------------------------------------------------------
/src/context/PreSetTheme.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const PreSetTheme = () => (
4 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/context/ThemeContext.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createContext, useCallback, useContext, useMemo } from "react";
3 | import { ThemeProvider as MUIThemeProvider } from "@material-ui/styles";
4 | import { CssBaseline, createTheme } from "@material-ui/core";
5 | import blue from "@material-ui/core/colors/blue";
6 | import orange from "@material-ui/core/colors/orange";
7 |
8 | import { COLOR_MODE_KEY } from "../constants";
9 | import { usePersistentState } from "../hooks";
10 |
11 | export const ThemeContext = createContext();
12 |
13 | export const ThemeContextProvider = props => {
14 | const { children } = props;
15 |
16 | const [colorMode, setColorMode] = usePersistentState(COLOR_MODE_KEY, "dark");
17 |
18 | const muiTheme = useMemo(
19 | () =>
20 | createTheme({
21 | palette: {
22 | primary: blue,
23 | secondary: orange,
24 | type: colorMode
25 | }
26 | }),
27 | [colorMode]
28 | );
29 |
30 | const toggleColorMode = useCallback(() => {
31 | setColorMode(current => (current === "dark" ? "light" : "dark"));
32 | }, []);
33 |
34 | return (
35 |
36 |
37 |
45 | {children}
46 |
47 |
48 | );
49 | };
50 |
51 | export const useThemeContext = () => useContext(ThemeContext);
52 |
--------------------------------------------------------------------------------
/src/context/index.js:
--------------------------------------------------------------------------------
1 | export * from "./PreSetTheme";
2 | export * from "./ThemeContext";
3 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export * from "./useAlgorithmInfo";
2 | export * from "./useIntroductionInfo";
3 | export * from "./useIsFirstLoad";
4 | export * from "./usePersistentState";
5 | export * from "./useSolverWorker";
6 | export * from "./useUpdateEffect";
7 |
--------------------------------------------------------------------------------
/src/hooks/useAlgorithmInfo.js:
--------------------------------------------------------------------------------
1 | import { useStaticQuery, graphql } from "gatsby";
2 |
3 | export const useAlgorithmInfo = () => {
4 | const {
5 | allMarkdownRemark: { edges: algorithms }
6 | } = useStaticQuery(graphql`
7 | query AlgorithmModalsQuery {
8 | allMarkdownRemark(
9 | filter: {
10 | frontmatter: {
11 | type: {
12 | in: [
13 | "exhaustive"
14 | "heuristic-construction"
15 | "heuristic-improvement"
16 | ]
17 | }
18 | }
19 | }
20 | sort: { fields: frontmatter___order }
21 | ) {
22 | edges {
23 | node {
24 | frontmatter {
25 | order
26 | friendlyName
27 | solverKey
28 | type
29 | defaults {
30 | evaluatingDetailLevel
31 | maxEvaluatingDetailLevel
32 | }
33 | }
34 | html
35 | }
36 | }
37 | }
38 | }
39 | `);
40 |
41 | return algorithms.map(alg => ({
42 | ...alg.node.frontmatter,
43 | html: alg.node.html
44 | }));
45 | };
46 |
--------------------------------------------------------------------------------
/src/hooks/useIntroductionInfo.js:
--------------------------------------------------------------------------------
1 | import { useStaticQuery, graphql } from "gatsby";
2 |
3 | export const useIntroductionInfo = () => {
4 | const {
5 | allMarkdownRemark: { edges: introductions }
6 | } = useStaticQuery(graphql`
7 | query IntroductionModalQuery {
8 | allMarkdownRemark(
9 | filter: { frontmatter: { type: { eq: "introduction" } } }
10 | ) {
11 | edges {
12 | node {
13 | html
14 | }
15 | }
16 | }
17 | }
18 | `);
19 |
20 | return introductions[0].node.html;
21 | };
22 |
--------------------------------------------------------------------------------
/src/hooks/useIsFirstLoad.js:
--------------------------------------------------------------------------------
1 | export const useIsFirstLoad = (keyName = "isFirstVisit") => {
2 | if (!window.localStorage) {
3 | return false;
4 | }
5 |
6 | if (!localStorage[keyName]) {
7 | localStorage.setItem(keyName, true);
8 | return true;
9 | }
10 |
11 | return false;
12 | };
13 |
--------------------------------------------------------------------------------
/src/hooks/usePersistentState.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export const usePersistentState = (key, defaultValue = undefined) => {
4 | const [value, setValue] = useState(() => {
5 | const existing = typeof window !== "undefined" && window.localStorage?.getItem(key);
6 | if (existing) {
7 | return existing;
8 | }
9 | return defaultValue;
10 | });
11 |
12 | useEffect(() => {
13 | localStorage.setItem(key, value);
14 | }, [value, key]);
15 |
16 | return [value, setValue];
17 | };
18 |
--------------------------------------------------------------------------------
/src/hooks/useSolverWorker.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import solvers from "../solvers";
3 |
4 | export const useSolverWorker = (onSolverMessage, algorithm) => {
5 | const [solver, setSolver] = useState();
6 |
7 | const resetSolver = () => {
8 | if (solver) {
9 | solver.terminate();
10 | }
11 | const worker = new solvers[algorithm]();
12 | worker.onmessage = ({ data }) => onSolverMessage(data);
13 | worker.onerror = console.error;
14 | setSolver(worker);
15 | };
16 |
17 | useEffect(resetSolver, [algorithm, onSolverMessage]);
18 |
19 | const postMessage = data => {
20 | if (solver) {
21 | solver.postMessage(data);
22 | }
23 | };
24 |
25 | return {
26 | postMessage,
27 | terminate: resetSolver
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/hooks/useUpdateEffect.js:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/55075604/react-hooks-useeffect-only-on-update
2 | import { useEffect, useRef } from "react";
3 |
4 | export const useUpdateEffect = (effect, dependencies = []) => {
5 | const isInitialMount = useRef(true);
6 |
7 | useEffect(() => {
8 | if (isInitialMount.current) {
9 | isInitialMount.current = false;
10 | } else {
11 | effect();
12 | }
13 | }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
14 | };
15 |
--------------------------------------------------------------------------------
/src/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhackshaw/tspvis/819435473cb462f6cd79133dbe6e138ae3388d33/src/images/favicon.png
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | useRef,
3 | useEffect,
4 | useCallback,
5 | useState,
6 | useMemo
7 | } from "react";
8 | import { useSelector, useDispatch } from "react-redux";
9 | import {
10 | AlgorithmModals,
11 | IntroductionModal,
12 | Layout,
13 | MapPlot,
14 | Menu,
15 | SEO,
16 | ThemeToggle,
17 | CoordinateInput
18 | } from "../components";
19 | import { useSolverWorker, useAlgorithmInfo } from "../hooks";
20 | import * as selectors from "../store/selectors";
21 | import * as actions from "../store/actions";
22 |
23 | const IndexPage = () => {
24 | const mapRef = useRef(null);
25 | const dispatch = useDispatch();
26 |
27 | const algorithm = useSelector(selectors.selectAlgorithm);
28 | const algorithmInfo = useAlgorithmInfo();
29 | const delay = useSelector(selectors.selectDelay);
30 | const evaluatingDetailLevel = useSelector(
31 | selectors.selectEvaluatingDetailLevel
32 | );
33 | const points = useSelector(selectors.selectPoints);
34 | const pointCount = useSelector(selectors.selectPointCount);
35 | const definingPoints = useSelector(selectors.selectDefiningPoints);
36 |
37 | const solver = useSolverWorker(dispatch, algorithm);
38 |
39 | const onRandomizePoints = useCallback(() => {
40 | if (!definingPoints) {
41 | const bounds = mapRef.current.getBounds();
42 | dispatch(actions.randomizePoints(bounds, pointCount));
43 | }
44 | }, [mapRef, dispatch, pointCount, definingPoints]);
45 |
46 | const start = useCallback(() => {
47 | if (points.length < 2) {
48 | console.error("Not enough points to start solving.");
49 | return;
50 | }
51 |
52 | dispatch(actions.startSolving(points, delay, evaluatingDetailLevel));
53 | solver.postMessage(
54 | actions.startSolvingAction(points, delay, evaluatingDetailLevel)
55 | );
56 | }, [solver, dispatch, delay, points, evaluatingDetailLevel]);
57 |
58 | const fullSpeed = useCallback(() => {
59 | dispatch(actions.goFullSpeed());
60 | solver.postMessage(actions.goFullSpeed());
61 | }, [solver, dispatch]);
62 |
63 | const pause = useCallback(() => {
64 | dispatch(actions.pause());
65 | solver.postMessage(actions.pause());
66 | }, [solver, dispatch]);
67 |
68 | const unpause = useCallback(() => {
69 | dispatch(actions.unpause());
70 | solver.postMessage(actions.unpause());
71 | }, [solver, dispatch]);
72 |
73 | const stop = useCallback(() => {
74 | dispatch(actions.stopSolving());
75 | solver.terminate();
76 | }, [solver, dispatch]);
77 |
78 | const step = useCallback(() => {
79 | dispatch(actions.stepSolving());
80 | solver.postMessage(actions.stepSolving());
81 | }, [solver, dispatch]);
82 |
83 |
84 | useEffect(() => {
85 | solver.postMessage(actions.setDelay(delay));
86 | }, [delay, solver]);
87 |
88 | useEffect(() => {
89 | solver.postMessage(actions.setEvaluatingDetailLevel(evaluatingDetailLevel));
90 | }, [evaluatingDetailLevel, solver]);
91 |
92 | const algTitle = useMemo(() => {
93 | const alg = algorithmInfo.find(alg => alg.solverKey === algorithm);
94 | return alg.friendlyName;
95 | }, [algorithm, algorithmInfo]);
96 |
97 | return (
98 |
99 |
100 |
101 |
102 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default IndexPage;
117 |
--------------------------------------------------------------------------------
/src/solvers/cost.js:
--------------------------------------------------------------------------------
1 | // haversine great circle distance
2 | export const distance = (pt1, pt2) => {
3 | const [lng1, lat1] = pt1;
4 | const [lng2, lat2] = pt2;
5 | if (lat1 === lat2 && lng1 === lng2) {
6 | return 0;
7 | }
8 |
9 | var radlat1 = (Math.PI * lat1) / 180;
10 | var radlat2 = (Math.PI * lat2) / 180;
11 |
12 | var theta = lng1 - lng2;
13 | var radtheta = (Math.PI * theta) / 180;
14 |
15 | var dist =
16 | Math.sin(radlat1) * Math.sin(radlat2) +
17 | Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
18 |
19 | if (dist > 1) {
20 | dist = 1;
21 | }
22 | dist = Math.acos(dist);
23 | dist = (dist * 180) / Math.PI;
24 | return dist * 60 * 1.1515 * 1.609344;
25 | };
26 |
27 | export const pathCost = path => {
28 | return path
29 | .slice(0, -1)
30 | .map((point, idx) => distance(point, path[idx + 1]))
31 | .reduce((a, b) => a + b, 0);
32 | };
33 |
34 | export const counterClockWise = (p, q, r) => {
35 | return (q[0] - p[0]) * (r[1] - q[1]) < (q[1] - p[1]) * (r[0] - q[0]);
36 | };
37 |
38 | export const intersects = (a, b, c, d) => {
39 | return (
40 | counterClockWise(a, c, d) !== counterClockWise(b, c, d) &&
41 | counterClockWise(a, b, c) !== counterClockWise(a, b, d)
42 | );
43 | };
44 |
45 | export const setDifference = (setA, setB) => {
46 | const ret = new Set(setA);
47 | setB.forEach(p => {
48 | ret.delete(p);
49 | });
50 | return ret;
51 | };
52 |
53 | export const rotateToStartingPoint = (path, startingPoint) => {
54 | const startIdx = path.findIndex(p => p === startingPoint);
55 | path.unshift(...path.splice(startIdx, path.length));
56 | };
57 |
--------------------------------------------------------------------------------
/src/solvers/exhaustive/branchAndBoundOnCost.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, setDifference } from "../cost";
4 |
5 | import {
6 | EVALUATING_PATH_COLOR,
7 | EVALUATING_ERROR_COLOR,
8 | EVALUATING_SEGMENT_COLOR
9 | } from "../../constants";
10 |
11 | const branchAndBoundOnCost = async (
12 | points,
13 | path = [],
14 | visited = null,
15 | overallBest = Infinity
16 | ) => {
17 | if (visited === null) {
18 | // initial call
19 | path = [points.shift()];
20 | points = new Set(points);
21 | visited = new Set();
22 | }
23 |
24 | // figure out which points are left
25 | const available = setDifference(points, visited);
26 |
27 | // calculate the cost, from here, to go home
28 | const backToStart = [...path, path[0]];
29 | const cost = pathCost(backToStart);
30 |
31 | if (cost > overallBest) {
32 | // we may not be done, but have already traveled further than the best path
33 | // no reason to continue
34 | self.setEvaluatingPaths(
35 | () => ({
36 | paths: [
37 | {
38 | path: path.slice(0, path.length - 1),
39 | color: EVALUATING_SEGMENT_COLOR
40 | },
41 | {
42 | path: path.slice(path.length - 2, path.length + 1),
43 | color: EVALUATING_ERROR_COLOR
44 | }
45 | ],
46 | cost
47 | }),
48 | 2
49 | );
50 | await self.sleep();
51 |
52 | return [null, null];
53 | }
54 |
55 | // still cheaper than the best, keep going deeper, and deeper, and deeper...
56 | else {
57 | self.setEvaluatingPaths(
58 | () => ({
59 | paths: [
60 | {
61 | path: path.slice(0, path.length - 1),
62 | color: EVALUATING_SEGMENT_COLOR
63 | },
64 | {
65 | path: path.slice(path.length - 2, path.length + 1),
66 | color: EVALUATING_PATH_COLOR
67 | }
68 | ],
69 | cost
70 | }),
71 | 2
72 | );
73 | }
74 |
75 | await self.sleep();
76 |
77 | if (available.size === 0) {
78 | // at the end of the path, return where we're at
79 | self.setEvaluatingPath(() => ({
80 | path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
81 | cost
82 | }));
83 |
84 | return [cost, backToStart];
85 | }
86 |
87 | let [bestCost, bestPath] = [null, null];
88 |
89 | // for every point yet to be visited along this path
90 | for (const p of available) {
91 | // go to that point
92 | visited.add(p);
93 | path.push(p);
94 |
95 | // RECURSE - go through all the possible points from that point
96 | const [curCost, curPath] = await branchAndBoundOnCost(
97 | points,
98 | path,
99 | visited,
100 | overallBest
101 | );
102 |
103 | // if that path is better and complete, keep it
104 | if (curCost && (!bestCost || curCost < bestCost)) {
105 | [bestCost, bestPath] = [curCost, curPath];
106 |
107 | if (!overallBest || bestCost < overallBest) {
108 | // found a new best complete path
109 | overallBest = bestCost;
110 | self.setBestPath(bestPath, bestCost);
111 | }
112 | }
113 |
114 | // go back up and make that point available again
115 | visited.delete(p);
116 | path.pop();
117 |
118 | self.setEvaluatingPath(
119 | () => ({
120 | path: { path, color: EVALUATING_SEGMENT_COLOR }
121 | }),
122 | 2
123 | );
124 | await self.sleep();
125 | }
126 |
127 | await self.sleep();
128 | return [bestCost, bestPath];
129 | };
130 |
131 | makeSolver(branchAndBoundOnCost);
132 |
--------------------------------------------------------------------------------
/src/solvers/exhaustive/branchAndBoundOnCostAndCross.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, setDifference, intersects } from "../cost";
4 |
5 | import {
6 | EVALUATING_PATH_COLOR,
7 | EVALUATING_ERROR_COLOR,
8 | EVALUATING_SEGMENT_COLOR
9 | } from "../../constants";
10 |
11 | const branchAndBoundOnCostAndCross = async (
12 | points,
13 | path = [],
14 | visited = null,
15 | overallBest = Infinity
16 | ) => {
17 | if (visited === null) {
18 | // initial call
19 | path = [points.shift()];
20 | points = new Set(points);
21 | visited = new Set();
22 | }
23 |
24 | // figure out which points are left
25 | const available = setDifference(points, visited);
26 |
27 | // calculate the cost, from here, to go home
28 | const backToStart = [...path, path[0]];
29 | const cost = pathCost(backToStart);
30 |
31 | if (path.length > 3) {
32 | // if this newly added edge crosses over the existing path,
33 | // don't continue. It's been proven that an optimal path will
34 | // not cross itself.
35 | const newSegment = [path[path.length - 2], path[path.length - 1]];
36 | for (let i = 1; i < path.length - 2; i++) {
37 | if (intersects(path[i], path[i - 1], ...newSegment)) {
38 | self.setEvaluatingPaths(
39 | () => ({
40 | paths: [
41 | {
42 | path: path.slice(0, path.length - 1),
43 | color: EVALUATING_SEGMENT_COLOR
44 | },
45 | {
46 | path: path.slice(path.length - 2, path.length + 1),
47 | color: EVALUATING_ERROR_COLOR
48 | }
49 | ],
50 | cost
51 | }),
52 | 2
53 | );
54 | await self.sleep();
55 |
56 | return [null, null];
57 | }
58 | }
59 | }
60 |
61 | if (cost > overallBest) {
62 | // we may not be done, but have already traveled further than the best path
63 | // no reason to continue
64 | self.setEvaluatingPaths(
65 | () => ({
66 | paths: [
67 | {
68 | path: path.slice(0, path.length - 1),
69 | color: EVALUATING_SEGMENT_COLOR
70 | },
71 | {
72 | path: path.slice(path.length - 2, path.length + 1),
73 | color: EVALUATING_ERROR_COLOR
74 | }
75 | ],
76 | cost
77 | }),
78 | 2
79 | );
80 | await self.sleep();
81 |
82 | return [null, null];
83 | }
84 |
85 | // still cheaper than the best, keep going deeper, and deeper, and deeper...
86 | self.setEvaluatingPaths(
87 | () => ({
88 | paths: [
89 | {
90 | path: path.slice(0, path.length - 1),
91 | color: EVALUATING_SEGMENT_COLOR
92 | },
93 | {
94 | path: path.slice(path.length - 2, path.length + 1),
95 | color: EVALUATING_PATH_COLOR
96 | }
97 | ],
98 | cost
99 | }),
100 | 2
101 | );
102 | await self.sleep();
103 |
104 | if (available.size === 0) {
105 | // at the end of the path, return where we're at
106 | self.setEvaluatingPath(() => ({
107 | path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
108 | cost
109 | }));
110 |
111 | await self.sleep();
112 | return [cost, backToStart];
113 | }
114 |
115 | let [bestCost, bestPath] = [null, null];
116 |
117 | // for every point yet to be visited along this path
118 | for (const p of available) {
119 | // go to that point
120 | visited.add(p);
121 | path.push(p);
122 |
123 | // RECURSE - go through all the possible points from that point
124 | const [curCost, curPath] = await branchAndBoundOnCostAndCross(
125 | points,
126 | path,
127 | visited,
128 | overallBest
129 | );
130 |
131 | // if that path is better and complete, keep it
132 | if (curCost && (!bestCost || curCost < bestCost)) {
133 | [bestCost, bestPath] = [curCost, curPath];
134 |
135 | if (!overallBest || bestCost < overallBest) {
136 | // found a new best complete path
137 | overallBest = bestCost;
138 | self.setBestPath(bestPath, bestCost);
139 | }
140 | }
141 |
142 | // go back up and make that point available again
143 | visited.delete(p);
144 | path.pop();
145 |
146 | self.setEvaluatingPath(
147 | () => ({
148 | path: { path, color: EVALUATING_SEGMENT_COLOR }
149 | }),
150 | 2
151 | );
152 | await self.sleep();
153 | }
154 |
155 | await self.sleep();
156 | return [bestCost, bestPath];
157 | };
158 |
159 | makeSolver(branchAndBoundOnCostAndCross);
160 |
--------------------------------------------------------------------------------
/src/solvers/exhaustive/depthFirstSearch.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost } from "../cost";
4 |
5 | import {
6 | EVALUATING_PATH_COLOR,
7 | EVALUATING_SEGMENT_COLOR
8 | } from "../../constants";
9 |
10 | const setDifference = (setA, setB) => {
11 | const ret = new Set(setA);
12 | setB.forEach(p => {
13 | ret.delete(p);
14 | });
15 | return ret;
16 | };
17 |
18 | const dfs = async (points, path = [], visited = null, overallBest = null) => {
19 | if (visited === null) {
20 | // initial call
21 | path = [points.shift()];
22 | points = new Set(points);
23 | visited = new Set();
24 | }
25 |
26 | self.setEvaluatingPaths(
27 | () => ({
28 | paths: [
29 | {
30 | path: path.slice(0, path.length - 1),
31 | color: EVALUATING_SEGMENT_COLOR
32 | },
33 | {
34 | path: path.slice(path.length - 2, path.length + 1),
35 | color: EVALUATING_PATH_COLOR
36 | }
37 | ]
38 | }),
39 | 2
40 | );
41 | await self.sleep();
42 |
43 | // figure out what points are left from this point
44 | const available = setDifference(points, visited);
45 |
46 | if (available.size === 0) {
47 | // this must be a complete path
48 | const backToStart = [...path, path[0]];
49 |
50 | // calculate the cost of this path
51 | const cost = pathCost(backToStart);
52 |
53 | self.setEvaluatingPath(
54 | () => ({
55 | path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR }
56 | }),
57 | cost
58 | );
59 |
60 | await self.sleep();
61 |
62 | // return both the cost and the path where we're at
63 | return [cost, backToStart];
64 | }
65 |
66 | let [bestCost, bestPath] = [null, null];
67 |
68 | // for every point yet to be visited along this path
69 | for (const p of available) {
70 | // go to that point
71 | visited.add(p);
72 | path.push(p);
73 |
74 | // RECURSE - go through all the possible points from that point
75 | const [curCost, curPath] = await dfs(points, path, visited, overallBest);
76 |
77 | // if that path is better, keep it
78 | if (bestCost === null || curCost < bestCost) {
79 | [bestCost, bestPath] = [curCost, curPath];
80 |
81 | if (overallBest === null || bestCost < overallBest) {
82 | // found a new best complete path
83 | overallBest = bestCost;
84 | self.setBestPath(bestPath, bestCost);
85 | }
86 | }
87 |
88 | // go back up and make that point available again
89 | visited.delete(p);
90 | path.pop();
91 |
92 | self.setEvaluatingPath(
93 | () => ({
94 | path: { path, color: EVALUATING_SEGMENT_COLOR }
95 | }),
96 | 2
97 | );
98 | await self.sleep();
99 | }
100 | return [bestCost, bestPath];
101 | };
102 |
103 | makeSolver(dfs);
104 |
--------------------------------------------------------------------------------
/src/solvers/exhaustive/random.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost } from "../cost";
4 |
5 | const random = async points => {
6 | let best = Infinity;
7 |
8 | while (true) {
9 | // save off the starting point
10 | const start = points.shift();
11 |
12 | // sort the remaining points
13 | const path = points.sort(() => Math.random() - 0.5);
14 |
15 | // put the starting point back
16 | path.unshift(start);
17 |
18 | // return to the starting point
19 | path.push(start);
20 |
21 | // calculate the new cost
22 | const cost = pathCost(path);
23 |
24 | if (cost < best) {
25 | // we found a better path
26 | best = cost;
27 | self.setBestPath(path, cost);
28 | }
29 |
30 | self.setEvaluatingPaths(() => ({
31 | paths: [{ path }],
32 | cost
33 | }));
34 |
35 | // get rid of starting point at the end
36 | path.pop();
37 | await self.sleep();
38 | }
39 | };
40 |
41 | makeSolver(random);
42 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-construction/arbitraryInsertion.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, distance } from "../cost";
4 |
5 | const arbitraryInsertion = async points => {
6 | // from the starting point
7 | const path = [points.shift()];
8 |
9 | //
10 | // INITIALIZATION - go to the nearest point first
11 | //
12 | points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
13 | path.push(points.pop());
14 |
15 | self.setEvaluatingPaths(() => ({
16 | paths: [{ path }],
17 | cost: pathCost(path)
18 | }));
19 |
20 | // randomly sort points - this is the order they will be added
21 | // to the path
22 | points.sort(() => Math.random() - 0.5);
23 |
24 | while (points.length > 0) {
25 | //
26 | // SELECTION - choose a next point randomly
27 | //
28 | const nextPoint = points.pop();
29 |
30 | //
31 | // INSERTION - find the insertion spot that minimizes distance
32 | //
33 | let [bestCost, bestIdx] = [Infinity, null];
34 | for (let i = 1; i < path.length; i++) {
35 | const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
36 | if (insertionCost < bestCost) {
37 | [bestCost, bestIdx] = [insertionCost, i];
38 | }
39 | }
40 | path.splice(bestIdx, 0, nextPoint);
41 |
42 | self.setEvaluatingPaths(() => ({
43 | paths: [{ path }],
44 | cost: pathCost(path)
45 | }));
46 |
47 | await self.sleep();
48 | }
49 |
50 | // return to start after visiting all other points
51 | path.push(path[0]);
52 | const cost = pathCost(path);
53 |
54 | self.setEvaluatingPaths(() => ({
55 | paths: [{ path }],
56 | cost
57 | }));
58 | await self.sleep();
59 |
60 | self.setBestPath(path, cost);
61 | };
62 |
63 | makeSolver(arbitraryInsertion);
64 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-construction/convexHull.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, counterClockWise, rotateToStartingPoint } from "../cost";
4 | import {
5 | EVALUATING_PATH_COLOR,
6 | EVALUATING_SEGMENT_COLOR
7 | } from "../../constants";
8 |
9 | const convexHull = async points => {
10 | const sp = points[0];
11 |
12 | // Find the "left most point"
13 | let leftmost = points[0];
14 | for (const p of points) {
15 | if (p[1] < leftmost[1]) {
16 | leftmost = p;
17 | }
18 | }
19 |
20 | const path = [leftmost];
21 |
22 | while (true) {
23 | const curPoint = path[path.length - 1];
24 | let [selectedIdx, selectedPoint] = [0, null];
25 |
26 | // find the "most counterclockwise" point
27 | for (let [idx, p] of points.entries()) {
28 | // eslint-disable-next-line
29 | self.setEvaluatingPaths(
30 | () => ({
31 | paths: [
32 | {
33 | path: [...path, selectedPoint || curPoint],
34 | color: EVALUATING_SEGMENT_COLOR
35 | },
36 | { path: [curPoint, p], color: EVALUATING_PATH_COLOR }
37 | ]
38 | }),
39 | 2
40 | );
41 | await self.sleep();
42 |
43 | if (!selectedPoint || counterClockWise(curPoint, p, selectedPoint)) {
44 | // this point is counterclockwise with respect to the current hull
45 | // and selected point (e.g. more counterclockwise)
46 | [selectedIdx, selectedPoint] = [idx, p];
47 | }
48 | }
49 |
50 | // adding this to the hull so it's no longer available
51 | points.splice(selectedIdx, 1);
52 |
53 | // back to the furthest left point, formed a cycle, break
54 | if (selectedPoint === leftmost) {
55 | break;
56 | }
57 |
58 | // add to hull
59 | path.push(selectedPoint);
60 | }
61 |
62 | self.setEvaluatingPaths(() => ({
63 | paths: [{ path, color: EVALUATING_PATH_COLOR }],
64 | cost: pathCost(path)
65 | }));
66 | await self.sleep();
67 |
68 | while (points.length > 0) {
69 | let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
70 |
71 | for (let [freeIdx, freePoint] of points.entries()) {
72 | // for every free point, find the point in the current path
73 | // that minimizes the cost of adding the path minus the cost of
74 | // the original segment
75 | let [bestCost, bestIdx] = [Infinity, 0];
76 | for (let [pathIdx, pathPoint] of path.entries()) {
77 | const nextPathPoint = path[(pathIdx + 1) % path.length];
78 |
79 | // the new cost minus the old cost
80 | const evalCost =
81 | pathCost([pathPoint, freePoint, nextPathPoint]) -
82 | pathCost([pathPoint, nextPathPoint]);
83 |
84 | if (evalCost < bestCost) {
85 | [bestCost, bestIdx] = [evalCost, pathIdx];
86 | }
87 | }
88 |
89 | // figure out how "much" more expensive this is with respect to the
90 | // overall length of the segment
91 | const nextPoint = path[(bestIdx + 1) % path.length];
92 | const prevCost = pathCost([path[bestIdx], nextPoint]);
93 | const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
94 | const ratio = newCost / prevCost;
95 |
96 | if (ratio < bestRatio) {
97 | [bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
98 | }
99 | }
100 |
101 | const [nextPoint] = points.splice(bestPointIdx, 1);
102 | path.splice(insertIdx, 0, nextPoint);
103 |
104 | self.setEvaluatingPaths(() => ({
105 | paths: [{ path }],
106 | cost: pathCost(path)
107 | }));
108 | await self.sleep();
109 | }
110 |
111 | // rotate the array so that starting point is back first
112 | rotateToStartingPoint(path, sp);
113 |
114 | // go back home
115 | path.push(sp);
116 | const cost = pathCost(path);
117 |
118 | self.setEvaluatingPaths(() => ({
119 | paths: [{ path }],
120 | cost
121 | }));
122 | self.setBestPath(path, cost);
123 | };
124 |
125 | makeSolver(convexHull);
126 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-construction/furthestInsertion.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, distance } from "../cost";
4 |
5 | const furthestInsertion = async points => {
6 | // from the starting point
7 | const path = [points.shift()];
8 |
9 | //
10 | // INITIALIZATION - go to the nearest point first
11 | //
12 | points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
13 | path.push(points.pop());
14 |
15 | self.setEvaluatingPaths(() => ({
16 | paths: [{ path }],
17 | cost: pathCost(path)
18 | }));
19 |
20 | await self.sleep();
21 |
22 | while (points.length > 0) {
23 | //
24 | // SELECTION - furthest point from the path
25 | //
26 | let [selectedDistance, selectedIdx] = [0, null];
27 | for (const [freePointIdx, freePoint] of points.entries()) {
28 | // find the minimum distance to the path for freePoint
29 | let [bestCostToPath, costToPathIdx] = [Infinity, null];
30 | for (const pathPoint of path) {
31 | const dist = distance(freePoint, pathPoint);
32 | if (dist < bestCostToPath) {
33 | [bestCostToPath, costToPathIdx] = [dist, freePointIdx];
34 | }
35 | }
36 |
37 | // if this point is further from the path than the currently selected
38 | if (bestCostToPath > selectedDistance) {
39 | [selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
40 | }
41 | }
42 |
43 | // get the next point to add
44 | const [nextPoint] = points.splice(selectedIdx, 1);
45 |
46 | //
47 | // INSERTION - find the insertion spot that minimizes distance
48 | //
49 | let [bestCost, bestIdx] = [Infinity, null];
50 | for (let i = 1; i < path.length; i++) {
51 | const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
52 | if (insertionCost < bestCost) {
53 | [bestCost, bestIdx] = [insertionCost, i];
54 | }
55 | }
56 | path.splice(bestIdx, 0, nextPoint);
57 |
58 | self.setEvaluatingPaths(() => ({
59 | paths: [{ path }],
60 | cost: pathCost(path)
61 | }));
62 |
63 | await self.sleep();
64 | }
65 |
66 | // return to start after visiting all other points
67 | path.push(path[0]);
68 | const cost = pathCost(path);
69 |
70 | self.setEvaluatingPaths(() => ({
71 | paths: [{ path }],
72 | cost
73 | }));
74 | await self.sleep();
75 |
76 | self.setBestPath(path, cost);
77 | };
78 |
79 | makeSolver(furthestInsertion);
80 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-construction/nearestInsertion.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, distance } from "../cost";
4 |
5 | const nearestInsertion = async points => {
6 | // from the starting point
7 | const path = [points.shift()];
8 |
9 | //
10 | // INITIALIZATION - go to the nearest point first
11 | //
12 | points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
13 | path.push(points.pop());
14 |
15 | self.setEvaluatingPaths(() => ({
16 | paths: [{ path }],
17 | cost: pathCost(path)
18 | }));
19 |
20 | await self.sleep();
21 |
22 | while (points.length > 0) {
23 | //
24 | // SELECTION - nearest point to the path
25 | //
26 | let [selectedDistance, selectedIdx] = [Infinity, null];
27 | for (const [freePointIdx, freePoint] of points.entries()) {
28 | for (const pathPoint of path) {
29 | const dist = distance(freePoint, pathPoint);
30 | if (dist < selectedDistance) {
31 | [selectedDistance, selectedIdx] = [dist, freePointIdx];
32 | }
33 | }
34 | }
35 |
36 | // get the next point to add
37 | const [nextPoint] = points.splice(selectedIdx, 1);
38 |
39 | //
40 | // INSERTION - find the insertion spot that minimizes distance
41 | //
42 | let [bestCost, bestIdx] = [Infinity, null];
43 | for (let i = 1; i < path.length; i++) {
44 | const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
45 | if (insertionCost < bestCost) {
46 | [bestCost, bestIdx] = [insertionCost, i];
47 | }
48 | }
49 | path.splice(bestIdx, 0, nextPoint);
50 |
51 | self.setEvaluatingPaths(() => ({
52 | paths: [{ path }],
53 | cost: pathCost(path)
54 | }));
55 |
56 | await self.sleep();
57 | }
58 |
59 | // return to start after visiting all other points
60 | path.push(path[0]);
61 | const cost = pathCost(path);
62 |
63 | self.setEvaluatingPaths(() => ({
64 | paths: [{ path }],
65 | cost
66 | }));
67 | await self.sleep();
68 |
69 | self.setBestPath(path, cost);
70 | };
71 |
72 | makeSolver(nearestInsertion);
73 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-construction/nearestNeighbor.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, distance } from "../cost";
4 |
5 | const nearestNeighbor = async points => {
6 | const path = [points.shift()];
7 |
8 | while (points.length > 0) {
9 | // sort remaining points in place by their
10 | // distance from the last point in the current path
11 | points.sort(
12 | (a, b) =>
13 | distance(path[path.length - 1], b) - distance(path[path.length - 1], a)
14 | );
15 |
16 | // go to the closest remaining point
17 | path.push(points.pop());
18 |
19 | self.setEvaluatingPaths(() => ({
20 | paths: [{ path }],
21 | cost: pathCost(path)
22 | }));
23 |
24 | await self.sleep();
25 | }
26 |
27 | // return to start after visiting all other points
28 | path.push(path[0]);
29 | const cost = pathCost(path);
30 |
31 | self.setEvaluatingPaths(() => ({
32 | paths: [{ path }],
33 | cost
34 | }));
35 | await self.sleep();
36 |
37 | self.setBestPath(path, cost);
38 | };
39 |
40 | makeSolver(nearestNeighbor);
41 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-construction/simulatedAnnealing.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost, distance, rotateToStartingPoint } from "../cost";
4 | import { EVALUATING_PATH_COLOR } from "../../constants";
5 |
6 | const simulatedAnnealing = async points => {
7 | const sp = points[0];
8 | const path = points;
9 |
10 | const tempCoeff =
11 | path.length < 10
12 | ? 1 - 1e-4
13 | : path.length < 15
14 | ? 1 - 1e-5
15 | : path.length < 30
16 | ? 1 - 1e-6
17 | : 1 - 5e-7;
18 |
19 | const deltaDistance = (aIdx, bIdx) => {
20 | const aPrev = (aIdx - 1 + path.length) % path.length;
21 | const aNext = (aIdx + 1 + path.length) % path.length;
22 | const bPrev = (bIdx - 1 + path.length) % path.length;
23 | const bNext = (bIdx + 1 + path.length) % path.length;
24 | let diff =
25 | distance(path[bPrev], path[aIdx]) +
26 | distance(path[aIdx], path[bNext]) +
27 | distance(path[aPrev], path[bIdx]) +
28 | distance(path[bIdx], path[aNext]) -
29 | distance(path[aPrev], path[aIdx]) -
30 | distance(path[aIdx], path[aNext]) -
31 | distance(path[bPrev], path[bIdx]) -
32 | distance(path[bIdx], path[bNext]);
33 |
34 | if (bPrev === aIdx || bNext === aIdx) {
35 | diff += 2 * distance(path[aIdx], path[bIdx]);
36 | }
37 | return diff;
38 | };
39 |
40 | const changePath = temperature => {
41 | // 2 random points
42 | const a = 1 + Math.floor(Math.random() * (path.length - 1));
43 | const b = 1 + Math.floor(Math.random() * (path.length - 1));
44 |
45 | const delta = deltaDistance(a, b);
46 | if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
47 | // swap points
48 | [path[a], path[b]] = [path[b], path[a]];
49 | }
50 | };
51 |
52 | const initialTemp = 100 * distance(path[0], path[1]);
53 | let i = 0;
54 |
55 | for (
56 | let temperature = initialTemp;
57 | temperature > 1e-6;
58 | temperature *= tempCoeff
59 | ) {
60 | changePath(temperature);
61 | if (i % 10000 == 0) {
62 | self.setEvaluatingPaths(() => ({
63 | paths: [{ path, color: EVALUATING_PATH_COLOR }],
64 | cost: pathCost(path)
65 | }));
66 | await self.sleep();
67 | }
68 | if (i % 100000 == 0) {
69 | path.push(sp);
70 | self.setBestPath(path, pathCost(path));
71 | path.pop();
72 | }
73 | i++;
74 | }
75 |
76 | // rotate the array so that starting point is back first
77 | rotateToStartingPoint(path, sp);
78 |
79 | // go back home
80 | path.push(sp);
81 | const cost = pathCost(path);
82 |
83 | self.setEvaluatingPaths(() => ({
84 | paths: [{ path }],
85 | cost
86 | }));
87 | self.setBestPath(path, cost);
88 | };
89 |
90 | makeSolver(simulatedAnnealing);
91 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-improvement/twoOptInversion.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost } from "../cost";
4 |
5 | import {
6 | EVALUATING_PATH_COLOR,
7 | EVALUATING_SEGMENT_COLOR
8 | } from "../../constants";
9 |
10 | const twoOptInversion = async path => {
11 | path.push(path[0]);
12 | let best = pathCost(path);
13 | let swapped = true;
14 |
15 | self.setBestPath(path, best);
16 |
17 | while (swapped) {
18 | swapped = false;
19 | for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
20 | for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
21 | // section of the path to reverse
22 | const section = path.slice(pt1, pt2 + 1);
23 |
24 | // reverse section in place
25 | section.reverse();
26 |
27 | // replace section of path with reversed section in place
28 | path.splice(pt1, pt2 + 1 - pt1, ...section);
29 |
30 | // calculate new cost
31 | const newPath = path;
32 | const cost = pathCost(newPath);
33 |
34 | self.setEvaluatingPaths(() => ({
35 | paths: [
36 | { path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
37 | { path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
38 | { path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
39 | {
40 | path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
41 | color: EVALUATING_PATH_COLOR
42 | },
43 | {
44 | path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
45 | color: EVALUATING_PATH_COLOR
46 | }
47 | ],
48 | cost
49 | }));
50 | await self.sleep();
51 |
52 | if (cost < best) {
53 | // found a better path after the swap, keep it
54 | swapped = true;
55 | best = cost;
56 | self.setBestPath(newPath, best);
57 | } else {
58 | // un-reverse the section
59 | section.reverse();
60 | path.splice(pt1, pt2 + 1 - pt1, ...section);
61 | }
62 |
63 | self.setEvaluatingPaths(() => ({
64 | paths: [{ path, color: EVALUATING_SEGMENT_COLOR }]
65 | }));
66 | await self.sleep();
67 | }
68 | }
69 | }
70 | };
71 |
72 | makeSolver(twoOptInversion);
73 |
--------------------------------------------------------------------------------
/src/solvers/heuristic-improvement/twoOptReciprocalExchange.worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import makeSolver from "../makeSolver";
3 | import { pathCost } from "../cost";
4 |
5 | import {
6 | EVALUATING_PATH_COLOR,
7 | EVALUATING_SEGMENT_COLOR
8 | } from "../../constants";
9 |
10 | const twoOptReciprocalExchange = async path => {
11 | path.push(path[0]);
12 | let best = pathCost(path);
13 | let swapped = true;
14 |
15 | self.setBestPath(path, best);
16 |
17 | while (swapped) {
18 | swapped = false;
19 | for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
20 | for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
21 | // swap current pair of points
22 | [path[pt1], path[pt2]] = [path[pt2], path[pt1]];
23 |
24 | // calculate new cost
25 | const cost = pathCost(path);
26 |
27 | self.setEvaluatingPaths(() => ({
28 | paths: [
29 | { path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
30 | { path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
31 | { path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
32 | {
33 | path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
34 | color: EVALUATING_PATH_COLOR
35 | },
36 | {
37 | path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
38 | color: EVALUATING_PATH_COLOR
39 | }
40 | ],
41 | cost
42 | }));
43 | await self.sleep();
44 |
45 | if (cost < best) {
46 | // found a better path after the swap, keep it
47 | swapped = true;
48 | best = cost;
49 | self.setBestPath(path, best);
50 | } else {
51 | // swap back - this one's worse
52 | [path[pt1], path[pt2]] = [path[pt2], path[pt1]];
53 | }
54 |
55 | self.setEvaluatingPath(() => ({
56 | path: { path, color: EVALUATING_SEGMENT_COLOR }
57 | }));
58 |
59 | await self.sleep();
60 | }
61 | }
62 | }
63 | };
64 |
65 | makeSolver(twoOptReciprocalExchange);
66 |
--------------------------------------------------------------------------------
/src/solvers/index.js:
--------------------------------------------------------------------------------
1 | import random from "./exhaustive/random.worker";
2 | import depthFirstSearch from "./exhaustive/depthFirstSearch.worker";
3 | import branchAndBoundOnCost from "./exhaustive/branchAndBoundOnCost.worker";
4 | import branchAndBoundOnCostAndCross from "./exhaustive/branchAndBoundOnCostAndCross.worker";
5 |
6 | import nearestNeighbor from "./heuristic-construction/nearestNeighbor.worker";
7 | import arbitraryInsertion from "./heuristic-construction/arbitraryInsertion.worker";
8 | import nearestInsertion from "./heuristic-construction/nearestInsertion.worker";
9 | import furthestInsertion from "./heuristic-construction/furthestInsertion.worker";
10 | import convexHull from "./heuristic-construction/convexHull.worker";
11 | import simulatedAnnealing from "./heuristic-construction/simulatedAnnealing.worker";
12 |
13 | import twoOptInversion from "./heuristic-improvement/twoOptInversion.worker";
14 | import twoOptReciprocalExchange from "./heuristic-improvement/twoOptReciprocalExchange.worker";
15 |
16 | export default {
17 | random,
18 | depthFirstSearch,
19 | branchAndBoundOnCost,
20 | branchAndBoundOnCostAndCross,
21 |
22 | nearestNeighbor,
23 | arbitraryInsertion,
24 | furthestInsertion,
25 | nearestInsertion,
26 | convexHull,
27 | simulatedAnnealing,
28 |
29 | twoOptInversion,
30 | twoOptReciprocalExchange
31 | };
32 |
--------------------------------------------------------------------------------
/src/solvers/makeSolver.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import * as actions from "../store/actions";
3 |
4 | const wrapSolver = solver => async (...args) => {
5 | await solver(...args);
6 | self.postMessage(actions.stopSolvingAction());
7 | };
8 |
9 | export const makeSolver = solver => {
10 | const run = wrapSolver(solver);
11 |
12 | self.solverConfig = {
13 | detailLevel: 0,
14 | delay: 10,
15 | fullSpeed: false,
16 | paused: false,
17 | stepRequested: false
18 | };
19 |
20 |
21 | self.setBestPath = (...args) => {
22 | self.postMessage(actions.setBestPath(...args));
23 | };
24 |
25 |
26 | self.setEvaluatingPaths = (getPaths, level = 1) => {
27 | if (self.solverConfig.detailLevel >= level) {
28 | const { paths, cost } = getPaths();
29 | self.postMessage(actions.setEvaluatingPaths(paths, cost));
30 | }
31 | };
32 |
33 | self.setEvaluatingPath = (getPath, level = 1) => {
34 | if (self.solverConfig.detailLevel >= level) {
35 | const { path, cost } = getPath();
36 | self.postMessage(actions.setEvaluatingPath(path, cost));
37 | }
38 | };
39 |
40 |
41 | self.waitPause = async () => {
42 | while (self.solverConfig.paused && !self.solverConfig.stepRequested) {
43 | await new Promise(r => setTimeout(r, 50));
44 | }
45 | };
46 |
47 |
48 | self.sleep = async () => {
49 | if (self.solverConfig.paused) {
50 | await self.waitPause();
51 | }
52 |
53 | if (self.solverConfig.stepRequested) {
54 | self.solverConfig.stepRequested = false;
55 | return;
56 | }
57 |
58 | const ms = self.solverConfig.fullSpeed ? 0 : self.solverConfig.delay || 10;
59 | return new Promise(r => setTimeout(r, ms));
60 | };
61 |
62 |
63 |
64 | self.onmessage = async ({ data: action }) => {
65 | switch (action.type) {
66 | case actions.START_SOLVING:
67 | Object.assign(self.solverConfig, {
68 | delay: action.delay,
69 | detailLevel: action.evaluatingDetailLevel,
70 | fullSpeed: action.fullSpeed,
71 | paused: false,
72 | stepRequested: false
73 | });
74 | run(action.points);
75 | break;
76 |
77 | case actions.SET_DELAY:
78 | self.solverConfig.delay = action.delay;
79 | break;
80 |
81 | case actions.SET_EVALUATING_DETAIL_LEVEL:
82 | self.solverConfig.detailLevel = action.level;
83 | break;
84 |
85 | case actions.GO_FULL_SPEED:
86 | self.solverConfig.fullSpeed = true;
87 | self.solverConfig.paused = false;
88 | self.solverConfig.stepRequested = false;
89 | break;
90 |
91 | case actions.PAUSE:
92 | self.solverConfig.paused = true;
93 | break;
94 |
95 | case actions.UNPAUSE:
96 | self.solverConfig.paused = false;
97 | break;
98 |
99 | case actions.STEP_SOLVING:
100 | self.solverConfig.stepRequested = true;
101 | break;
102 |
103 | default:
104 | throw new Error(`invalid action sent to solver ${action.type}`);
105 | }
106 | };
107 | };
108 |
109 | export default makeSolver;
110 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import gtmEmit from "./emitCustomEvent";
2 |
3 | export const SET_VIEWPORT_STATE = "SET_VIEWPORT_STATE";
4 |
5 | export const RESET_EVALUATING_STATE = "RESET_EVALUATING_STATE";
6 | export const RESET_BEST_PATH_STATE = "RESET_BEST_PATH_STATE";
7 |
8 | export const SET_ALGORITHM = "SET_ALGORITHM";
9 | export const SET_DELAY = "SET_DELAY";
10 | export const SET_EVALUATING_DETAIL_LEVEL = "SET_EVALUATING_DETAIL_LEVEL";
11 | export const SET_SHOW_BEST_PATH = "SET_SHOW_BEST_PATH";
12 | export const START_SOLVING = "START_SOLVING";
13 | export const GO_FULL_SPEED = "GO_FULL_SPEED";
14 | export const PAUSE = "PAUSE";
15 | export const UNPAUSE = "UNPAUSE";
16 | export const STOP_SOLVING = "STOP_SOLVING";
17 | export const STEP_SOLVING = "STEP_SOLVING";
18 |
19 | export const SET_BEST_PATH = "SET_BEST_PATH";
20 | export const SET_EVALUATING_PATHS = "SET_EVALUATING_PATHS";
21 |
22 | export const START_DEFINING_POINTS = "START_DEFINING_POINTS";
23 | export const ADD_DEFINED_POINT = "ADD_DEFINED_POINT";
24 | export const STOP_DEFINING_POINTS = "STOP_DEFINING_POINTS";
25 | export const SET_POINT_COUNT = "SET_POINT_COUNT";
26 | export const SET_POINTS = "SET_POINTS";
27 | export const SET_DEFAULT_MAP = "SET_DEFAULT_MAP";
28 |
29 | export const TOGGLE_SITE_INFO_OPEN = "TOGGLE_SITE_INFO_OPEN";
30 | export const TOGGLE_ALG_INFO_OPEN = "TOGGLE_ALG_INFO_OPEN";
31 |
32 | export const UPDATE_POINTS = 'UPDATE_POINTS';
33 |
34 | const getRandomPoint = (max, min) => Math.random() * (max - min) + min;
35 |
36 | //
37 | // BASIC UI
38 | //
39 | export const toggleSiteInfoOpen = () => ({
40 | type: TOGGLE_SITE_INFO_OPEN
41 | });
42 |
43 | export const toggleAlgInfoOpen = () => ({
44 | type: TOGGLE_ALG_INFO_OPEN
45 | });
46 |
47 | //
48 | // MAP INTERACTION
49 | //
50 | export const setViewportState = viewport => ({
51 | type: SET_VIEWPORT_STATE,
52 | viewport
53 | });
54 |
55 | //
56 | // SOLVER CONTROLS
57 | //
58 | const resetEvaluatingStateAction = () => ({
59 | type: RESET_EVALUATING_STATE
60 | });
61 |
62 | const resetBestPathStateAction = () => ({
63 | type: RESET_BEST_PATH_STATE
64 | });
65 |
66 | const setAlgorithmAction = (algorithm, defaults) => ({
67 | type: SET_ALGORITHM,
68 | algorithm,
69 | defaults
70 | });
71 |
72 | export const startSolvingAction = (points, delay, evaluatingDetailLevel) => ({
73 | type: START_SOLVING,
74 | points,
75 | delay,
76 | evaluatingDetailLevel,
77 | fullSpeed: false
78 | });
79 |
80 | export const stopSolvingAction = () => ({
81 | type: STOP_SOLVING
82 | });
83 | export const stepSolving = () => ({
84 | type: STEP_SOLVING
85 | });
86 |
87 | export const setAlgorithm = (algorithm, defaults = {}) => dispatch => {
88 | dispatch(resetEvaluatingStateAction());
89 | dispatch(setAlgorithmAction(algorithm, defaults));
90 | };
91 |
92 | export const setDelay = delay => ({
93 | type: SET_DELAY,
94 | delay
95 | });
96 |
97 | export const setEvaluatingDetailLevel = level => ({
98 | type: SET_EVALUATING_DETAIL_LEVEL,
99 | level
100 | });
101 |
102 | export const setShowBestPath = show => ({
103 | type: SET_SHOW_BEST_PATH,
104 | show
105 | });
106 |
107 | export const resetSolverState = () => dispatch => {
108 | dispatch(stopSolving());
109 | dispatch(resetEvaluatingStateAction());
110 | dispatch(resetBestPathStateAction());
111 | };
112 |
113 | export const startSolving = (...args) => (dispatch, getState) => {
114 | const { algorithm, pointCount } = getState();
115 | gtmEmit({
116 | event: "start-solving",
117 | algorithm,
118 | pointCount
119 | });
120 | dispatch(resetEvaluatingStateAction());
121 | dispatch(startSolvingAction(...args));
122 | };
123 |
124 | export const goFullSpeed = () => ({
125 | type: GO_FULL_SPEED
126 | });
127 |
128 | export const pause = () => ({
129 | type: PAUSE
130 | });
131 |
132 | export const unpause = () => ({
133 | type: UNPAUSE
134 | });
135 |
136 | export const stopSolving = () => dispatch => {
137 | dispatch(resetEvaluatingStateAction());
138 | dispatch(stopSolvingAction());
139 | };
140 |
141 | //
142 | // SOLVER ACTIONS
143 | //
144 | export const setEvaluatingPath = (path, cost) => ({
145 | type: SET_EVALUATING_PATHS,
146 | paths: [path],
147 | cost
148 | });
149 |
150 | export const setEvaluatingPaths = (paths, cost) => ({
151 | type: SET_EVALUATING_PATHS,
152 | paths,
153 | cost
154 | });
155 |
156 | export const setBestPath = (path, cost) => ({
157 | type: SET_BEST_PATH,
158 | path,
159 | cost
160 | });
161 |
162 | //
163 | // POINT CONTROLS
164 | //
165 | const setDefaultMapAction = () => ({
166 | type: SET_DEFAULT_MAP
167 | });
168 |
169 | const setPointsAction = points => ({
170 | type: SET_POINTS,
171 | points
172 | });
173 |
174 | const setPointCountAction = count => ({
175 | type: SET_POINT_COUNT,
176 | count
177 | });
178 |
179 | const startDefiningPointsAction = () => ({
180 | type: START_DEFINING_POINTS
181 | });
182 |
183 | export const startDefiningPoints = () => dispatch => {
184 | dispatch(resetSolverState());
185 | dispatch(startDefiningPointsAction());
186 | };
187 |
188 | export const addDefinedPoint = point => ({
189 | type: ADD_DEFINED_POINT,
190 | point
191 | });
192 |
193 | export const stopDefiningPoints = () => ({
194 | type: STOP_DEFINING_POINTS
195 | });
196 |
197 | export const setPointCount = count => dispatch => {
198 | dispatch(resetSolverState());
199 | dispatch(setPointCountAction(count));
200 | };
201 |
202 | export const randomizePoints = bounds => (dispatch, getState) => {
203 | const { pointCount } = getState();
204 | const { top, bottom, left, right } = bounds;
205 | const points = Array.from({ length: pointCount }).map(_ => [
206 | getRandomPoint(right, left),
207 | getRandomPoint(top, bottom)
208 | ]);
209 | dispatch(resetSolverState());
210 | dispatch(setPointsAction(points));
211 | };
212 |
213 | export const setDefaultMap = (...args) => dispatch => {
214 | dispatch(resetSolverState());
215 | dispatch(setDefaultMapAction());
216 | };
217 |
218 | export const updatePoints = (points) => ({
219 | type: UPDATE_POINTS,
220 | payload: points,
221 | });
222 |
--------------------------------------------------------------------------------
/src/store/emitCustomEvent.js:
--------------------------------------------------------------------------------
1 | export default ev => {
2 | if (typeof window !== "undefined" && window.dataLayer) {
3 | window.dataLayer.push(ev);
4 | } else {
5 | console.log(ev);
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/store/reducer.js:
--------------------------------------------------------------------------------
1 | import * as actions from "./actions";
2 |
3 | const usTop12 = [
4 | [-73.85835427500902, 40.56507951957753],
5 | [-77.54976052500858, 38.772432514145194],
6 | [-78.91206521250587, 42.66742768420476],
7 | [-70.95796365000933, 42.66742768420476],
8 | [-80.27436990000314, 26.176558881220437],
9 | [-84.4052292750001, 34.108547937473524],
10 | [-82.55952615000031, 28.24770207922181],
11 | [-84.66890115000008, 30.089457425014395],
12 | [-89.89839333750201, 29.746655988569763],
13 | [-96.62202615000125, 32.640688397241334],
14 | [-95.3036667750014, 29.287759374472813],
15 | [-97.76460427500368, 30.089457425014395],
16 | [-101.89546365000065, 34.97727964358472],
17 | [-112.22261208749687, 33.23080293029681],
18 | [-111.38765114999953, 35.01327961148759],
19 | [-115.56245583750162, 36.08588188690158],
20 | [-118.63862771249869, 33.999320468363095],
21 | [-117.2323777124963, 32.97311239658548],
22 | [-123.12104958749816, 38.222145234071036],
23 | [-124.26362771250061, 41.13019627380825],
24 | [-120.13276833749595, 39.72528830651809],
25 | [-111.82710427499693, 41.13019627380825],
26 | [-105.2353073999977, 39.961475963760066],
27 | [-87.43745583749975, 41.69048709677229],
28 | [-93.1064011499991, 45.29144400095841],
29 | [-90.20601052499944, 38.772432514145194],
30 | [-117.27632302500142, 47.50341272285311],
31 | [-122.72554177499823, 45.8757982618686],
32 | [-122.81343240000076, 48.152468818056875]
33 | ];
34 |
35 | const initialViewport = {
36 | latitude: 39.8097343,
37 | longitude: -98.5556199,
38 | zoom: 4
39 | };
40 |
41 | const initialState = {
42 | points: usTop12.sort(() => Math.random() + 0.5),
43 | viewport: initialViewport,
44 | algorithm: "convexHull",
45 | delay: 100,
46 | evaluatingDetailLevel: 2,
47 | maxEvaluatingDetailLevel: 2,
48 | showBestPath: true,
49 |
50 | bestPath: [],
51 | bestDisplaySegments: [],
52 | bestCost: null,
53 |
54 | evaluatingPaths: [],
55 | evaluatingCost: null,
56 | running: false,
57 | fullSpeed: false,
58 | paused: false,
59 | startedRunningAt: null,
60 |
61 | pointCount: usTop12.length,
62 | definingPoints: false,
63 |
64 | siteInfoOpen: false,
65 | algInfoOpen: false
66 | };
67 |
68 | const reducer = (state = initialState, action) => {
69 | switch (action.type) {
70 | case actions.TOGGLE_SITE_INFO_OPEN:
71 | return {
72 | ...state,
73 | siteInfoOpen: !state.siteInfoOpen
74 | };
75 |
76 | case actions.TOGGLE_ALG_INFO_OPEN:
77 | return {
78 | ...state,
79 | algInfoOpen: !state.algInfoOpen
80 | };
81 |
82 | case actions.SET_VIEWPORT_STATE:
83 | return {
84 | ...state,
85 | viewport: action.viewport
86 | };
87 |
88 | case actions.RESET_EVALUATING_STATE:
89 | return {
90 | ...state,
91 | evaluatingPaths: [],
92 | evaluatingCost: null
93 | };
94 |
95 | case actions.RESET_BEST_PATH_STATE:
96 | return {
97 | ...state,
98 | bestPath: [],
99 | bestCost: null
100 | };
101 |
102 | //
103 | // SOLVER CONTROLS
104 | //
105 | case actions.SET_ALGORITHM:
106 | return {
107 | ...state,
108 | ...action.defaults,
109 | algorithm: action.algorithm
110 | };
111 |
112 | case actions.SET_DELAY:
113 | return {
114 | ...state,
115 | delay: action.delay
116 | };
117 |
118 | case actions.SET_EVALUATING_DETAIL_LEVEL:
119 | return {
120 | ...state,
121 | evaluatingDetailLevel: action.level,
122 | evaluatingPaths: action.level ? state.evaluatingPaths : [],
123 | evaluatingCost: action.level ? state.evaluatingCost : null
124 | };
125 |
126 | case actions.SET_SHOW_BEST_PATH:
127 | return {
128 | ...state,
129 | showBestPath: action.show
130 | };
131 |
132 | case actions.START_SOLVING:
133 | return {
134 | ...state,
135 | showBestPath: false,
136 | running: true,
137 | startedRunningAt: Date.now(),
138 | pointCount: state.points.length
139 | };
140 |
141 | case actions.GO_FULL_SPEED:
142 | return {
143 | ...state,
144 | showBestPath: true,
145 | evaluatingDetailLevel: 0,
146 | evaluatingPaths: [],
147 | fullSpeed: true
148 | };
149 |
150 | case actions.PAUSE:
151 | return {
152 | ...state,
153 | paused: true,
154 | running: false
155 | };
156 |
157 | case actions.UNPAUSE:
158 | return {
159 | ...state,
160 | paused: false,
161 | running: true
162 | };
163 |
164 | case actions.STOP_SOLVING:
165 | return {
166 | ...state,
167 | points:
168 | state.bestPath.length > 0
169 | ? state.bestPath.slice(0, state.bestPath.length - 1)
170 | : state.points,
171 | showBestPath: true,
172 | running: false,
173 | paused: false,
174 | fullSpeed: false,
175 | startedRunningAt: null
176 | };
177 |
178 | //
179 | // SOLVER ACTIONS
180 | //
181 | case actions.SET_EVALUATING_PATHS:
182 | return {
183 | ...state,
184 | evaluatingPaths: state.evaluatingDetailLevel ? action.paths : [],
185 | evaluatingCost: state.evaluatingDetailLevel ? action.cost : null
186 | };
187 |
188 | case actions.SET_BEST_PATH:
189 | return {
190 | ...state,
191 | bestPath: action.path,
192 | bestCost: action.cost
193 | };
194 |
195 | //
196 | // POINT CONTROLS
197 | //
198 | case actions.SET_POINT_COUNT:
199 | return {
200 | ...state,
201 | pointCount: action.count
202 | };
203 |
204 | case actions.SET_POINTS:
205 | return {
206 | ...state,
207 | points: action.points
208 | };
209 |
210 | case actions.START_DEFINING_POINTS:
211 | return {
212 | ...state,
213 | points: [],
214 | definingPoints: true,
215 | pointCount: 0
216 | };
217 |
218 | case actions.ADD_DEFINED_POINT:
219 | return {
220 | ...state,
221 | points: [...state.points, action.point],
222 | pointCount: state.pointCount + 1
223 | };
224 |
225 | case actions.STOP_DEFINING_POINTS:
226 | return {
227 | ...state,
228 | definingPoints: false
229 | };
230 |
231 | case actions.SET_DEFAULT_MAP:
232 | return {
233 | ...state,
234 | viewport: initialViewport,
235 | points: usTop12,
236 | pointCount: usTop12.length
237 | };
238 |
239 | case actions.UPDATE_POINTS:
240 | return {
241 | ...state,
242 | points: action.payload,
243 | pointCount: action.payload.length,
244 | };
245 |
246 | default:
247 | return state;
248 | }
249 | };
250 |
251 | export default reducer;
252 |
--------------------------------------------------------------------------------
/src/store/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect";
2 | import {
3 | START_POINT_COLOR,
4 | POINT_COLOR,
5 | BEST_PATH_COLOR,
6 | EVALUATING_PATH_COLOR
7 | } from "../constants";
8 |
9 | //
10 | // FOR UI
11 | //
12 | export const selectSiteInfoOpen = state => state.siteInfoOpen;
13 | export const selectAlgInfoOpen = state => state.algInfoOpen;
14 |
15 | //
16 | // FOR SOLVER CONTROLS
17 | //
18 | export const selectAlgorithm = state => state.algorithm;
19 |
20 | export const selectDelay = state => state.delay;
21 |
22 | export const selectEvaluatingDetailLevel = state => state.evaluatingDetailLevel;
23 |
24 | export const selectMaxEvaluatingDetailLevel = state =>
25 | state.maxEvaluatingDetailLevel;
26 |
27 | export const selectRunning = state => state.running;
28 |
29 | export const selectFullSpeed = state => state.fullSpeed;
30 |
31 | export const selectPaused = state => state.paused;
32 |
33 | export const selectStartedRunningAt = state => state.startedRunningAt;
34 |
35 | //
36 | // FOR POINT CONTROLS
37 | //
38 | export const selectDefiningPoints = state => state.definingPoints;
39 |
40 | export const selectPointCount = state => state.pointCount;
41 |
42 | //
43 | // FOR PLOT
44 | //
45 |
46 | export const selectViewport = state => state.viewport;
47 |
48 | export const selectPoints = state => state.points;
49 | export const selectPointsDisplay = createSelector(selectPoints, points =>
50 | points.map((p, idx) => ({
51 | position: [p[0], p[1]], // Correct order: [longitude, latitude]
52 | color: idx === 0 ? START_POINT_COLOR : POINT_COLOR
53 | }))
54 | );
55 |
56 | export const selectShowBestPath = state => state.showBestPath;
57 | export const selectBestPath = state => state.bestPath;
58 | export const selectBestPathDisplay = createSelector(
59 | selectBestPath,
60 | selectShowBestPath,
61 | (path, show) => ({
62 | path: show ? path : [],
63 | color: BEST_PATH_COLOR,
64 | width: 20
65 | })
66 | );
67 |
68 | export const selectBestCost = state => state.bestCost;
69 | export const selectBestCostDisplay = createSelector(selectBestCost, cost =>
70 | cost ? cost.toFixed(2) : ""
71 | );
72 |
73 | export const selectEvaluatingPaths = state => state.evaluatingPaths;
74 | export const selectEvaluatingPathsDisplay = createSelector(
75 | selectEvaluatingPaths,
76 | paths =>
77 | paths.map(({ path, color }) => ({
78 | path,
79 | color: color || EVALUATING_PATH_COLOR,
80 | width: 5
81 | }))
82 | );
83 |
84 | export const selectEvaluatingCost = state => state.evaluatingCost;
85 | export const selectEvaluatingCostDisplay = createSelector(
86 | selectEvaluatingCost,
87 | cost => (cost ? cost.toFixed(2) : "")
88 | );
89 |
90 | export const selectPlotPaths = createSelector(
91 | selectBestPathDisplay,
92 | selectEvaluatingPathsDisplay,
93 | (bestPath, evaluatingPaths) => [...evaluatingPaths, bestPath]
94 | );
95 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from "redux";
2 | import thunk from "redux-thunk";
3 | import reducer from "./reducer";
4 |
5 | export default createStore(reducer, applyMiddleware(thunk));
6 |
--------------------------------------------------------------------------------
/static/.well-known/brave-rewards-verification.txt:
--------------------------------------------------------------------------------
1 | This is a Brave Rewards publisher verification file.
2 |
3 | Domain: tspvis.com
4 | Token: b687d7e75fc19d979ab3ab8b264164a91d8206d0bb2ffa33334069d9092406b1
--------------------------------------------------------------------------------
/static/CNAME:
--------------------------------------------------------------------------------
1 | tspvis.com
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------