feat: first commit
This commit is contained in:
commit
8b0c05008e
79 changed files with 30897 additions and 0 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules/
|
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends: "react-app",
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
SharedArrayBuffer: "readonly",
|
||||
__PATH_PREFIX__: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: ["react"]
|
||||
};
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules/
|
5
.prettierignore
Normal file
5
.prettierignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
.vscode/
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2
|
||||
}
|
22
Dockerfile
Normal file
22
Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
|||
FROM node:latest
|
||||
|
||||
# Install Python, make, and build dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
make \
|
||||
g++ \
|
||||
libvips-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Jeff Hackshaw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
compose.yaml
Normal file
3
compose.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
services:
|
||||
nodejs-app:
|
||||
build: .
|
17
gatsby-browser.js
Normal file
17
gatsby-browser.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./src/store/store";
|
||||
import "react-vis/dist/style.css";
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
import "typeface-roboto";
|
||||
import { ThemeContextProvider } from "./src/context";
|
||||
|
||||
export const wrapRootElement = ({ element }) => (
|
||||
<ThemeContextProvider>
|
||||
<Provider store={store}>{element}</Provider>
|
||||
</ThemeContextProvider>
|
||||
);
|
||||
|
||||
export const onServiceWorkerUpdateReady = () => {
|
||||
window.location.reload();
|
||||
};
|
44
gatsby-config.js
Normal file
44
gatsby-config.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: `Traveling Salesman Problem Solver`,
|
||||
description: ``,
|
||||
author: `@jhackshaw`
|
||||
},
|
||||
plugins: [
|
||||
`gatsby-plugin-material-ui`,
|
||||
`gatsby-plugin-react-helmet`,
|
||||
{
|
||||
resolve: "gatsby-plugin-google-tagmanager",
|
||||
options: {
|
||||
id: "GTM-PS28HLQ",
|
||||
includeInDevelopment: false
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
name: "content",
|
||||
path: `${__dirname}/src/content`
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-transformer-remark`,
|
||||
options: {
|
||||
plugins: [`gatsby-remark-prismjs`]
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-plugin-manifest`,
|
||||
options: {
|
||||
name: `tsp visualizer`,
|
||||
short_name: `tspvis`,
|
||||
start_url: `/`,
|
||||
background_color: `#663399`,
|
||||
theme_color: `#663399`,
|
||||
display: `fullscreen`,
|
||||
icon: `src/images/favicon.png`
|
||||
}
|
||||
},
|
||||
`gatsby-plugin-offline`
|
||||
]
|
||||
};
|
20
gatsby-node.js
Normal file
20
gatsby-node.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
exports.onCreateWebpackConfig = ({
|
||||
actions: { replaceWebpackConfig },
|
||||
getConfig
|
||||
}) => {
|
||||
const config = getConfig();
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: "worker-loader",
|
||||
options: {
|
||||
inline: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
config.output.globalObject = "this";
|
||||
|
||||
replaceWebpackConfig(config);
|
||||
};
|
17
gatsby-ssr.js
Normal file
17
gatsby-ssr.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import { ThemeContextProvider, PreSetTheme } from "./src/context";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./src/store/store";
|
||||
import "react-vis/dist/style.css";
|
||||
|
||||
export const wrapRootElement = ({ element }) => {
|
||||
return (
|
||||
<ThemeContextProvider>
|
||||
<Provider store={store}>{element}</Provider>
|
||||
</ThemeContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const onRenderBody = ({ setPreBodyComponents }) => {
|
||||
setPreBodyComponents([<PreSetTheme key="prerender-theme" />]);
|
||||
};
|
26594
package-lock.json
generated
Normal file
26594
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
63
package.json
Normal file
63
package.json
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "tsp-visualizer",
|
||||
"private": true,
|
||||
"description": "Visualizer for the traveling salesman problem",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/styles": "^4.11.3",
|
||||
"deck.gl": "^7.3.15",
|
||||
"eslint-config-react-app": "^5.2.1",
|
||||
"gatsby": "^2.32.12",
|
||||
"gatsby-cli": "^2.19.2",
|
||||
"gatsby-plugin-google-tagmanager": "^2.11.0",
|
||||
"gatsby-plugin-manifest": "^2.12.1",
|
||||
"gatsby-plugin-material-ui": "^2.1.6",
|
||||
"gatsby-plugin-offline": "^3.10.2",
|
||||
"gatsby-plugin-react-helmet": "^3.10.0",
|
||||
"gatsby-remark-prismjs": "^3.13.0",
|
||||
"gatsby-source-filesystem": "^2.11.1",
|
||||
"gatsby-transformer-remark": "^2.16.1",
|
||||
"gh-pages": "^2.2.0",
|
||||
"prismjs": "^1.23.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-map-gl": "^5.3.12",
|
||||
"react-redux": "^7.2.3",
|
||||
"react-vis": "^1.11.7",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"prettier": "^1.19.1"
|
||||
},
|
||||
"keywords": [
|
||||
"gatsby"
|
||||
],
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "gatsby build",
|
||||
"develop": "gatsby develop",
|
||||
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
|
||||
"start": "npm run develop",
|
||||
"serve": "gatsby serve",
|
||||
"clean": "gatsby clean"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jhackshaw/tspvis"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/jhackshaw/tspvis/issues"
|
||||
}
|
||||
}
|
33
src/components/AlgorithmModals.jsx
Normal file
33
src/components/AlgorithmModals.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import { useAlgorithmInfo } from "../hooks";
|
||||
import * as selectors from "../store/selectors";
|
||||
import * as actions from "../store/actions";
|
||||
|
||||
import { InformationModal } from "./InformationModal";
|
||||
|
||||
export const AlgorithmModals = props => {
|
||||
const dispatch = useDispatch();
|
||||
const algorithms = useAlgorithmInfo();
|
||||
const selectedAlgorithm = useSelector(selectors.selectAlgorithm);
|
||||
const open = useSelector(selectors.selectAlgInfoOpen);
|
||||
|
||||
const onClose = () => {
|
||||
dispatch(actions.toggleAlgInfoOpen());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{algorithms.map(alg => (
|
||||
<InformationModal
|
||||
key={alg.solverKey}
|
||||
open={open && selectedAlgorithm === alg.solverKey}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: alg.html }} />
|
||||
</InformationModal>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
55
src/components/InformationModal.jsx
Normal file
55
src/components/InformationModal.jsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from "@material-ui/core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
closeButton: {
|
||||
position: "absolute",
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500]
|
||||
},
|
||||
root: {
|
||||
zIndex: "10000 !important"
|
||||
}
|
||||
}));
|
||||
|
||||
export const InformationModal = ({ open, onClose, children }) => {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullScreen={fullScreen}
|
||||
maxWidth="md"
|
||||
scroll="paper"
|
||||
keepMounted
|
||||
disablePortal
|
||||
fullWidth
|
||||
classes={{ root: classes.root, paper: classes.root }}
|
||||
>
|
||||
<DialogContent>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={onClose}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWindowClose} width="0" />
|
||||
</IconButton>
|
||||
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
24
src/components/IntroductionModal.jsx
Normal file
24
src/components/IntroductionModal.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import * as selectors from "../store/selectors";
|
||||
import * as actions from "../store/actions";
|
||||
import { useIntroductionInfo } from "../hooks";
|
||||
|
||||
import { InformationModal } from "./InformationModal";
|
||||
|
||||
export const IntroductionModal = props => {
|
||||
const dispatch = useDispatch();
|
||||
const introduction = useIntroductionInfo();
|
||||
const open = useSelector(selectors.selectSiteInfoOpen);
|
||||
|
||||
const onClose = () => {
|
||||
dispatch(actions.toggleSiteInfoOpen());
|
||||
};
|
||||
|
||||
return (
|
||||
<InformationModal open={open} onClose={onClose}>
|
||||
<div dangerouslySetInnerHTML={{ __html: introduction }} />
|
||||
</InformationModal>
|
||||
);
|
||||
};
|
23
src/components/Layout.jsx
Normal file
23
src/components/Layout.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
flexDirection: "row"
|
||||
},
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
container: {
|
||||
justifyContent: "flex-end",
|
||||
flexDirection: "column-reverse"
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export const Layout = ({ children }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return <div className={classes.container}>{children}</div>;
|
||||
};
|
107
src/components/MapPlot.jsx
Normal file
107
src/components/MapPlot.jsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import React, { useRef, useImperativeHandle, useEffect, useMemo } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useMediaQuery } from "@material-ui/core";
|
||||
import MapGL from "react-map-gl";
|
||||
import "mapbox-gl/dist/mapbox-gl.css";
|
||||
import DeckGL from '@deck.gl/react';
|
||||
import { ScatterplotLayer, PathLayer } from "@deck.gl/layers";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
import * as actions from "../store/actions";
|
||||
import * as selectors from "../store/selectors";
|
||||
import { useThemeContext } from "../context";
|
||||
|
||||
// not secret
|
||||
const TOKEN =
|
||||
"pk.eyJ1IjoiaW50cmVwaWRldiIsImEiOiJjazBpa2M5YnowMHcyM21ubzgycW8zZHJmIn0.DCO2aRA6MJweC8HN-d_cgQ";
|
||||
|
||||
export const MapPlot = React.forwardRef((props, ref) => {
|
||||
const { children } = props;
|
||||
const { muiTheme, colorMode } = useThemeContext();
|
||||
const matches = useMediaQuery(muiTheme.breakpoints.down("sm"));
|
||||
const mapGlRef = useRef();
|
||||
const plotPoints = useSelector(selectors.selectPointsDisplay);
|
||||
const plotPaths = useSelector(selectors.selectPlotPaths);
|
||||
const viewport = useSelector(selectors.selectViewport);
|
||||
const running = useSelector(selectors.selectRunning);
|
||||
const definingPoints = useSelector(selectors.selectDefiningPoints);
|
||||
const mapStyle = useMemo(() =>
|
||||
colorMode === "dark"
|
||||
? "mapbox://styles/mapbox/dark-v8"
|
||||
: "mapbox://styles/mapbox/light-v8"
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getBounds: () => {
|
||||
const map = mapGlRef.current.getMap();
|
||||
const { _ne, _sw } = map.getBounds();
|
||||
return {
|
||||
top: _ne.lat,
|
||||
bottom: _sw.lat,
|
||||
left: _ne.lng,
|
||||
right: _sw.lng
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (matches) {
|
||||
dispatch(
|
||||
actions.setViewportState({
|
||||
...viewport,
|
||||
zoom: 2
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [matches, dispatch]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const onViewportChanged = viewport => {
|
||||
dispatch(actions.setViewportState(viewport));
|
||||
};
|
||||
|
||||
const onDefinedPoint = ({ lngLat }) => {
|
||||
dispatch(actions.addDefinedPoint(lngLat));
|
||||
};
|
||||
const layers = useMemo(() => [
|
||||
new PathLayer({
|
||||
id: "path-layer",
|
||||
data: plotPaths,
|
||||
getPath: d => d.path,
|
||||
getColor: d => d.color,
|
||||
pickable: true,
|
||||
widthMinPixels: 4,
|
||||
widthMaxPixels: 8
|
||||
}),
|
||||
new ScatterplotLayer({
|
||||
id: "scatter-layer",
|
||||
data: plotPoints,
|
||||
pickable: true,
|
||||
opacity: 0.8,
|
||||
getFillColor: p => p.color,
|
||||
radiusMinPixels: 6,
|
||||
radiusMaxPixels: 8
|
||||
})
|
||||
], [plotPaths, plotPoints]);
|
||||
|
||||
return (
|
||||
|
||||
<MapGL
|
||||
{...viewport}
|
||||
ref={mapGlRef}
|
||||
width="100%"
|
||||
height={matches ? "50%" : "100%"}
|
||||
maxPitch={0}
|
||||
onViewportChange={onViewportChanged}
|
||||
mapboxApiAccessToken={TOKEN}
|
||||
disableTokenWarning={true}
|
||||
onNativeClick={definingPoints && onDefinedPoint}
|
||||
doubleClickZoom={false}
|
||||
mapStyle={mapStyle}
|
||||
>
|
||||
{running && <LinearProgress color="secondary" />}
|
||||
<DeckGL viewState={viewport} layers = {layers} />
|
||||
|
||||
{children}
|
||||
</MapGL>
|
||||
);
|
||||
});
|
54
src/components/Menu.jsx
Normal file
54
src/components/Menu.jsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import { Paper, Divider } from "@material-ui/core";
|
||||
import { MenuHeader } from "./MenuHeader";
|
||||
import { MenuSolverControls } from "./MenuSolverControls";
|
||||
import { MenuMetrics } from "./MenuMetrics";
|
||||
import { MenuPointControls } from "./MenuPointControls";
|
||||
import { OtherControls } from "./OtherControls";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
wrapper: {
|
||||
overflowY: "auto",
|
||||
flex: "0 0 400px",
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexWrap: "nowrap",
|
||||
alginItems: "flex-start",
|
||||
zIndex: 100
|
||||
},
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
width: "100%"
|
||||
}
|
||||
}));
|
||||
|
||||
export const Menu = ({
|
||||
onStart,
|
||||
onPause,
|
||||
onUnPause,
|
||||
onFullSpeed,
|
||||
onStop,
|
||||
onRandomizePoints
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Paper classes={{ root: classes.wrapper }}>
|
||||
<MenuHeader />
|
||||
<Divider />
|
||||
<MenuMetrics />
|
||||
<MenuSolverControls
|
||||
onStart={onStart}
|
||||
onPause={onPause}
|
||||
onUnPause={onUnPause}
|
||||
onStop={onStop}
|
||||
onFullSpeed={onFullSpeed}
|
||||
/>
|
||||
<Divider />
|
||||
<MenuPointControls onRandomizePoints={onRandomizePoints} />
|
||||
<Divider />
|
||||
<OtherControls />
|
||||
</Paper>
|
||||
);
|
||||
};
|
73
src/components/MenuHeader.jsx
Normal file
73
src/components/MenuHeader.jsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Grid, Typography, IconButton, Tooltip } from "@material-ui/core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faInfoCircle,
|
||||
faBriefcase,
|
||||
faDonate
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import { MenuSection } from "./MenuSection";
|
||||
|
||||
import * as actions from "../store/actions";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
paddingTop: theme.spacing(3),
|
||||
paddingBottom: theme.spacing(3)
|
||||
},
|
||||
title: {
|
||||
fontSize: "1.2rem"
|
||||
}
|
||||
}));
|
||||
|
||||
export const MenuHeader = props => {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onOpenSiteInfo = () => {
|
||||
dispatch(actions.toggleSiteInfoOpen());
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuSection>
|
||||
<Grid container justify="space-between" alignItems="center">
|
||||
<Typography
|
||||
gutterBottom
|
||||
display="inline"
|
||||
variant="button"
|
||||
component="h1"
|
||||
classes={{ root: classes.title }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBriefcase} width="0" /> TSPVIS
|
||||
</Typography>
|
||||
<Typography gutterBottom display="inline" color="textSecondary">
|
||||
<Tooltip title="Source code">
|
||||
<IconButton
|
||||
target="_blank"
|
||||
href="https://github.com/jhackshaw/tspvis"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} size="xs" width="0" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="General site information">
|
||||
<IconButton onClick={onOpenSiteInfo} edge="end">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" width="0" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Visualize algorithms for the traveling salesman problem. Use the
|
||||
controls below to plot points, choose an algorithm, and control
|
||||
execution.
|
||||
<br />
|
||||
(Hint: try a construction alogorithm followed by an improvement
|
||||
algorithm)
|
||||
</Typography>
|
||||
</MenuSection>
|
||||
);
|
||||
};
|
38
src/components/MenuItem.jsx
Normal file
38
src/components/MenuItem.jsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import { Grid, Typography } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
item: {
|
||||
margin: `${theme.spacing(1.5)}px 0`
|
||||
}
|
||||
}));
|
||||
|
||||
export const MenuItem = ({ children, title = "", row = false }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.item}>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction={row ? "row" : "column"}
|
||||
alignItems={row ? "center" : "flex-start"}
|
||||
>
|
||||
{title && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
gutterBottom
|
||||
color="textSecondary"
|
||||
variant="button"
|
||||
component="div"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{children}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
122
src/components/MenuMetrics.jsx
Normal file
122
src/components/MenuMetrics.jsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Grid, Typography } from "@material-ui/core";
|
||||
import * as selectors from "../store/selectors";
|
||||
|
||||
import { MenuSection } from "./MenuSection";
|
||||
import { MenuItem } from "./MenuItem";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
paddingRight: theme.spacing(1)
|
||||
},
|
||||
unit: {
|
||||
flexShrink: 0,
|
||||
width: "2rem"
|
||||
}
|
||||
}));
|
||||
|
||||
export const MenuMetrics = props => {
|
||||
const classes = useStyles();
|
||||
const best = useSelector(selectors.selectBestCostDisplay);
|
||||
const evaluating = useSelector(selectors.selectEvaluatingCostDisplay);
|
||||
const startedRunningAt = useSelector(selectors.selectStartedRunningAt);
|
||||
const [runningFor, setRunningFor] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (startedRunningAt) {
|
||||
const interval = setInterval(() => {
|
||||
setRunningFor(Math.floor((Date.now() - startedRunningAt) / 1000));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [startedRunningAt]);
|
||||
|
||||
return (
|
||||
<MenuSection>
|
||||
<MenuItem row>
|
||||
<Grid item container justify="space-between">
|
||||
<Typography
|
||||
display="inline"
|
||||
variant="button"
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
>
|
||||
Current Best:{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
classes={{ root: classes.grow }}
|
||||
align="right"
|
||||
display="inline"
|
||||
variant="button"
|
||||
>
|
||||
{best}
|
||||
</Typography>
|
||||
<Typography
|
||||
classes={{ root: classes.unit }}
|
||||
align="right"
|
||||
display="inline"
|
||||
variant="button"
|
||||
>
|
||||
km
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item container justify="space-between">
|
||||
<Typography
|
||||
display="inline"
|
||||
variant="button"
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
>
|
||||
Evaluating:{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
classes={{ root: classes.grow }}
|
||||
align="right"
|
||||
display="inline"
|
||||
variant="button"
|
||||
>
|
||||
{evaluating}
|
||||
</Typography>
|
||||
<Typography
|
||||
classes={{ root: classes.unit }}
|
||||
align="right"
|
||||
display="inline"
|
||||
variant="button"
|
||||
>
|
||||
km
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item container justify="space-between">
|
||||
<Typography
|
||||
display="inline"
|
||||
variant="button"
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
>
|
||||
Running For:{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
classes={{ root: classes.grow }}
|
||||
align="right"
|
||||
display="inline"
|
||||
variant="button"
|
||||
>
|
||||
{runningFor || ""}
|
||||
</Typography>
|
||||
<Typography
|
||||
classes={{ root: classes.unit }}
|
||||
align="right"
|
||||
display="inline"
|
||||
variant="button"
|
||||
>
|
||||
s
|
||||
</Typography>
|
||||
</Grid>
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
);
|
||||
};
|
141
src/components/MenuPointControls.jsx
Normal file
141
src/components/MenuPointControls.jsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Button,
|
||||
Slider,
|
||||
Grid,
|
||||
Typography,
|
||||
makeStyles
|
||||
} from "@material-ui/core";
|
||||
import {
|
||||
faRandom,
|
||||
faSave,
|
||||
faMousePointer,
|
||||
faMapMarked
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { MenuSection } from "./MenuSection";
|
||||
import { MenuItem } from "./MenuItem";
|
||||
import * as selectors from "../store/selectors";
|
||||
import * as actions from "../store/actions";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
grow: {
|
||||
flexGrow: 1
|
||||
}
|
||||
}));
|
||||
|
||||
// [0, 1/2, 1, 3, 12]
|
||||
let cache = ["1e+0", "1e+0"];
|
||||
const possRoutes = n => {
|
||||
if (n <= 2) {
|
||||
return "1e+0";
|
||||
}
|
||||
if (typeof cache[n - 1] !== "undefined") {
|
||||
return cache[n - 1];
|
||||
}
|
||||
|
||||
let result = 1;
|
||||
|
||||
for (let i = 1; i <= n; i++) {
|
||||
result *= i;
|
||||
cache[i] = (result / 2).toExponential(3);
|
||||
}
|
||||
|
||||
return cache[n - 1];
|
||||
};
|
||||
|
||||
export const MenuPointControls = ({ onRandomizePoints }) => {
|
||||
const classes = useStyles();
|
||||
const [possiblePaths, setPossiblePaths] = useState("0");
|
||||
const dispatch = useDispatch();
|
||||
const pointCount = useSelector(selectors.selectPointCount);
|
||||
const running = useSelector(selectors.selectRunning);
|
||||
const definingPoints = useSelector(selectors.selectDefiningPoints);
|
||||
|
||||
const onDefaultMap = () => {
|
||||
dispatch(actions.setDefaultMap());
|
||||
};
|
||||
|
||||
const onToggleDefiningPoints = () => {
|
||||
const action = definingPoints
|
||||
? actions.stopDefiningPoints()
|
||||
: actions.startDefiningPoints();
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const onPointCountChange = (_, newCount) => {
|
||||
dispatch(actions.setPointCount(newCount));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPossiblePaths(possRoutes(pointCount));
|
||||
}, [pointCount]);
|
||||
|
||||
const [num, exp] = possiblePaths.split("e+");
|
||||
|
||||
return (
|
||||
<MenuSection>
|
||||
<MenuItem title="Points">
|
||||
<ButtonGroup
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
disabled={running}
|
||||
>
|
||||
<Button
|
||||
onClick={onRandomizePoints}
|
||||
disabled={definingPoints || pointCount < 3}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRandom} width="0" />
|
||||
</Button>
|
||||
<Button onClick={onToggleDefiningPoints}>
|
||||
<FontAwesomeIcon
|
||||
icon={definingPoints ? faSave : faMousePointer}
|
||||
width="0"
|
||||
/>
|
||||
</Button>
|
||||
<Button disabled={definingPoints} onClick={onDefaultMap}>
|
||||
<FontAwesomeIcon icon={faMapMarked} width="0" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem title="Number of random points">
|
||||
<Slider
|
||||
value={pointCount}
|
||||
onChange={onPointCountChange}
|
||||
step={1}
|
||||
min={3}
|
||||
max={200}
|
||||
valueLabelDisplay="auto"
|
||||
color="secondary"
|
||||
disabled={running || definingPoints}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem row>
|
||||
<Grid item container justify="space-between">
|
||||
<Typography
|
||||
display="inline"
|
||||
variant="button"
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
>
|
||||
Possible Paths:{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
classes={{ root: classes.grow }}
|
||||
align="right"
|
||||
display="inline"
|
||||
component="span"
|
||||
>
|
||||
{num} x 10<sup>{exp}</sup>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
);
|
||||
};
|
26
src/components/MenuSection.jsx
Normal file
26
src/components/MenuSection.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import { Grid } from "@material-ui/core";
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
section: {
|
||||
padding: theme.spacing(2),
|
||||
// backgroundColor: ({ highlight = false }) =>
|
||||
// highlight ? theme.palette.grey[100] : theme.palette.paper,
|
||||
border: ({ highlight = false }) =>
|
||||
highlight ? `2px solid ${theme.palette.secondary.main}` : "none",
|
||||
borderRadius: "10px"
|
||||
}
|
||||
}));
|
||||
|
||||
export const MenuSection = ({ children, ...rest }) => {
|
||||
const classes = useStyles(rest);
|
||||
|
||||
return (
|
||||
<div className={classes.section}>
|
||||
<Grid container direction="column" wrap="nowrap">
|
||||
{children}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
236
src/components/MenuSolverControls.jsx
Normal file
236
src/components/MenuSolverControls.jsx
Normal file
|
@ -0,0 +1,236 @@
|
|||
import React from "react";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Button,
|
||||
Slider,
|
||||
Select,
|
||||
ListSubheader,
|
||||
MenuItem as SelectItem,
|
||||
Typography,
|
||||
Switch,
|
||||
Grid,
|
||||
IconButton
|
||||
} from "@material-ui/core";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faPlay,
|
||||
faStop,
|
||||
faRedo,
|
||||
faQuestion,
|
||||
faFastForward,
|
||||
faPause
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { MenuSection } from "./MenuSection";
|
||||
import { MenuItem } from "./MenuItem";
|
||||
import { useAlgorithmInfo } from "../hooks";
|
||||
import * as actions from "../store/actions";
|
||||
import * as selectors from "../store/selectors";
|
||||
|
||||
export const MenuSolverControls = ({
|
||||
onStart,
|
||||
onPause,
|
||||
onUnPause,
|
||||
onFullSpeed,
|
||||
onStop
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const algorithms = useAlgorithmInfo();
|
||||
const selectedAlgorithm = useSelector(selectors.selectAlgorithm);
|
||||
const delay = useSelector(selectors.selectDelay);
|
||||
const evaluatingDetailLevel = useSelector(
|
||||
selectors.selectEvaluatingDetailLevel
|
||||
);
|
||||
const maxEvaluatingDetailLevel = useSelector(
|
||||
selectors.selectMaxEvaluatingDetailLevel
|
||||
);
|
||||
const showBestPath = useSelector(selectors.selectShowBestPath);
|
||||
const running = useSelector(selectors.selectRunning);
|
||||
const fullSpeed = useSelector(selectors.selectFullSpeed);
|
||||
const paused = useSelector(selectors.selectPaused);
|
||||
const definingPoints = useSelector(selectors.selectDefiningPoints);
|
||||
|
||||
const onAlgorithmChange = event => {
|
||||
event.persist();
|
||||
onStop();
|
||||
const solverKey = event.target.value;
|
||||
const { defaults } = algorithms.find(alg => alg.solverKey === solverKey);
|
||||
dispatch(actions.setAlgorithm(solverKey, defaults));
|
||||
};
|
||||
|
||||
const onDelayChange = (_, newDelay) => {
|
||||
dispatch(actions.setDelay(newDelay));
|
||||
};
|
||||
|
||||
const onEvaluatingDetailLevelChange = (onLevel, offLevel) => event => {
|
||||
const level = event.target.checked ? onLevel : offLevel;
|
||||
dispatch(actions.setEvaluatingDetailLevel(level));
|
||||
};
|
||||
|
||||
const onShowBestPathChange = event => {
|
||||
dispatch(actions.setShowBestPath(event.target.checked));
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
onStop();
|
||||
dispatch(actions.resetSolverState());
|
||||
};
|
||||
|
||||
const onShowAlgInfo = () => {
|
||||
dispatch(actions.toggleAlgInfoOpen());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuSection highlight>
|
||||
<MenuItem title="Algorithm">
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={11}>
|
||||
<Select
|
||||
value={selectedAlgorithm}
|
||||
onChange={onAlgorithmChange}
|
||||
disabled={running || paused || definingPoints}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
>
|
||||
<ListSubheader>Heuristic Construction</ListSubheader>
|
||||
{algorithms
|
||||
.filter(alg => alg.type === "heuristic-construction")
|
||||
.map(alg => (
|
||||
<SelectItem value={alg.solverKey} key={alg.solverKey}>
|
||||
{alg.friendlyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
<ListSubheader>Heuristic Improvement</ListSubheader>
|
||||
{algorithms
|
||||
.filter(alg => alg.type === "heuristic-improvement")
|
||||
.map(alg => (
|
||||
<SelectItem value={alg.solverKey} key={alg.solverKey}>
|
||||
{alg.friendlyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
<ListSubheader>Exhaustive</ListSubheader>
|
||||
{algorithms
|
||||
.filter(alg => alg.type === "exhaustive")
|
||||
.map(alg => (
|
||||
<SelectItem value={alg.solverKey} key={alg.solverKey}>
|
||||
{alg.friendlyName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<Typography align="right" color="textSecondary">
|
||||
<IconButton edge="end" onClick={onShowAlgInfo}>
|
||||
<FontAwesomeIcon icon={faQuestion} size="xs" />
|
||||
</IconButton>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem title="Controls">
|
||||
<ButtonGroup
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
>
|
||||
<Button
|
||||
onClick={paused ? onUnPause : running ? onPause : onStart}
|
||||
disabled={definingPoints || fullSpeed}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={paused ? faPlay : running ? faPause : faPlay}
|
||||
width="0"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={paused ? onStop : onFullSpeed}
|
||||
disabled={(!running && !paused) || definingPoints || fullSpeed}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={paused ? faStop : faFastForward}
|
||||
width="0"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Button onClick={onReset} disabled={running || definingPoints}>
|
||||
<FontAwesomeIcon icon={faRedo} width="0" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</MenuItem>
|
||||
<MenuItem title="Delay">
|
||||
<Slider
|
||||
value={delay}
|
||||
onChange={onDelayChange}
|
||||
step={25}
|
||||
min={0}
|
||||
max={250}
|
||||
valueLabelDisplay="auto"
|
||||
color="secondary"
|
||||
disabled={definingPoints || fullSpeed}
|
||||
/>
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
<MenuSection>
|
||||
<MenuItem row>
|
||||
<Grid item xs={10}>
|
||||
<Typography variant="button" color="textSecondary" component="div">
|
||||
Show Best Path
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Switch
|
||||
checked={showBestPath}
|
||||
onChange={onShowBestPathChange}
|
||||
color="secondary"
|
||||
disabled={definingPoints || fullSpeed}
|
||||
id="show-best-path"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={10}>
|
||||
<Typography variant="button" color="textSecondary" component="div">
|
||||
Show Evaluated Paths
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Switch
|
||||
checked={evaluatingDetailLevel > 0}
|
||||
onChange={onEvaluatingDetailLevelChange(1, 0)}
|
||||
color="secondary"
|
||||
disabled={definingPoints || fullSpeed}
|
||||
id="show-evaluating-paths"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{maxEvaluatingDetailLevel > 1 && (
|
||||
<>
|
||||
<Grid item xs={10}>
|
||||
<Typography
|
||||
variant="button"
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
>
|
||||
Show Evaluated Steps
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Switch
|
||||
checked={evaluatingDetailLevel > 1}
|
||||
onChange={onEvaluatingDetailLevelChange(2, 1)}
|
||||
color="secondary"
|
||||
disabled={definingPoints || fullSpeed}
|
||||
id="show-evaluating-steps"
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
</>
|
||||
);
|
||||
};
|
22
src/components/OtherControls.jsx
Normal file
22
src/components/OtherControls.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from "react";
|
||||
import { Grid, Typography } from "@material-ui/core";
|
||||
import { MenuItem } from "./MenuItem";
|
||||
import { MenuSection } from "./MenuSection";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
export const OtherControls = props => {
|
||||
return (
|
||||
<MenuSection>
|
||||
<MenuItem row>
|
||||
<Grid item xs={10}>
|
||||
<Typography variant="button" color="textSecondary" component="div">
|
||||
Dark Mode
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<ThemeToggle />
|
||||
</Grid>
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
);
|
||||
};
|
27
src/components/SEO.jsx
Normal file
27
src/components/SEO.jsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React from "react";
|
||||
import Helmet from "react-helmet";
|
||||
|
||||
const description =
|
||||
"Interactive solver for the traveling salesman problem to visualize different algorithms. Includes various Heuristic and Exhaustive algorithms.";
|
||||
|
||||
export const SEO = ({ subtitle }) => {
|
||||
return (
|
||||
<Helmet
|
||||
title={`${subtitle}${
|
||||
subtitle ? " | " : ""
|
||||
}Traveling Salesman Problem Visualizer`}
|
||||
htmlAttributes={{ lang: "en" }}
|
||||
>
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`Traveling Salesman Problem Visualizer`}
|
||||
/>
|
||||
<meta property="og:url" content="https://tspvis.com" />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content="https://i.imgur.com/u4ibcsC.jpg" />
|
||||
<meta property="og:type" content="website" />
|
||||
</Helmet>
|
||||
);
|
||||
};
|
15
src/components/ThemeToggle.js
Normal file
15
src/components/ThemeToggle.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from "react";
|
||||
import { Switch } from "@material-ui/core";
|
||||
import { useThemeContext } from "../context";
|
||||
|
||||
export const ThemeToggle = () => {
|
||||
const { colorMode, toggleColorMode } = useThemeContext();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={colorMode === "dark"}
|
||||
onChange={toggleColorMode}
|
||||
color="secondary"
|
||||
/>
|
||||
);
|
||||
};
|
15
src/components/index.js
Normal file
15
src/components/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
export * from "./AlgorithmModals";
|
||||
export * from "./InformationModal";
|
||||
export * from "./IntroductionModal";
|
||||
export * from "./Layout";
|
||||
export * from "./MapPlot";
|
||||
export * from "./Menu";
|
||||
export * from "./MenuHeader";
|
||||
export * from "./MenuItem";
|
||||
export * from "./MenuMetrics";
|
||||
export * from "./MenuPointControls";
|
||||
export * from "./MenuSection";
|
||||
export * from "./MenuSolverControls";
|
||||
export * from "../context/PreSetTheme";
|
||||
export * from "./SEO";
|
||||
export * from "./ThemeToggle";
|
19
src/constants.js
Normal file
19
src/constants.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
// orangish
|
||||
export const START_POINT_COLOR = [255, 87, 34];
|
||||
|
||||
// blueish
|
||||
export const POINT_COLOR = [41, 121, 255];
|
||||
|
||||
// greenish
|
||||
export const BEST_PATH_COLOR = [76, 175, 80];
|
||||
|
||||
// orangish
|
||||
export const EVALUATING_PATH_COLOR = [255, 87, 34, 225];
|
||||
|
||||
// reddish
|
||||
export const EVALUATING_ERROR_COLOR = [255, 25, 25, 240];
|
||||
|
||||
// greyish
|
||||
export const EVALUATING_SEGMENT_COLOR = [180, 180, 180, 240];
|
||||
|
||||
export const COLOR_MODE_KEY = "color-mode";
|
99
src/content/exhaustive/branchAndBoundOnCost.md
Normal file
99
src/content/exhaustive/branchAndBoundOnCost.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
type: exhaustive
|
||||
order: 3
|
||||
solverKey: branchAndBoundOnCost
|
||||
friendlyName: Branch and Bound (Cost)
|
||||
defaults:
|
||||
evaluatingDetailLevel: 2
|
||||
maxEvaluatingDetailLevel: 2
|
||||
---
|
||||
|
||||
# Branch and Bound on Cost
|
||||
|
||||
This is a recursive algorithm, similar to depth first search, that is guaranteed to find the optimal solution.
|
||||
|
||||
The candidate solution space is generated by systematically traversing possible paths, and discarding large subsets of fruitless candidates by comparing the current solution to an upper and lower bound. In this case, the upper bound is the best path found so far.
|
||||
|
||||
While evaluating paths, if at any point the current solution is already more expensive (longer) than the best complete path discovered, there is no point continuing.
|
||||
|
||||
For example, imagine:
|
||||
|
||||
1. A -> B -> C -> D -> E -> A was already found with a cost of 100.
|
||||
2. We are evaluating A -> C -> E, which has a cost of 110. There is **no point** evaluating the remaining solutions.
|
||||
3. Instead of continuing to evaluate all of the child solutions from here, we can go down a different path, eliminating candidates not worth evaluating:
|
||||
- `A -> C -> E -> D -> B -> A`
|
||||
- `A -> C -> E -> B -> D -> A`
|
||||
|
||||
Implementation is very similar to depth first search, with the exception that we cut paths that are already longer than the current best.
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const branchAndBoundOnCost = async (
|
||||
points,
|
||||
path = [],
|
||||
visited = null,
|
||||
overallBest = Infinity
|
||||
) => {
|
||||
if (visited === null) {
|
||||
// initial call
|
||||
path = [points.shift()];
|
||||
points = new Set(points);
|
||||
visited = new Set();
|
||||
}
|
||||
|
||||
// figure out which points are left
|
||||
const available = setDifference(points, visited);
|
||||
|
||||
// calculate the cost, from here, to go home
|
||||
const backToStart = [...path, path[0]];
|
||||
const cost = pathCost(backToStart);
|
||||
|
||||
if (cost > overallBest) {
|
||||
// we may not be done, but have already traveled further than the best path
|
||||
// no reason to continue
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// still cheaper than the best, keep going deeper, and deeper, and deeper...
|
||||
|
||||
if (available.size === 0) {
|
||||
// at the end of the path, return where we're at
|
||||
return [cost, backToStart];
|
||||
}
|
||||
|
||||
let [bestCost, bestPath] = [null, null];
|
||||
|
||||
// for every point yet to be visited along this path
|
||||
for (const p of available) {
|
||||
// go to that point
|
||||
visited.add(p);
|
||||
path.push(p);
|
||||
|
||||
// RECURSE - go through all the possible points from that point
|
||||
const [curCost, curPath] = await branchAndBoundOnCost(
|
||||
points,
|
||||
path,
|
||||
visited,
|
||||
overallBest
|
||||
);
|
||||
|
||||
// if that path is better and complete, keep it
|
||||
if (curCost && (!bestCost || curCost < bestCost)) {
|
||||
[bestCost, bestPath] = [curCost, curPath];
|
||||
|
||||
if (!overallBest || bestCost < overallBest) {
|
||||
// found a new best complete path
|
||||
overallBest = bestCost;
|
||||
self.setBestPath(bestPath, bestCost);
|
||||
}
|
||||
}
|
||||
|
||||
// go back up and make that point available again
|
||||
visited.delete(p);
|
||||
path.pop();
|
||||
}
|
||||
|
||||
return [bestCost, bestPath];
|
||||
};
|
||||
```
|
56
src/content/exhaustive/branchAndBoundOnCostAndCross.md
Normal file
56
src/content/exhaustive/branchAndBoundOnCostAndCross.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
type: exhaustive
|
||||
order: 4
|
||||
solverKey: branchAndBoundOnCostAndCross
|
||||
friendlyName: Branch and Bound (Cost, Crossings)
|
||||
defaults:
|
||||
evaluatingDetailLevel: 2
|
||||
maxEvaluatingDetailLevel: 2
|
||||
---
|
||||
|
||||
# Branch and Bound (Cost, Intersections)
|
||||
|
||||
This is the same as branch and bound on cost, with an additional heuristic added to further minimize the search space.
|
||||
|
||||
While traversing paths, if at any point the path intersects (crosses over) itself, than backtrack and try the next way. It's been proven that an optimal path will never contain crossings.
|
||||
|
||||
Implementation is almost identical to branch and bound on cost only, with the added heuristic below:
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
|
||||
const counterClockWise = (p, q, r) => {
|
||||
return (q[0] - p[0]) * (r[1] - q[1]) <
|
||||
(q[1] - p[1]) * (r[0] - q[0])
|
||||
}
|
||||
|
||||
const intersects = (a, b, c, d) => {
|
||||
return counterClockWise(a, c, d) !== counterClockWise(b, c, d) &&
|
||||
counterClockWise(a, b, c) !== counterClockWise(a, b, d)
|
||||
}
|
||||
|
||||
const branchAndBoundOnCostAndCross = async (...) => {
|
||||
//
|
||||
// .....
|
||||
//
|
||||
|
||||
if (path.length > 3) {
|
||||
// if this newly added edge crosses over the existing path,
|
||||
// don't continue. It's been proven that an optimal path will
|
||||
// not cross itself.
|
||||
const newSegment = [
|
||||
path[path.length-2], path[path.length-1]
|
||||
]
|
||||
for (let i=1; i<path.length-2; i++) {
|
||||
if (intersects(path[i], path[i-1], ...newSegment)) {
|
||||
return [null, null]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// .....
|
||||
//
|
||||
}
|
||||
```
|
87
src/content/exhaustive/depthFirstSearch.md
Normal file
87
src/content/exhaustive/depthFirstSearch.md
Normal file
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
type: exhaustive
|
||||
order: 1
|
||||
solverKey: depthFirstSearch
|
||||
friendlyName: Depth First Search (Brute Force)
|
||||
defaults:
|
||||
evaluatingDetailLevel: 2
|
||||
maxEvaluatingDetailLevel: 2
|
||||
---
|
||||
|
||||
# Depth First Search (Brute Force)
|
||||
|
||||
This is an exhaustive, brute-force algorithm. It is guaranteed to find the best possible path, however depending on the number of points in the traveling salesman problem it is likely impractical. For example,
|
||||
|
||||
- With 10 points there are 181,400 paths to evaluate.
|
||||
- With 11 points, there are 1,814,000.
|
||||
- With 12 points there are 19,960,000.
|
||||
- With 20 points there are 60,820,000,000,000,000, give or take.
|
||||
- With 25 points there are 310,200,000,000,000,000,000,000, give or take.
|
||||
|
||||
This is factorial growth, and it quickly makes the TSP impractical to brute force. That is why heuristics exist to give a good approximation of the best path, but it is very difficult to determine without a doubt what the best path is for a reasonably sized traveling salesman problem.
|
||||
|
||||
This is a recursive, depth-first-search algorithm, as follows:
|
||||
|
||||
1. From the starting point
|
||||
2. For all other points not visited
|
||||
3. If there are no points left return the current cost/path
|
||||
4. Else, go to every remaining point and
|
||||
|
||||
:
|
||||
|
||||
1. Mark that point as visited
|
||||
2. "**recurse**" through those paths (go back to 1. )
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const dfs = async (points, path = [], visited = null, overallBest = null) => {
|
||||
if (visited === null) {
|
||||
// initial call
|
||||
path = [points.shift()];
|
||||
points = new Set(points);
|
||||
visited = new Set();
|
||||
}
|
||||
|
||||
// figure out what points are left from this point
|
||||
const available = setDifference(points, visited);
|
||||
|
||||
if (available.size === 0) {
|
||||
// this must be a complete path
|
||||
const backToStart = [...path, path[0]];
|
||||
|
||||
// calculate the cost of this path
|
||||
const cost = pathCost(backToStart);
|
||||
|
||||
// return both the cost and the path where we're at
|
||||
return [cost, backToStart];
|
||||
}
|
||||
|
||||
let [bestCost, bestPath] = [null, null];
|
||||
|
||||
// for every point yet to be visited along this path
|
||||
for (const p of available) {
|
||||
// go to that point
|
||||
visited.add(p);
|
||||
path.push(p);
|
||||
|
||||
// RECURSE - go through all the possible points from that point
|
||||
const [curCost, curPath] = await dfs(points, path, visited, overallBest);
|
||||
|
||||
// if that path is better, keep it
|
||||
if (bestCost === null || curCost < bestCost) {
|
||||
[bestCost, bestPath] = [curCost, curPath];
|
||||
|
||||
if (overallBest === null || bestCost < overallBest) {
|
||||
// found a new best complete path
|
||||
overallBest = bestCost;
|
||||
}
|
||||
}
|
||||
|
||||
// go back up and make that point available again
|
||||
visited.delete(p);
|
||||
path.pop();
|
||||
}
|
||||
return [bestCost, bestPath];
|
||||
};
|
||||
```
|
53
src/content/exhaustive/random.md
Normal file
53
src/content/exhaustive/random.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
type: exhaustive
|
||||
order: 2
|
||||
solverKey: random
|
||||
friendlyName: Random
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Random
|
||||
|
||||
This is an impractical, albeit exhaustive algorithm. It is here only for demonstration purposes, but will not find a reasonable path for traveling salesman problems above 7 or 8 points.
|
||||
|
||||
I consider it exhaustive because if it runs for infinity, eventually it will encounter every possible path.
|
||||
|
||||
1. From the starting path
|
||||
2. Randomly shuffle the path
|
||||
3. If it's better, keep it
|
||||
4. If not, ditch it and keep going
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const random = async points => {
|
||||
let best = Infinity;
|
||||
|
||||
while (true) {
|
||||
// save off the starting point
|
||||
const start = points.shift();
|
||||
|
||||
// sort the remaining points
|
||||
const path = points.sort(() => Math.random() - 0.5);
|
||||
|
||||
// put the starting point back
|
||||
path.unshift(start);
|
||||
|
||||
// return to the starting point
|
||||
path.push(start);
|
||||
|
||||
// calculate the new cost
|
||||
const cost = pathCost(path);
|
||||
|
||||
if (cost < best) {
|
||||
// we found a better path
|
||||
best = cost;
|
||||
}
|
||||
|
||||
// get rid of starting point at the end
|
||||
path.pop();
|
||||
}
|
||||
};
|
||||
```
|
61
src/content/heurisitc-construction/arbitraryInsertion.md
Normal file
61
src/content/heurisitc-construction/arbitraryInsertion.md
Normal file
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
type: heuristic-construction
|
||||
order: 2
|
||||
solverKey: arbitraryInsertion
|
||||
friendlyName: Arbitrary Insertion
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Arbitrary Insertion
|
||||
|
||||
This is a heuristic construction algorithm. It select a random point, and then figures out where the best place to put it will be.
|
||||
|
||||
1. From the starting point
|
||||
2. First, go to the closest point
|
||||
3. Choose a random point to go to
|
||||
4. Find the cheapest place to add it in the path
|
||||
5. Chosen point is no longer an "available point"
|
||||
6. Continue from #3 until there are no available points, and then return to the start.
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const arbitraryInsertion = async points => {
|
||||
// from the starting point
|
||||
const path = [points.shift()];
|
||||
|
||||
//
|
||||
// INITIALIZATION - go to the nearest point
|
||||
//
|
||||
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||
path.push(points.pop());
|
||||
|
||||
// randomly sort points - this is the order they will be added
|
||||
// to the path
|
||||
points.sort(() => Math.random() - 0.5);
|
||||
|
||||
while (points.length > 0) {
|
||||
//
|
||||
// SELECTION - choose a next point randomly
|
||||
//
|
||||
const nextPoint = points.pop();
|
||||
|
||||
//
|
||||
// INSERTION -find the insertion spot that minimizes distance
|
||||
//
|
||||
let [bestCost, bestIdx] = [Infinity, null];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||
if (insertionCost < bestCost) {
|
||||
[bestCost, bestIdx] = [insertionCost, i];
|
||||
}
|
||||
}
|
||||
path.splice(bestIdx, 0, nextPoint);
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
};
|
||||
```
|
111
src/content/heurisitc-construction/convexHull.md
Normal file
111
src/content/heurisitc-construction/convexHull.md
Normal file
|
@ -0,0 +1,111 @@
|
|||
---
|
||||
type: heuristic-construction
|
||||
order: 5
|
||||
solverKey: convexHull
|
||||
friendlyName: Convex Hull
|
||||
defaults:
|
||||
evaluatingDetailLevel: 2
|
||||
maxEvaluatingDetailLevel: 2
|
||||
---
|
||||
|
||||
# Convex Hull
|
||||
|
||||
This is a heuristic construction algorithm. It starts by building the [convex hull](https://en.wikipedia.org/wiki/Convex_hull), and adding interior points from there. This implmentation uses another heuristic for insertion based on the ratio of the cost of adding the new point to the overall length of the segment, however any insertion algorithm could be applied after building the hull.
|
||||
|
||||
There are a number of algorithms to determine the convex hull. This implementation uses the [gift wrapping algorithm](https://en.wikipedia.org/wiki/Gift_wrapping_algorithm).
|
||||
|
||||
In essence, the steps are:
|
||||
|
||||
1. Determine the leftmost point
|
||||
2. Continually add the most counterclockwise point until the convex hull is formed
|
||||
3. For each remaining point p, find the segment i => j in the hull that minimizes cost(i -> p) + cost(p -> j) - cost(i -> j)
|
||||
4. Of those, choose p that minimizes cost(i -> p -> j) / cost(i -> j)
|
||||
5. Add p to the path between i and j
|
||||
6. Repeat from #3 until there are no remaining points
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const convexHull = async points => {
|
||||
const sp = points[0];
|
||||
|
||||
// Find the "left most point"
|
||||
let leftmost = points[0];
|
||||
for (const p of points) {
|
||||
if (p[1] < leftmost[1]) {
|
||||
leftmost = p;
|
||||
}
|
||||
}
|
||||
|
||||
const path = [leftmost];
|
||||
|
||||
while (true) {
|
||||
const curPoint = path[path.length - 1];
|
||||
let [selectedIdx, selectedPoint] = [0, null];
|
||||
|
||||
// find the "most counterclockwise" point
|
||||
for (let [idx, p] of points.entries()) {
|
||||
if (!selectedPoint || orientation(curPoint, p, selectedPoint) === 2) {
|
||||
// this point is counterclockwise with respect to the current hull
|
||||
// and selected point (e.g. more counterclockwise)
|
||||
[selectedIdx, selectedPoint] = [idx, p];
|
||||
}
|
||||
}
|
||||
|
||||
// adding this to the hull so it's no longer available
|
||||
points.splice(selectedIdx, 1);
|
||||
|
||||
// back to the furthest left point, formed a cycle, break
|
||||
if (selectedPoint === leftmost) {
|
||||
break;
|
||||
}
|
||||
|
||||
// add to hull
|
||||
path.push(selectedPoint);
|
||||
}
|
||||
|
||||
while (points.length > 0) {
|
||||
let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
|
||||
|
||||
for (let [freeIdx, freePoint] of points.entries()) {
|
||||
// for every free point, find the point in the current path
|
||||
// that minimizes the cost of adding the point minus the cost of
|
||||
// the original segment
|
||||
let [bestCost, bestIdx] = [Infinity, 0];
|
||||
for (let [pathIdx, pathPoint] of path.entries()) {
|
||||
const nextPathPoint = path[(pathIdx + 1) % path.length];
|
||||
|
||||
// the new cost minus the old cost
|
||||
const evalCost =
|
||||
pathCost([pathPoint, freePoint, nextPathPoint]) -
|
||||
pathCost([pathPoint, nextPathPoint]);
|
||||
|
||||
if (evalCost < bestCost) {
|
||||
[bestCost, bestIdx] = [evalCost, pathIdx];
|
||||
}
|
||||
}
|
||||
|
||||
// figure out how "much" more expensive this is with respect to the
|
||||
// overall length of the segment
|
||||
const nextPoint = path[(bestIdx + 1) % path.length];
|
||||
const prevCost = pathCost([path[bestIdx], nextPoint]);
|
||||
const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
|
||||
const ratio = newCost / prevCost;
|
||||
|
||||
if (ratio < bestRatio) {
|
||||
[bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
|
||||
}
|
||||
}
|
||||
|
||||
const [nextPoint] = points.splice(bestPointIdx, 1);
|
||||
path.splice(insertIdx, 0, nextPoint);
|
||||
}
|
||||
|
||||
// rotate the array so that starting point is back first
|
||||
const startIdx = path.findIndex(p => p === sp);
|
||||
path.unshift(...path.splice(startIdx, path.length));
|
||||
|
||||
// go back home
|
||||
path.push(sp);
|
||||
};
|
||||
```
|
73
src/content/heurisitc-construction/furthestInsertion.md
Normal file
73
src/content/heurisitc-construction/furthestInsertion.md
Normal file
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
type: heuristic-construction
|
||||
order: 4
|
||||
solverKey: furthestInsertion
|
||||
friendlyName: Furthest Insertion
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Furthest Insertion
|
||||
|
||||
This is a heuristic construction algorithm. It selects the furthest point from the path, and then figures out where the best place to put it will be.
|
||||
|
||||
1. From the starting point
|
||||
2. First, go to the closest point
|
||||
3. Choose the point that is furthest from any of the points on the path
|
||||
4. Find the cheapest place to add it in the path
|
||||
5. Chosen point is no longer an "available point"
|
||||
6. Continue from #3 until there are no available points, and then return to the start.
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const furthestInsertion = async points => {
|
||||
// from the starting point
|
||||
const path = [points.shift()];
|
||||
|
||||
//
|
||||
// INITIALIZATION - go to the nearest point first
|
||||
//
|
||||
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||
path.push(points.pop());
|
||||
|
||||
while (points.length > 0) {
|
||||
//
|
||||
// SELECTION - furthest point from the path
|
||||
//
|
||||
let [selectedDistance, selectedIdx] = [0, null];
|
||||
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||
// find the minimum distance to the path for freePoint
|
||||
let [bestCostToPath, costToPathIdx] = [Infinity, null];
|
||||
for (const pathPoint of path) {
|
||||
const dist = distance(freePoint, pathPoint);
|
||||
if (dist < bestCostToPath) {
|
||||
[bestCostToPath, costToPathIdx] = [dist, freePointIdx];
|
||||
}
|
||||
}
|
||||
|
||||
// if this point is further from the path than the currently selected
|
||||
if (bestCostToPath > selectedDistance) {
|
||||
[selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
|
||||
}
|
||||
}
|
||||
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||
|
||||
//
|
||||
// INSERTION - find the insertion spot that minimizes distance
|
||||
//
|
||||
let [bestCost, bestIdx] = [Infinity, null];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||
if (insertionCost < bestCost) {
|
||||
[bestCost, bestIdx] = [insertionCost, i];
|
||||
}
|
||||
}
|
||||
path.splice(bestIdx, 0, nextPoint);
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
};
|
||||
```
|
68
src/content/heurisitc-construction/nearestInsertion.md
Normal file
68
src/content/heurisitc-construction/nearestInsertion.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
type: heuristic-construction
|
||||
order: 3
|
||||
solverKey: nearestInsertion
|
||||
friendlyName: Nearest Insertion
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Furthest Insertion
|
||||
|
||||
This is a heuristic construction algorithm. It selects the closest point to the path, and then figures out where the best place to put it will be.
|
||||
|
||||
1. From the starting point
|
||||
2. First, go to the closest point
|
||||
3. Choose the point that is **nearest** to the current path
|
||||
4. Find the cheapest place to add it in the path
|
||||
5. Chosen point is no longer an "available point"
|
||||
6. Continue from #3 until there are no available points, and then return to the start.
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const nearestInsertion = async points => {
|
||||
// from the starting point
|
||||
const path = [points.shift()];
|
||||
|
||||
//
|
||||
// INITIALIZATION - go to the nearest point first
|
||||
//
|
||||
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||
path.push(points.pop());
|
||||
|
||||
while (points.length > 0) {
|
||||
//
|
||||
// SELECTION - nearest point to the path
|
||||
//
|
||||
let [selectedDistance, selectedIdx] = [Infinity, null];
|
||||
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||
for (const pathPoint of path) {
|
||||
const dist = distance(freePoint, pathPoint);
|
||||
if (dist < selectedDistance) {
|
||||
[selectedDistance, selectedIdx] = [dist, freePointIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get the next point to add
|
||||
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||
|
||||
//
|
||||
// INSERTION - find the insertion spot that minimizes distance
|
||||
//
|
||||
let [bestCost, bestIdx] = [Infinity, null];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||
if (insertionCost < bestCost) {
|
||||
[bestCost, bestIdx] = [insertionCost, i];
|
||||
}
|
||||
}
|
||||
path.splice(bestIdx, 0, nextPoint);
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
};
|
||||
```
|
43
src/content/heurisitc-construction/nearestNeighbor.md
Normal file
43
src/content/heurisitc-construction/nearestNeighbor.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
type: heuristic-construction
|
||||
order: 1
|
||||
solverKey: nearestNeighbor
|
||||
friendlyName: Nearest Neighbor
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Nearest Neighbor
|
||||
|
||||
This is a heuristic, greedy algorithm also known as nearest neighbor. It continually chooses the best looking option from the current state.
|
||||
|
||||
1. From the starting point
|
||||
2. sort the remaining available points based on cost (distance)
|
||||
3. Choose the closest point and go there
|
||||
4. Chosen point is no longer an "available point"
|
||||
5. Continue this way until there are no available points, and then return to the start.
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const nearestNeighbor = async points => {
|
||||
const path = [points.shift()];
|
||||
|
||||
while (points.length > 0) {
|
||||
// sort remaining points in place by their
|
||||
// distance from the last point in the current path
|
||||
points.sort(
|
||||
(a, b) =>
|
||||
distance(path[path.length - 1], b) - distance(path[path.length - 1], a)
|
||||
);
|
||||
|
||||
// go to the closest remaining point
|
||||
path.push(points.pop());
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
const cost = pathCost(path);
|
||||
};
|
||||
```
|
105
src/content/heurisitc-construction/simulatedAnnealing.md
Normal file
105
src/content/heurisitc-construction/simulatedAnnealing.md
Normal file
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
type: heuristic-construction
|
||||
order: 6
|
||||
solverKey: simulatedAnnealing
|
||||
friendlyName: Simulated Annealing
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Simulated Annealing
|
||||
|
||||
Simulated annealing (SA) is a probabilistic technique for approximating the global optimum of a given function. Specifically, it is a metaheuristic to approximate global optimization in a large search space for an optimization problem.
|
||||
|
||||
For problems where finding an approximate global optimum is more important than finding a precise local optimum in a fixed amount of time, simulated annealing may be preferable to exact algorithms
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const simulatedAnnealing = async points => {
|
||||
const sp = points[0];
|
||||
const path = points;
|
||||
|
||||
const tempCoeff =
|
||||
path.length < 10
|
||||
? 1 - 1e-4
|
||||
: path.length < 15
|
||||
? 1 - 1e-5
|
||||
: path.length < 25
|
||||
? 1 - 1e-6
|
||||
: 1 - 5e-7;
|
||||
|
||||
const deltaDistance = (aIdx, bIdx) => {
|
||||
const aPrev = (aIdx - 1 + path.length) % path.length;
|
||||
const aNext = (aIdx + 1 + path.length) % path.length;
|
||||
const bPrev = (bIdx - 1 + path.length) % path.length;
|
||||
const bNext = (bIdx + 1 + path.length) % path.length;
|
||||
let diff =
|
||||
distance(path[bPrev], path[aIdx]) +
|
||||
distance(path[aIdx], path[bNext]) +
|
||||
distance(path[aPrev], path[bIdx]) +
|
||||
distance(path[bIdx], path[aNext]) -
|
||||
distance(path[aPrev], path[aIdx]) -
|
||||
distance(path[aIdx], path[aNext]) -
|
||||
distance(path[bPrev], path[bIdx]) -
|
||||
distance(path[bIdx], path[bNext]);
|
||||
|
||||
if (bPrev === aIdx || bNext === aIdx) {
|
||||
diff += 2 * distance(path[aIdx], path[bIdx]);
|
||||
}
|
||||
return diff;
|
||||
};
|
||||
|
||||
const changePath = temperature => {
|
||||
// 2 random points
|
||||
const a = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||
const b = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||
|
||||
const delta = deltaDistance(a, b);
|
||||
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
|
||||
// swap points
|
||||
[path[a], path[b]] = [path[b], path[a]];
|
||||
}
|
||||
};
|
||||
|
||||
const initialTemp = 100 * distance(path[0], path[1]);
|
||||
let i = 0;
|
||||
|
||||
for (
|
||||
let temperature = initialTemp;
|
||||
temperature > 1e-6;
|
||||
temperature *= tempCoeff
|
||||
) {
|
||||
changePath(temperature);
|
||||
if (i % 10000 == 0) {
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path, color: EVALUATING_PATH_COLOR }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
await self.sleep();
|
||||
}
|
||||
if (i % 100000 == 0) {
|
||||
path.push(sp);
|
||||
self.setBestPath(path, pathCost(path));
|
||||
path.pop();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// rotate the array so that starting point is back first
|
||||
rotateToStartingPoint(path, sp);
|
||||
|
||||
// go back home
|
||||
path.push(sp);
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
self.setBestPath(path, cost);
|
||||
};
|
||||
|
||||
makeSolver(simulatedAnnealing);
|
||||
```
|
60
src/content/heuristic-improvement/twoOptInversion.md
Normal file
60
src/content/heuristic-improvement/twoOptInversion.md
Normal file
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
type: heuristic-improvement
|
||||
order: 1
|
||||
solverKey: twoOptInversion
|
||||
friendlyName: Two Opt Inversion
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Two-Opt inversion
|
||||
|
||||
This algorithm is also known as 2-opt, 2-opt mutation, and cross-aversion. The general goal is to find places where the path crosses over itself, and then "undo" that crossing. It repeats until there are no crossings. A characteristic of this algorithm is that afterwards the path is guaranteed to have no crossings.
|
||||
|
||||
1. While a better path has not been found.
|
||||
2. For each pair of points:
|
||||
3. Reverse the path between the selected points.
|
||||
4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
|
||||
5. If not, revert the path and continue searching.
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const twoOptInversion = async path => {
|
||||
path.push(path[0]);
|
||||
let best = pathCost(path);
|
||||
let swapped = true;
|
||||
|
||||
while (swapped) {
|
||||
swapped = false;
|
||||
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||
// section of the path to reverse
|
||||
const section = path.slice(pt1, pt2 + 1);
|
||||
|
||||
// reverse section in place
|
||||
section.reverse();
|
||||
|
||||
// replace section of path with reversed section in place
|
||||
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||
|
||||
// calculate new cost
|
||||
const newPath = path;
|
||||
const cost = pathCost(newPath);
|
||||
|
||||
if (cost < best) {
|
||||
// found a better path after the swap, keep it
|
||||
swapped = true;
|
||||
best = cost;
|
||||
self.setBestPath(newPath, best);
|
||||
} else {
|
||||
// un-reverse the section
|
||||
section.reverse();
|
||||
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
type: heuristic-improvement
|
||||
order: 2
|
||||
solverKey: twoOptReciprocalExchange
|
||||
friendlyName: Two Opt Reciprocal Exchange
|
||||
defaults:
|
||||
evaluatingDetailLevel: 1
|
||||
maxEvaluatingDetailLevel: 1
|
||||
---
|
||||
|
||||
# Two-Opt Reciprocal Exchange
|
||||
|
||||
This algorithm is similar to the 2-opt mutation or inversion algorithm, although generally will find a less optimal path. However, the computational cost of calculating new solutions is less intensive.
|
||||
|
||||
The big difference with 2-opt mutation is not reversing the path between the 2 points. This algorithm is **not** always going to find a path that doesn't cross itself.
|
||||
|
||||
It could be worthwhile to try this algorithm prior to 2-opt inversion because of the cheaper cost of calculation, but probably not.
|
||||
|
||||
1. While a better path has not been found.
|
||||
2. For each pair of points:
|
||||
3. Swap the points in the path. That is, go to point B before point A, continue along the same path, and go to point A where point B was.
|
||||
4. If the new path is cheaper (shorter), keep it and continue searching. Remember that we found a better path.
|
||||
5. If not, revert the path and continue searching.
|
||||
|
||||
## Implementation
|
||||
|
||||
```javascript
|
||||
const twoOptReciprocalExchange = async path => {
|
||||
path.push(path[0]);
|
||||
let best = pathCost(path);
|
||||
let swapped = true;
|
||||
|
||||
self.setBestPath(path, best);
|
||||
|
||||
while (swapped) {
|
||||
swapped = false;
|
||||
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||
// swap current pair of points
|
||||
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||
|
||||
// calculate new cost
|
||||
const cost = pathCost(path);
|
||||
|
||||
if (cost < best) {
|
||||
// found a better path after the swap, keep it
|
||||
swapped = true;
|
||||
best = cost;
|
||||
} else {
|
||||
// swap back - this one's worse
|
||||
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
54
src/content/introduction.md
Normal file
54
src/content/introduction.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
type: introduction
|
||||
---
|
||||
|
||||
# Traveling Salesman Problem
|
||||
|
||||
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?".
|
||||
|
||||
### This project
|
||||
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
### Heuristic algorithms
|
||||
|
||||
Heuristic algorithms attempt to find a good approximation of the optimal path within a more _reasonable_ amount of time.
|
||||
|
||||
**Construction** - Build a path (e.g. shortest path)
|
||||
|
||||
- Shortest Path
|
||||
- Arbitrary Insertion
|
||||
- Furthest Insertion
|
||||
- Nearest Insertion
|
||||
- Convex Hull Insertion\*
|
||||
- Simulated Annealing\*
|
||||
|
||||
**Improvement** - Attempt to take an existing constructed path and improve on it
|
||||
|
||||
- 2-Opt Inversion
|
||||
- 2-Opt Reciprcal Exchange\*
|
||||
|
||||
### Exhaustive algorithms
|
||||
|
||||
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:
|
||||
|
||||
- Random Paths
|
||||
- Depth First Search (Brute Force)
|
||||
- Branch and Bound (Cost)
|
||||
- Branch and Bound (Cost, Intersections)\*
|
||||
|
||||
## Dependencies
|
||||
|
||||
These are the main tools used to build this site:
|
||||
|
||||
- [gatsbyjs](https://www.gatsbyjs.org)
|
||||
- [reactjs](https://reactjs.org)
|
||||
- [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
|
||||
- [material-ui](https://material-ui.com/)
|
||||
- [deck.gl](https://deck.gl/#/)
|
||||
- [mapbox](https://www.mapbox.com/)
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are always welcome! Also, feel free to raise any ideas, suggestions, or bugs as an issue.
|
23
src/context/PreSetTheme.jsx
Normal file
23
src/context/PreSetTheme.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
|
||||
export const PreSetTheme = () => (
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
function getInitialMode() {
|
||||
const stored = window.localStorage.getItem('color-mode');
|
||||
if (stored && (stored === "dark" || stored === "light")) {
|
||||
return stored;
|
||||
}
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
return mql.matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
const mode = getInitialMode();
|
||||
localStorage.setItem('color-mode', mode);
|
||||
})();
|
||||
`
|
||||
}}
|
||||
/>
|
||||
);
|
51
src/context/ThemeContext.jsx
Normal file
51
src/context/ThemeContext.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from "react";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { ThemeProvider as MUIThemeProvider } from "@material-ui/styles";
|
||||
import { CssBaseline, createTheme } from "@material-ui/core";
|
||||
import blue from "@material-ui/core/colors/blue";
|
||||
import orange from "@material-ui/core/colors/orange";
|
||||
|
||||
import { COLOR_MODE_KEY } from "../constants";
|
||||
import { usePersistentState } from "../hooks";
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
|
||||
export const ThemeContextProvider = props => {
|
||||
const { children } = props;
|
||||
|
||||
const [colorMode, setColorMode] = usePersistentState(COLOR_MODE_KEY, "dark");
|
||||
|
||||
const muiTheme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
palette: {
|
||||
primary: blue,
|
||||
secondary: orange,
|
||||
type: colorMode
|
||||
}
|
||||
}),
|
||||
[colorMode]
|
||||
);
|
||||
|
||||
const toggleColorMode = useCallback(() => {
|
||||
setColorMode(current => (current === "dark" ? "light" : "dark"));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MUIThemeProvider theme={muiTheme}>
|
||||
<CssBaseline />
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
colorMode,
|
||||
setColorMode,
|
||||
toggleColorMode,
|
||||
muiTheme
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
</MUIThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useThemeContext = () => useContext(ThemeContext);
|
2
src/context/index.js
Normal file
2
src/context/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./PreSetTheme";
|
||||
export * from "./ThemeContext";
|
6
src/hooks/index.js
Normal file
6
src/hooks/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export * from "./useAlgorithmInfo";
|
||||
export * from "./useIntroductionInfo";
|
||||
export * from "./useIsFirstLoad";
|
||||
export * from "./usePersistentState";
|
||||
export * from "./useSolverWorker";
|
||||
export * from "./useUpdateEffect";
|
45
src/hooks/useAlgorithmInfo.js
Normal file
45
src/hooks/useAlgorithmInfo.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { useStaticQuery, graphql } from "gatsby";
|
||||
|
||||
export const useAlgorithmInfo = () => {
|
||||
const {
|
||||
allMarkdownRemark: { edges: algorithms }
|
||||
} = useStaticQuery(graphql`
|
||||
query AlgorithmModalsQuery {
|
||||
allMarkdownRemark(
|
||||
filter: {
|
||||
frontmatter: {
|
||||
type: {
|
||||
in: [
|
||||
"exhaustive"
|
||||
"heuristic-construction"
|
||||
"heuristic-improvement"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
sort: { fields: frontmatter___order }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
frontmatter {
|
||||
order
|
||||
friendlyName
|
||||
solverKey
|
||||
type
|
||||
defaults {
|
||||
evaluatingDetailLevel
|
||||
maxEvaluatingDetailLevel
|
||||
}
|
||||
}
|
||||
html
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return algorithms.map(alg => ({
|
||||
...alg.node.frontmatter,
|
||||
html: alg.node.html
|
||||
}));
|
||||
};
|
21
src/hooks/useIntroductionInfo.js
Normal file
21
src/hooks/useIntroductionInfo.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { useStaticQuery, graphql } from "gatsby";
|
||||
|
||||
export const useIntroductionInfo = () => {
|
||||
const {
|
||||
allMarkdownRemark: { edges: introductions }
|
||||
} = useStaticQuery(graphql`
|
||||
query IntroductionModalQuery {
|
||||
allMarkdownRemark(
|
||||
filter: { frontmatter: { type: { eq: "introduction" } } }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
html
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return introductions[0].node.html;
|
||||
};
|
12
src/hooks/useIsFirstLoad.js
Normal file
12
src/hooks/useIsFirstLoad.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const useIsFirstLoad = (keyName = "isFirstVisit") => {
|
||||
if (!window.localStorage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!localStorage[keyName]) {
|
||||
localStorage.setItem(keyName, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
17
src/hooks/usePersistentState.js
Normal file
17
src/hooks/usePersistentState.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export const usePersistentState = (key, defaultValue = undefined) => {
|
||||
const [value, setValue] = useState(() => {
|
||||
const existing = typeof window !== "undefined" && window.localStorage?.getItem(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, value);
|
||||
}, [value, key]);
|
||||
|
||||
return [value, setValue];
|
||||
};
|
29
src/hooks/useSolverWorker.js
Normal file
29
src/hooks/useSolverWorker.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import solvers from "../solvers";
|
||||
|
||||
export const useSolverWorker = (onSolverMessage, algorithm) => {
|
||||
const [solver, setSolver] = useState();
|
||||
|
||||
const resetSolver = () => {
|
||||
if (solver) {
|
||||
solver.terminate();
|
||||
}
|
||||
const worker = new solvers[algorithm]();
|
||||
worker.onmessage = ({ data }) => onSolverMessage(data);
|
||||
worker.onerror = console.error;
|
||||
setSolver(worker);
|
||||
};
|
||||
|
||||
useEffect(resetSolver, [algorithm, onSolverMessage]);
|
||||
|
||||
const postMessage = data => {
|
||||
if (solver) {
|
||||
solver.postMessage(data);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
postMessage,
|
||||
terminate: resetSolver
|
||||
};
|
||||
};
|
14
src/hooks/useUpdateEffect.js
Normal file
14
src/hooks/useUpdateEffect.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
// https://stackoverflow.com/questions/55075604/react-hooks-useeffect-only-on-update
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const useUpdateEffect = (effect, dependencies = []) => {
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
} else {
|
||||
effect();
|
||||
}
|
||||
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
};
|
BIN
src/images/favicon.png
Normal file
BIN
src/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
103
src/pages/index.js
Normal file
103
src/pages/index.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
import React, {
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useState,
|
||||
useMemo
|
||||
} from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
AlgorithmModals,
|
||||
IntroductionModal,
|
||||
Layout,
|
||||
MapPlot,
|
||||
Menu,
|
||||
SEO,
|
||||
ThemeToggle
|
||||
} from "../components";
|
||||
import { useSolverWorker, useAlgorithmInfo } from "../hooks";
|
||||
import * as selectors from "../store/selectors";
|
||||
import * as actions from "../store/actions";
|
||||
|
||||
const IndexPage = () => {
|
||||
const mapRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const algorithm = useSelector(selectors.selectAlgorithm);
|
||||
const algorithmInfo = useAlgorithmInfo();
|
||||
const delay = useSelector(selectors.selectDelay);
|
||||
const evaluatingDetailLevel = useSelector(
|
||||
selectors.selectEvaluatingDetailLevel
|
||||
);
|
||||
const points = useSelector(selectors.selectPoints);
|
||||
const pointCount = useSelector(selectors.selectPointCount);
|
||||
const definingPoints = useSelector(selectors.selectDefiningPoints);
|
||||
|
||||
const solver = useSolverWorker(dispatch, algorithm);
|
||||
|
||||
const onRandomizePoints = useCallback(() => {
|
||||
if (!definingPoints) {
|
||||
const bounds = mapRef.current.getBounds();
|
||||
dispatch(actions.randomizePoints(bounds, pointCount));
|
||||
}
|
||||
}, [mapRef, dispatch, pointCount, definingPoints]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
dispatch(actions.startSolving(points, delay, evaluatingDetailLevel));
|
||||
solver.postMessage(
|
||||
actions.startSolvingAction(points, delay, evaluatingDetailLevel)
|
||||
);
|
||||
}, [solver, dispatch, delay, points, evaluatingDetailLevel]);
|
||||
|
||||
const fullSpeed = useCallback(() => {
|
||||
dispatch(actions.goFullSpeed());
|
||||
solver.postMessage(actions.goFullSpeed());
|
||||
}, [solver, dispatch]);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
dispatch(actions.pause());
|
||||
solver.postMessage(actions.pause());
|
||||
}, [solver, dispatch]);
|
||||
|
||||
const unpause = useCallback(() => {
|
||||
dispatch(actions.unpause());
|
||||
solver.postMessage(actions.unpause());
|
||||
}, [solver, dispatch]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
dispatch(actions.stopSolving());
|
||||
solver.terminate();
|
||||
}, [solver, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
solver.postMessage(actions.setDelay(delay));
|
||||
}, [delay, solver]);
|
||||
|
||||
useEffect(() => {
|
||||
solver.postMessage(actions.setEvaluatingDetailLevel(evaluatingDetailLevel));
|
||||
}, [evaluatingDetailLevel, solver]);
|
||||
|
||||
const algTitle = useMemo(() => {
|
||||
const alg = algorithmInfo.find(alg => alg.solverKey === algorithm);
|
||||
return alg.friendlyName;
|
||||
}, [algorithm, algorithmInfo]);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO subtitle={algTitle} />
|
||||
<IntroductionModal />
|
||||
<AlgorithmModals />
|
||||
<Menu
|
||||
onStart={start}
|
||||
onPause={pause}
|
||||
onUnPause={unpause}
|
||||
onFullSpeed={fullSpeed}
|
||||
onStop={stop}
|
||||
onRandomizePoints={onRandomizePoints}
|
||||
/>
|
||||
<MapPlot ref={mapRef}></MapPlot>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexPage;
|
56
src/solvers/cost.js
Normal file
56
src/solvers/cost.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
// haversine great circle distance
|
||||
export const distance = (pt1, pt2) => {
|
||||
const [lng1, lat1] = pt1;
|
||||
const [lng2, lat2] = pt2;
|
||||
if (lat1 === lat2 && lng1 === lng2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var radlat1 = (Math.PI * lat1) / 180;
|
||||
var radlat2 = (Math.PI * lat2) / 180;
|
||||
|
||||
var theta = lng1 - lng2;
|
||||
var radtheta = (Math.PI * theta) / 180;
|
||||
|
||||
var dist =
|
||||
Math.sin(radlat1) * Math.sin(radlat2) +
|
||||
Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
|
||||
|
||||
if (dist > 1) {
|
||||
dist = 1;
|
||||
}
|
||||
dist = Math.acos(dist);
|
||||
dist = (dist * 180) / Math.PI;
|
||||
return dist * 60 * 1.1515 * 1.609344;
|
||||
};
|
||||
|
||||
export const pathCost = path => {
|
||||
return path
|
||||
.slice(0, -1)
|
||||
.map((point, idx) => distance(point, path[idx + 1]))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
};
|
||||
|
||||
export const counterClockWise = (p, q, r) => {
|
||||
return (q[0] - p[0]) * (r[1] - q[1]) < (q[1] - p[1]) * (r[0] - q[0]);
|
||||
};
|
||||
|
||||
export const intersects = (a, b, c, d) => {
|
||||
return (
|
||||
counterClockWise(a, c, d) !== counterClockWise(b, c, d) &&
|
||||
counterClockWise(a, b, c) !== counterClockWise(a, b, d)
|
||||
);
|
||||
};
|
||||
|
||||
export const setDifference = (setA, setB) => {
|
||||
const ret = new Set(setA);
|
||||
setB.forEach(p => {
|
||||
ret.delete(p);
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const rotateToStartingPoint = (path, startingPoint) => {
|
||||
const startIdx = path.findIndex(p => p === startingPoint);
|
||||
path.unshift(...path.splice(startIdx, path.length));
|
||||
};
|
131
src/solvers/exhaustive/branchAndBoundOnCost.worker.js
Normal file
131
src/solvers/exhaustive/branchAndBoundOnCost.worker.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, setDifference } from "../cost";
|
||||
|
||||
import {
|
||||
EVALUATING_PATH_COLOR,
|
||||
EVALUATING_ERROR_COLOR,
|
||||
EVALUATING_SEGMENT_COLOR
|
||||
} from "../../constants";
|
||||
|
||||
const branchAndBoundOnCost = async (
|
||||
points,
|
||||
path = [],
|
||||
visited = null,
|
||||
overallBest = Infinity
|
||||
) => {
|
||||
if (visited === null) {
|
||||
// initial call
|
||||
path = [points.shift()];
|
||||
points = new Set(points);
|
||||
visited = new Set();
|
||||
}
|
||||
|
||||
// figure out which points are left
|
||||
const available = setDifference(points, visited);
|
||||
|
||||
// calculate the cost, from here, to go home
|
||||
const backToStart = [...path, path[0]];
|
||||
const cost = pathCost(backToStart);
|
||||
|
||||
if (cost > overallBest) {
|
||||
// we may not be done, but have already traveled further than the best path
|
||||
// no reason to continue
|
||||
self.setEvaluatingPaths(
|
||||
() => ({
|
||||
paths: [
|
||||
{
|
||||
path: path.slice(0, path.length - 1),
|
||||
color: EVALUATING_SEGMENT_COLOR
|
||||
},
|
||||
{
|
||||
path: path.slice(path.length - 2, path.length + 1),
|
||||
color: EVALUATING_ERROR_COLOR
|
||||
}
|
||||
],
|
||||
cost
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// still cheaper than the best, keep going deeper, and deeper, and deeper...
|
||||
else {
|
||||
self.setEvaluatingPaths(
|
||||
() => ({
|
||||
paths: [
|
||||
{
|
||||
path: path.slice(0, path.length - 1),
|
||||
color: EVALUATING_SEGMENT_COLOR
|
||||
},
|
||||
{
|
||||
path: path.slice(path.length - 2, path.length + 1),
|
||||
color: EVALUATING_PATH_COLOR
|
||||
}
|
||||
],
|
||||
cost
|
||||
}),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
await self.sleep();
|
||||
|
||||
if (available.size === 0) {
|
||||
// at the end of the path, return where we're at
|
||||
self.setEvaluatingPath(() => ({
|
||||
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
|
||||
cost
|
||||
}));
|
||||
|
||||
return [cost, backToStart];
|
||||
}
|
||||
|
||||
let [bestCost, bestPath] = [null, null];
|
||||
|
||||
// for every point yet to be visited along this path
|
||||
for (const p of available) {
|
||||
// go to that point
|
||||
visited.add(p);
|
||||
path.push(p);
|
||||
|
||||
// RECURSE - go through all the possible points from that point
|
||||
const [curCost, curPath] = await branchAndBoundOnCost(
|
||||
points,
|
||||
path,
|
||||
visited,
|
||||
overallBest
|
||||
);
|
||||
|
||||
// if that path is better and complete, keep it
|
||||
if (curCost && (!bestCost || curCost < bestCost)) {
|
||||
[bestCost, bestPath] = [curCost, curPath];
|
||||
|
||||
if (!overallBest || bestCost < overallBest) {
|
||||
// found a new best complete path
|
||||
overallBest = bestCost;
|
||||
self.setBestPath(bestPath, bestCost);
|
||||
}
|
||||
}
|
||||
|
||||
// go back up and make that point available again
|
||||
visited.delete(p);
|
||||
path.pop();
|
||||
|
||||
self.setEvaluatingPath(
|
||||
() => ({
|
||||
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
}
|
||||
|
||||
await self.sleep();
|
||||
return [bestCost, bestPath];
|
||||
};
|
||||
|
||||
makeSolver(branchAndBoundOnCost);
|
159
src/solvers/exhaustive/branchAndBoundOnCostAndCross.worker.js
Normal file
159
src/solvers/exhaustive/branchAndBoundOnCostAndCross.worker.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, setDifference, intersects } from "../cost";
|
||||
|
||||
import {
|
||||
EVALUATING_PATH_COLOR,
|
||||
EVALUATING_ERROR_COLOR,
|
||||
EVALUATING_SEGMENT_COLOR
|
||||
} from "../../constants";
|
||||
|
||||
const branchAndBoundOnCostAndCross = async (
|
||||
points,
|
||||
path = [],
|
||||
visited = null,
|
||||
overallBest = Infinity
|
||||
) => {
|
||||
if (visited === null) {
|
||||
// initial call
|
||||
path = [points.shift()];
|
||||
points = new Set(points);
|
||||
visited = new Set();
|
||||
}
|
||||
|
||||
// figure out which points are left
|
||||
const available = setDifference(points, visited);
|
||||
|
||||
// calculate the cost, from here, to go home
|
||||
const backToStart = [...path, path[0]];
|
||||
const cost = pathCost(backToStart);
|
||||
|
||||
if (path.length > 3) {
|
||||
// if this newly added edge crosses over the existing path,
|
||||
// don't continue. It's been proven that an optimal path will
|
||||
// not cross itself.
|
||||
const newSegment = [path[path.length - 2], path[path.length - 1]];
|
||||
for (let i = 1; i < path.length - 2; i++) {
|
||||
if (intersects(path[i], path[i - 1], ...newSegment)) {
|
||||
self.setEvaluatingPaths(
|
||||
() => ({
|
||||
paths: [
|
||||
{
|
||||
path: path.slice(0, path.length - 1),
|
||||
color: EVALUATING_SEGMENT_COLOR
|
||||
},
|
||||
{
|
||||
path: path.slice(path.length - 2, path.length + 1),
|
||||
color: EVALUATING_ERROR_COLOR
|
||||
}
|
||||
],
|
||||
cost
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cost > overallBest) {
|
||||
// we may not be done, but have already traveled further than the best path
|
||||
// no reason to continue
|
||||
self.setEvaluatingPaths(
|
||||
() => ({
|
||||
paths: [
|
||||
{
|
||||
path: path.slice(0, path.length - 1),
|
||||
color: EVALUATING_SEGMENT_COLOR
|
||||
},
|
||||
{
|
||||
path: path.slice(path.length - 2, path.length + 1),
|
||||
color: EVALUATING_ERROR_COLOR
|
||||
}
|
||||
],
|
||||
cost
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// still cheaper than the best, keep going deeper, and deeper, and deeper...
|
||||
self.setEvaluatingPaths(
|
||||
() => ({
|
||||
paths: [
|
||||
{
|
||||
path: path.slice(0, path.length - 1),
|
||||
color: EVALUATING_SEGMENT_COLOR
|
||||
},
|
||||
{
|
||||
path: path.slice(path.length - 2, path.length + 1),
|
||||
color: EVALUATING_PATH_COLOR
|
||||
}
|
||||
],
|
||||
cost
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
|
||||
if (available.size === 0) {
|
||||
// at the end of the path, return where we're at
|
||||
self.setEvaluatingPath(() => ({
|
||||
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR },
|
||||
cost
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
return [cost, backToStart];
|
||||
}
|
||||
|
||||
let [bestCost, bestPath] = [null, null];
|
||||
|
||||
// for every point yet to be visited along this path
|
||||
for (const p of available) {
|
||||
// go to that point
|
||||
visited.add(p);
|
||||
path.push(p);
|
||||
|
||||
// RECURSE - go through all the possible points from that point
|
||||
const [curCost, curPath] = await branchAndBoundOnCostAndCross(
|
||||
points,
|
||||
path,
|
||||
visited,
|
||||
overallBest
|
||||
);
|
||||
|
||||
// if that path is better and complete, keep it
|
||||
if (curCost && (!bestCost || curCost < bestCost)) {
|
||||
[bestCost, bestPath] = [curCost, curPath];
|
||||
|
||||
if (!overallBest || bestCost < overallBest) {
|
||||
// found a new best complete path
|
||||
overallBest = bestCost;
|
||||
self.setBestPath(bestPath, bestCost);
|
||||
}
|
||||
}
|
||||
|
||||
// go back up and make that point available again
|
||||
visited.delete(p);
|
||||
path.pop();
|
||||
|
||||
self.setEvaluatingPath(
|
||||
() => ({
|
||||
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
}
|
||||
|
||||
await self.sleep();
|
||||
return [bestCost, bestPath];
|
||||
};
|
||||
|
||||
makeSolver(branchAndBoundOnCostAndCross);
|
103
src/solvers/exhaustive/depthFirstSearch.worker.js
Normal file
103
src/solvers/exhaustive/depthFirstSearch.worker.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost } from "../cost";
|
||||
|
||||
import {
|
||||
EVALUATING_PATH_COLOR,
|
||||
EVALUATING_SEGMENT_COLOR
|
||||
} from "../../constants";
|
||||
|
||||
const setDifference = (setA, setB) => {
|
||||
const ret = new Set(setA);
|
||||
setB.forEach(p => {
|
||||
ret.delete(p);
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
const dfs = async (points, path = [], visited = null, overallBest = null) => {
|
||||
if (visited === null) {
|
||||
// initial call
|
||||
path = [points.shift()];
|
||||
points = new Set(points);
|
||||
visited = new Set();
|
||||
}
|
||||
|
||||
self.setEvaluatingPaths(
|
||||
() => ({
|
||||
paths: [
|
||||
{
|
||||
path: path.slice(0, path.length - 1),
|
||||
color: EVALUATING_SEGMENT_COLOR
|
||||
},
|
||||
{
|
||||
path: path.slice(path.length - 2, path.length + 1),
|
||||
color: EVALUATING_PATH_COLOR
|
||||
}
|
||||
]
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
|
||||
// figure out what points are left from this point
|
||||
const available = setDifference(points, visited);
|
||||
|
||||
if (available.size === 0) {
|
||||
// this must be a complete path
|
||||
const backToStart = [...path, path[0]];
|
||||
|
||||
// calculate the cost of this path
|
||||
const cost = pathCost(backToStart);
|
||||
|
||||
self.setEvaluatingPath(
|
||||
() => ({
|
||||
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR }
|
||||
}),
|
||||
cost
|
||||
);
|
||||
|
||||
await self.sleep();
|
||||
|
||||
// return both the cost and the path where we're at
|
||||
return [cost, backToStart];
|
||||
}
|
||||
|
||||
let [bestCost, bestPath] = [null, null];
|
||||
|
||||
// for every point yet to be visited along this path
|
||||
for (const p of available) {
|
||||
// go to that point
|
||||
visited.add(p);
|
||||
path.push(p);
|
||||
|
||||
// RECURSE - go through all the possible points from that point
|
||||
const [curCost, curPath] = await dfs(points, path, visited, overallBest);
|
||||
|
||||
// if that path is better, keep it
|
||||
if (bestCost === null || curCost < bestCost) {
|
||||
[bestCost, bestPath] = [curCost, curPath];
|
||||
|
||||
if (overallBest === null || bestCost < overallBest) {
|
||||
// found a new best complete path
|
||||
overallBest = bestCost;
|
||||
self.setBestPath(bestPath, bestCost);
|
||||
}
|
||||
}
|
||||
|
||||
// go back up and make that point available again
|
||||
visited.delete(p);
|
||||
path.pop();
|
||||
|
||||
self.setEvaluatingPath(
|
||||
() => ({
|
||||
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
}
|
||||
return [bestCost, bestPath];
|
||||
};
|
||||
|
||||
makeSolver(dfs);
|
41
src/solvers/exhaustive/random.worker.js
Normal file
41
src/solvers/exhaustive/random.worker.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost } from "../cost";
|
||||
|
||||
const random = async points => {
|
||||
let best = Infinity;
|
||||
|
||||
while (true) {
|
||||
// save off the starting point
|
||||
const start = points.shift();
|
||||
|
||||
// sort the remaining points
|
||||
const path = points.sort(() => Math.random() - 0.5);
|
||||
|
||||
// put the starting point back
|
||||
path.unshift(start);
|
||||
|
||||
// return to the starting point
|
||||
path.push(start);
|
||||
|
||||
// calculate the new cost
|
||||
const cost = pathCost(path);
|
||||
|
||||
if (cost < best) {
|
||||
// we found a better path
|
||||
best = cost;
|
||||
self.setBestPath(path, cost);
|
||||
}
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
|
||||
// get rid of starting point at the end
|
||||
path.pop();
|
||||
await self.sleep();
|
||||
}
|
||||
};
|
||||
|
||||
makeSolver(random);
|
|
@ -0,0 +1,63 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, distance } from "../cost";
|
||||
|
||||
const arbitraryInsertion = async points => {
|
||||
// from the starting point
|
||||
const path = [points.shift()];
|
||||
|
||||
//
|
||||
// INITIALIZATION - go to the nearest point first
|
||||
//
|
||||
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||
path.push(points.pop());
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
|
||||
// randomly sort points - this is the order they will be added
|
||||
// to the path
|
||||
points.sort(() => Math.random() - 0.5);
|
||||
|
||||
while (points.length > 0) {
|
||||
//
|
||||
// SELECTION - choose a next point randomly
|
||||
//
|
||||
const nextPoint = points.pop();
|
||||
|
||||
//
|
||||
// INSERTION - find the insertion spot that minimizes distance
|
||||
//
|
||||
let [bestCost, bestIdx] = [Infinity, null];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||
if (insertionCost < bestCost) {
|
||||
[bestCost, bestIdx] = [insertionCost, i];
|
||||
}
|
||||
}
|
||||
path.splice(bestIdx, 0, nextPoint);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
await self.sleep();
|
||||
|
||||
self.setBestPath(path, cost);
|
||||
};
|
||||
|
||||
makeSolver(arbitraryInsertion);
|
125
src/solvers/heuristic-construction/convexHull.worker.js
Normal file
125
src/solvers/heuristic-construction/convexHull.worker.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, counterClockWise, rotateToStartingPoint } from "../cost";
|
||||
import {
|
||||
EVALUATING_PATH_COLOR,
|
||||
EVALUATING_SEGMENT_COLOR
|
||||
} from "../../constants";
|
||||
|
||||
const convexHull = async points => {
|
||||
const sp = points[0];
|
||||
|
||||
// Find the "left most point"
|
||||
let leftmost = points[0];
|
||||
for (const p of points) {
|
||||
if (p[1] < leftmost[1]) {
|
||||
leftmost = p;
|
||||
}
|
||||
}
|
||||
|
||||
const path = [leftmost];
|
||||
|
||||
while (true) {
|
||||
const curPoint = path[path.length - 1];
|
||||
let [selectedIdx, selectedPoint] = [0, null];
|
||||
|
||||
// find the "most counterclockwise" point
|
||||
for (let [idx, p] of points.entries()) {
|
||||
// eslint-disable-next-line
|
||||
self.setEvaluatingPaths(
|
||||
() => ({
|
||||
paths: [
|
||||
{
|
||||
path: [...path, selectedPoint || curPoint],
|
||||
color: EVALUATING_SEGMENT_COLOR
|
||||
},
|
||||
{ path: [curPoint, p], color: EVALUATING_PATH_COLOR }
|
||||
]
|
||||
}),
|
||||
2
|
||||
);
|
||||
await self.sleep();
|
||||
|
||||
if (!selectedPoint || counterClockWise(curPoint, p, selectedPoint)) {
|
||||
// this point is counterclockwise with respect to the current hull
|
||||
// and selected point (e.g. more counterclockwise)
|
||||
[selectedIdx, selectedPoint] = [idx, p];
|
||||
}
|
||||
}
|
||||
|
||||
// adding this to the hull so it's no longer available
|
||||
points.splice(selectedIdx, 1);
|
||||
|
||||
// back to the furthest left point, formed a cycle, break
|
||||
if (selectedPoint === leftmost) {
|
||||
break;
|
||||
}
|
||||
|
||||
// add to hull
|
||||
path.push(selectedPoint);
|
||||
}
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path, color: EVALUATING_PATH_COLOR }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
await self.sleep();
|
||||
|
||||
while (points.length > 0) {
|
||||
let [bestRatio, bestPointIdx, insertIdx] = [Infinity, null, 0];
|
||||
|
||||
for (let [freeIdx, freePoint] of points.entries()) {
|
||||
// for every free point, find the point in the current path
|
||||
// that minimizes the cost of adding the path minus the cost of
|
||||
// the original segment
|
||||
let [bestCost, bestIdx] = [Infinity, 0];
|
||||
for (let [pathIdx, pathPoint] of path.entries()) {
|
||||
const nextPathPoint = path[(pathIdx + 1) % path.length];
|
||||
|
||||
// the new cost minus the old cost
|
||||
const evalCost =
|
||||
pathCost([pathPoint, freePoint, nextPathPoint]) -
|
||||
pathCost([pathPoint, nextPathPoint]);
|
||||
|
||||
if (evalCost < bestCost) {
|
||||
[bestCost, bestIdx] = [evalCost, pathIdx];
|
||||
}
|
||||
}
|
||||
|
||||
// figure out how "much" more expensive this is with respect to the
|
||||
// overall length of the segment
|
||||
const nextPoint = path[(bestIdx + 1) % path.length];
|
||||
const prevCost = pathCost([path[bestIdx], nextPoint]);
|
||||
const newCost = pathCost([path[bestIdx], freePoint, nextPoint]);
|
||||
const ratio = newCost / prevCost;
|
||||
|
||||
if (ratio < bestRatio) {
|
||||
[bestRatio, bestPointIdx, insertIdx] = [ratio, freeIdx, bestIdx + 1];
|
||||
}
|
||||
}
|
||||
|
||||
const [nextPoint] = points.splice(bestPointIdx, 1);
|
||||
path.splice(insertIdx, 0, nextPoint);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
await self.sleep();
|
||||
}
|
||||
|
||||
// rotate the array so that starting point is back first
|
||||
rotateToStartingPoint(path, sp);
|
||||
|
||||
// go back home
|
||||
path.push(sp);
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
self.setBestPath(path, cost);
|
||||
};
|
||||
|
||||
makeSolver(convexHull);
|
|
@ -0,0 +1,79 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, distance } from "../cost";
|
||||
|
||||
const furthestInsertion = async points => {
|
||||
// from the starting point
|
||||
const path = [points.shift()];
|
||||
|
||||
//
|
||||
// INITIALIZATION - go to the nearest point first
|
||||
//
|
||||
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||
path.push(points.pop());
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
|
||||
while (points.length > 0) {
|
||||
//
|
||||
// SELECTION - furthest point from the path
|
||||
//
|
||||
let [selectedDistance, selectedIdx] = [0, null];
|
||||
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||
// find the minimum distance to the path for freePoint
|
||||
let [bestCostToPath, costToPathIdx] = [Infinity, null];
|
||||
for (const pathPoint of path) {
|
||||
const dist = distance(freePoint, pathPoint);
|
||||
if (dist < bestCostToPath) {
|
||||
[bestCostToPath, costToPathIdx] = [dist, freePointIdx];
|
||||
}
|
||||
}
|
||||
|
||||
// if this point is further from the path than the currently selected
|
||||
if (bestCostToPath > selectedDistance) {
|
||||
[selectedDistance, selectedIdx] = [bestCostToPath, costToPathIdx];
|
||||
}
|
||||
}
|
||||
|
||||
// get the next point to add
|
||||
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||
|
||||
//
|
||||
// INSERTION - find the insertion spot that minimizes distance
|
||||
//
|
||||
let [bestCost, bestIdx] = [Infinity, null];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||
if (insertionCost < bestCost) {
|
||||
[bestCost, bestIdx] = [insertionCost, i];
|
||||
}
|
||||
}
|
||||
path.splice(bestIdx, 0, nextPoint);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
await self.sleep();
|
||||
|
||||
self.setBestPath(path, cost);
|
||||
};
|
||||
|
||||
makeSolver(furthestInsertion);
|
|
@ -0,0 +1,72 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, distance } from "../cost";
|
||||
|
||||
const nearestInsertion = async points => {
|
||||
// from the starting point
|
||||
const path = [points.shift()];
|
||||
|
||||
//
|
||||
// INITIALIZATION - go to the nearest point first
|
||||
//
|
||||
points.sort((a, b) => distance(path[0], b) - distance(path[0], a));
|
||||
path.push(points.pop());
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
|
||||
while (points.length > 0) {
|
||||
//
|
||||
// SELECTION - nearest point to the path
|
||||
//
|
||||
let [selectedDistance, selectedIdx] = [Infinity, null];
|
||||
for (const [freePointIdx, freePoint] of points.entries()) {
|
||||
for (const pathPoint of path) {
|
||||
const dist = distance(freePoint, pathPoint);
|
||||
if (dist < selectedDistance) {
|
||||
[selectedDistance, selectedIdx] = [dist, freePointIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get the next point to add
|
||||
const [nextPoint] = points.splice(selectedIdx, 1);
|
||||
|
||||
//
|
||||
// INSERTION - find the insertion spot that minimizes distance
|
||||
//
|
||||
let [bestCost, bestIdx] = [Infinity, null];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const insertionCost = pathCost([path[i - 1], nextPoint, path[i]]);
|
||||
if (insertionCost < bestCost) {
|
||||
[bestCost, bestIdx] = [insertionCost, i];
|
||||
}
|
||||
}
|
||||
path.splice(bestIdx, 0, nextPoint);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
await self.sleep();
|
||||
|
||||
self.setBestPath(path, cost);
|
||||
};
|
||||
|
||||
makeSolver(nearestInsertion);
|
40
src/solvers/heuristic-construction/nearestNeighbor.worker.js
Normal file
40
src/solvers/heuristic-construction/nearestNeighbor.worker.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, distance } from "../cost";
|
||||
|
||||
const nearestNeighbor = async points => {
|
||||
const path = [points.shift()];
|
||||
|
||||
while (points.length > 0) {
|
||||
// sort remaining points in place by their
|
||||
// distance from the last point in the current path
|
||||
points.sort(
|
||||
(a, b) =>
|
||||
distance(path[path.length - 1], b) - distance(path[path.length - 1], a)
|
||||
);
|
||||
|
||||
// go to the closest remaining point
|
||||
path.push(points.pop());
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
}
|
||||
|
||||
// return to start after visiting all other points
|
||||
path.push(path[0]);
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
await self.sleep();
|
||||
|
||||
self.setBestPath(path, cost);
|
||||
};
|
||||
|
||||
makeSolver(nearestNeighbor);
|
|
@ -0,0 +1,90 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost, distance, rotateToStartingPoint } from "../cost";
|
||||
import { EVALUATING_PATH_COLOR } from "../../constants";
|
||||
|
||||
const simulatedAnnealing = async points => {
|
||||
const sp = points[0];
|
||||
const path = points;
|
||||
|
||||
const tempCoeff =
|
||||
path.length < 10
|
||||
? 1 - 1e-4
|
||||
: path.length < 15
|
||||
? 1 - 1e-5
|
||||
: path.length < 30
|
||||
? 1 - 1e-6
|
||||
: 1 - 5e-7;
|
||||
|
||||
const deltaDistance = (aIdx, bIdx) => {
|
||||
const aPrev = (aIdx - 1 + path.length) % path.length;
|
||||
const aNext = (aIdx + 1 + path.length) % path.length;
|
||||
const bPrev = (bIdx - 1 + path.length) % path.length;
|
||||
const bNext = (bIdx + 1 + path.length) % path.length;
|
||||
let diff =
|
||||
distance(path[bPrev], path[aIdx]) +
|
||||
distance(path[aIdx], path[bNext]) +
|
||||
distance(path[aPrev], path[bIdx]) +
|
||||
distance(path[bIdx], path[aNext]) -
|
||||
distance(path[aPrev], path[aIdx]) -
|
||||
distance(path[aIdx], path[aNext]) -
|
||||
distance(path[bPrev], path[bIdx]) -
|
||||
distance(path[bIdx], path[bNext]);
|
||||
|
||||
if (bPrev === aIdx || bNext === aIdx) {
|
||||
diff += 2 * distance(path[aIdx], path[bIdx]);
|
||||
}
|
||||
return diff;
|
||||
};
|
||||
|
||||
const changePath = temperature => {
|
||||
// 2 random points
|
||||
const a = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||
const b = 1 + Math.floor(Math.random() * (path.length - 1));
|
||||
|
||||
const delta = deltaDistance(a, b);
|
||||
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
|
||||
// swap points
|
||||
[path[a], path[b]] = [path[b], path[a]];
|
||||
}
|
||||
};
|
||||
|
||||
const initialTemp = 100 * distance(path[0], path[1]);
|
||||
let i = 0;
|
||||
|
||||
for (
|
||||
let temperature = initialTemp;
|
||||
temperature > 1e-6;
|
||||
temperature *= tempCoeff
|
||||
) {
|
||||
changePath(temperature);
|
||||
if (i % 10000 == 0) {
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path, color: EVALUATING_PATH_COLOR }],
|
||||
cost: pathCost(path)
|
||||
}));
|
||||
await self.sleep();
|
||||
}
|
||||
if (i % 100000 == 0) {
|
||||
path.push(sp);
|
||||
self.setBestPath(path, pathCost(path));
|
||||
path.pop();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// rotate the array so that starting point is back first
|
||||
rotateToStartingPoint(path, sp);
|
||||
|
||||
// go back home
|
||||
path.push(sp);
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path }],
|
||||
cost
|
||||
}));
|
||||
self.setBestPath(path, cost);
|
||||
};
|
||||
|
||||
makeSolver(simulatedAnnealing);
|
72
src/solvers/heuristic-improvement/twoOptInversion.worker.js
Normal file
72
src/solvers/heuristic-improvement/twoOptInversion.worker.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost } from "../cost";
|
||||
|
||||
import {
|
||||
EVALUATING_PATH_COLOR,
|
||||
EVALUATING_SEGMENT_COLOR
|
||||
} from "../../constants";
|
||||
|
||||
const twoOptInversion = async path => {
|
||||
path.push(path[0]);
|
||||
let best = pathCost(path);
|
||||
let swapped = true;
|
||||
|
||||
self.setBestPath(path, best);
|
||||
|
||||
while (swapped) {
|
||||
swapped = false;
|
||||
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||
// section of the path to reverse
|
||||
const section = path.slice(pt1, pt2 + 1);
|
||||
|
||||
// reverse section in place
|
||||
section.reverse();
|
||||
|
||||
// replace section of path with reversed section in place
|
||||
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||
|
||||
// calculate new cost
|
||||
const newPath = path;
|
||||
const cost = pathCost(newPath);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [
|
||||
{ path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
|
||||
{ path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
|
||||
{ path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
|
||||
{
|
||||
path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
|
||||
color: EVALUATING_PATH_COLOR
|
||||
},
|
||||
{
|
||||
path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
|
||||
color: EVALUATING_PATH_COLOR
|
||||
}
|
||||
],
|
||||
cost
|
||||
}));
|
||||
await self.sleep();
|
||||
|
||||
if (cost < best) {
|
||||
// found a better path after the swap, keep it
|
||||
swapped = true;
|
||||
best = cost;
|
||||
self.setBestPath(newPath, best);
|
||||
} else {
|
||||
// un-reverse the section
|
||||
section.reverse();
|
||||
path.splice(pt1, pt2 + 1 - pt1, ...section);
|
||||
}
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [{ path, color: EVALUATING_SEGMENT_COLOR }]
|
||||
}));
|
||||
await self.sleep();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
makeSolver(twoOptInversion);
|
|
@ -0,0 +1,65 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import makeSolver from "../makeSolver";
|
||||
import { pathCost } from "../cost";
|
||||
|
||||
import {
|
||||
EVALUATING_PATH_COLOR,
|
||||
EVALUATING_SEGMENT_COLOR
|
||||
} from "../../constants";
|
||||
|
||||
const twoOptReciprocalExchange = async path => {
|
||||
path.push(path[0]);
|
||||
let best = pathCost(path);
|
||||
let swapped = true;
|
||||
|
||||
self.setBestPath(path, best);
|
||||
|
||||
while (swapped) {
|
||||
swapped = false;
|
||||
for (let pt1 = 1; pt1 < path.length - 1; pt1++) {
|
||||
for (let pt2 = pt1 + 1; pt2 < path.length - 1; pt2++) {
|
||||
// swap current pair of points
|
||||
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||
|
||||
// calculate new cost
|
||||
const cost = pathCost(path);
|
||||
|
||||
self.setEvaluatingPaths(() => ({
|
||||
paths: [
|
||||
{ path: path.slice(0, pt1), color: EVALUATING_SEGMENT_COLOR },
|
||||
{ path: path.slice(pt1 + 1, pt2), color: EVALUATING_SEGMENT_COLOR },
|
||||
{ path: path.slice(pt2 + 1), color: EVALUATING_SEGMENT_COLOR },
|
||||
{
|
||||
path: [path[pt1 - 1], path[pt1], path[pt1 + 1]],
|
||||
color: EVALUATING_PATH_COLOR
|
||||
},
|
||||
{
|
||||
path: [path[pt2 - 1], path[pt2], path[pt2 + 1]],
|
||||
color: EVALUATING_PATH_COLOR
|
||||
}
|
||||
],
|
||||
cost
|
||||
}));
|
||||
await self.sleep();
|
||||
|
||||
if (cost < best) {
|
||||
// found a better path after the swap, keep it
|
||||
swapped = true;
|
||||
best = cost;
|
||||
self.setBestPath(path, best);
|
||||
} else {
|
||||
// swap back - this one's worse
|
||||
[path[pt1], path[pt2]] = [path[pt2], path[pt1]];
|
||||
}
|
||||
|
||||
self.setEvaluatingPath(() => ({
|
||||
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||
}));
|
||||
|
||||
await self.sleep();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
makeSolver(twoOptReciprocalExchange);
|
31
src/solvers/index.js
Normal file
31
src/solvers/index.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import random from "./exhaustive/random.worker";
|
||||
import depthFirstSearch from "./exhaustive/depthFirstSearch.worker";
|
||||
import branchAndBoundOnCost from "./exhaustive/branchAndBoundOnCost.worker";
|
||||
import branchAndBoundOnCostAndCross from "./exhaustive/branchAndBoundOnCostAndCross.worker";
|
||||
|
||||
import nearestNeighbor from "./heuristic-construction/nearestNeighbor.worker";
|
||||
import arbitraryInsertion from "./heuristic-construction/arbitraryInsertion.worker";
|
||||
import nearestInsertion from "./heuristic-construction/nearestInsertion.worker";
|
||||
import furthestInsertion from "./heuristic-construction/furthestInsertion.worker";
|
||||
import convexHull from "./heuristic-construction/convexHull.worker";
|
||||
import simulatedAnnealing from "./heuristic-construction/simulatedAnnealing.worker";
|
||||
|
||||
import twoOptInversion from "./heuristic-improvement/twoOptInversion.worker";
|
||||
import twoOptReciprocalExchange from "./heuristic-improvement/twoOptReciprocalExchange.worker";
|
||||
|
||||
export default {
|
||||
random,
|
||||
depthFirstSearch,
|
||||
branchAndBoundOnCost,
|
||||
branchAndBoundOnCostAndCross,
|
||||
|
||||
nearestNeighbor,
|
||||
arbitraryInsertion,
|
||||
furthestInsertion,
|
||||
nearestInsertion,
|
||||
convexHull,
|
||||
simulatedAnnealing,
|
||||
|
||||
twoOptInversion,
|
||||
twoOptReciprocalExchange
|
||||
};
|
91
src/solvers/makeSolver.js
Normal file
91
src/solvers/makeSolver.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import * as actions from "../store/actions";
|
||||
|
||||
const wrapSolver = solver => async (...args) => {
|
||||
await solver(...args);
|
||||
self.postMessage(actions.stopSolvingAction());
|
||||
};
|
||||
|
||||
export const makeSolver = solver => {
|
||||
const run = wrapSolver(solver);
|
||||
|
||||
self.solverConfig = {
|
||||
detailLevel: 0,
|
||||
delay: 10,
|
||||
fullSpeed: false,
|
||||
paused: false
|
||||
};
|
||||
|
||||
self.setBestPath = (...args) => {
|
||||
self.postMessage(actions.setBestPath(...args));
|
||||
};
|
||||
|
||||
self.setEvaluatingPaths = (getPaths, level = 1) => {
|
||||
if (self.solverConfig.detailLevel >= level) {
|
||||
const { paths, cost } = getPaths();
|
||||
self.postMessage(actions.setEvaluatingPaths(paths, cost));
|
||||
}
|
||||
};
|
||||
|
||||
self.setEvaluatingPath = (getPath, level = 1) => {
|
||||
if (self.solverConfig.detailLevel >= level) {
|
||||
const { path, cost } = getPath();
|
||||
self.postMessage(actions.setEvaluatingPath(path, cost));
|
||||
}
|
||||
};
|
||||
|
||||
self.waitPause = async () => {
|
||||
while (self.solverConfig.paused) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
};
|
||||
|
||||
self.sleep = async () => {
|
||||
if (self.solverConfig.paused) {
|
||||
return await self.waitPause();
|
||||
}
|
||||
const duration = self.solverConfig.fullSpeed
|
||||
? 0
|
||||
: self.solverConfig.delay || 10;
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, duration);
|
||||
});
|
||||
};
|
||||
|
||||
self.onmessage = async ({ data: action }) => {
|
||||
switch (action.type) {
|
||||
case actions.START_SOLVING:
|
||||
self.solverConfig.delay = action.delay;
|
||||
self.solverConfig.detailLevel = action.evaluatingDetailLevel;
|
||||
self.solverConfig.fullSpeed = action.fullSpeed;
|
||||
run(action.points);
|
||||
break;
|
||||
|
||||
case actions.SET_DELAY:
|
||||
self.solverConfig.delay = action.delay;
|
||||
break;
|
||||
|
||||
case actions.SET_EVALUATING_DETAIL_LEVEL:
|
||||
self.solverConfig.detailLevel = action.level;
|
||||
break;
|
||||
|
||||
case actions.GO_FULL_SPEED:
|
||||
self.solverConfig.evaluatingDetailLevel = 0;
|
||||
self.solverConfig.fullSpeed = true;
|
||||
break;
|
||||
|
||||
case actions.PAUSE:
|
||||
self.solverConfig.paused = true;
|
||||
break;
|
||||
|
||||
case actions.UNPAUSE:
|
||||
self.solverConfig.paused = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`invalid action sent to solver ${action.type}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default makeSolver;
|
209
src/store/actions.js
Normal file
209
src/store/actions.js
Normal file
|
@ -0,0 +1,209 @@
|
|||
import gtmEmit from "./emitCustomEvent";
|
||||
export const SET_VIEWPORT_STATE = "SET_VIEWPORT_STATE";
|
||||
|
||||
export const RESET_EVALUATING_STATE = "RESET_EVALUATING_STATE";
|
||||
export const RESET_BEST_PATH_STATE = "RESET_BEST_PATH_STATE";
|
||||
|
||||
export const SET_ALGORITHM = "SET_ALGORITHM";
|
||||
export const SET_DELAY = "SET_DELAY";
|
||||
export const SET_EVALUATING_DETAIL_LEVEL = "SET_EVALUATING_DETAIL_LEVEL";
|
||||
export const SET_SHOW_BEST_PATH = "SET_SHOW_BEST_PATH";
|
||||
export const START_SOLVING = "START_SOLVING";
|
||||
export const GO_FULL_SPEED = "GO_FULL_SPEED";
|
||||
export const PAUSE = "PAUSE";
|
||||
export const UNPAUSE = "UNPAUSE";
|
||||
export const STOP_SOLVING = "STOP_SOLVING";
|
||||
|
||||
export const SET_BEST_PATH = "SET_BEST_PATH";
|
||||
export const SET_EVALUATING_PATHS = "SET_EVALUATING_PATHS";
|
||||
|
||||
export const START_DEFINING_POINTS = "START_DEFINING_POINTS";
|
||||
export const ADD_DEFINED_POINT = "ADD_DEFINED_POINT";
|
||||
export const STOP_DEFINING_POINTS = "STOP_DEFINING_POINTS";
|
||||
export const SET_POINT_COUNT = "SET_POINT_COUNT";
|
||||
export const SET_POINTS = "SET_POINTS";
|
||||
export const SET_DEFAULT_MAP = "SET_DEFAULT_MAP";
|
||||
|
||||
export const TOGGLE_SITE_INFO_OPEN = "TOGGLE_SITE_INFO_OPEN";
|
||||
export const TOGGLE_ALG_INFO_OPEN = "TOGGLE_ALG_INFO_OPEN";
|
||||
|
||||
const getRandomPoint = (max, min) => Math.random() * (max - min) + min;
|
||||
|
||||
//
|
||||
// BASIC UI
|
||||
//
|
||||
export const toggleSiteInfoOpen = () => ({
|
||||
type: TOGGLE_SITE_INFO_OPEN
|
||||
});
|
||||
|
||||
export const toggleAlgInfoOpen = () => ({
|
||||
type: TOGGLE_ALG_INFO_OPEN
|
||||
});
|
||||
|
||||
//
|
||||
// MAP INTERACTION
|
||||
//
|
||||
export const setViewportState = viewport => ({
|
||||
type: SET_VIEWPORT_STATE,
|
||||
viewport
|
||||
});
|
||||
|
||||
//
|
||||
// SOLVER CONTROLS
|
||||
//
|
||||
const resetEvaluatingStateAction = () => ({
|
||||
type: RESET_EVALUATING_STATE
|
||||
});
|
||||
|
||||
const resetBestPathStateAction = () => ({
|
||||
type: RESET_BEST_PATH_STATE
|
||||
});
|
||||
|
||||
const setAlgorithmAction = (algorithm, defaults) => ({
|
||||
type: SET_ALGORITHM,
|
||||
algorithm,
|
||||
defaults
|
||||
});
|
||||
|
||||
export const startSolvingAction = (points, delay, evaluatingDetailLevel) => ({
|
||||
type: START_SOLVING,
|
||||
points,
|
||||
delay,
|
||||
evaluatingDetailLevel,
|
||||
fullSpeed: false
|
||||
});
|
||||
|
||||
export const stopSolvingAction = () => ({
|
||||
type: STOP_SOLVING
|
||||
});
|
||||
|
||||
export const setAlgorithm = (algorithm, defaults = {}) => dispatch => {
|
||||
dispatch(resetEvaluatingStateAction());
|
||||
dispatch(setAlgorithmAction(algorithm, defaults));
|
||||
};
|
||||
|
||||
export const setDelay = delay => ({
|
||||
type: SET_DELAY,
|
||||
delay
|
||||
});
|
||||
|
||||
export const setEvaluatingDetailLevel = level => ({
|
||||
type: SET_EVALUATING_DETAIL_LEVEL,
|
||||
level
|
||||
});
|
||||
|
||||
export const setShowBestPath = show => ({
|
||||
type: SET_SHOW_BEST_PATH,
|
||||
show
|
||||
});
|
||||
|
||||
export const resetSolverState = () => dispatch => {
|
||||
dispatch(stopSolving());
|
||||
dispatch(resetEvaluatingStateAction());
|
||||
dispatch(resetBestPathStateAction());
|
||||
};
|
||||
|
||||
export const startSolving = (...args) => (dispatch, getState) => {
|
||||
const { algorithm, pointCount } = getState();
|
||||
gtmEmit({
|
||||
event: "start-solving",
|
||||
algorithm,
|
||||
pointCount
|
||||
});
|
||||
dispatch(resetEvaluatingStateAction());
|
||||
dispatch(startSolvingAction(...args));
|
||||
};
|
||||
|
||||
export const goFullSpeed = () => ({
|
||||
type: GO_FULL_SPEED
|
||||
});
|
||||
|
||||
export const pause = () => ({
|
||||
type: PAUSE
|
||||
});
|
||||
|
||||
export const unpause = () => ({
|
||||
type: UNPAUSE
|
||||
});
|
||||
|
||||
export const stopSolving = () => dispatch => {
|
||||
dispatch(resetEvaluatingStateAction());
|
||||
dispatch(stopSolvingAction());
|
||||
};
|
||||
|
||||
//
|
||||
// SOLVER ACTIONS
|
||||
//
|
||||
export const setEvaluatingPath = (path, cost) => ({
|
||||
type: SET_EVALUATING_PATHS,
|
||||
paths: [path],
|
||||
cost
|
||||
});
|
||||
|
||||
export const setEvaluatingPaths = (paths, cost) => ({
|
||||
type: SET_EVALUATING_PATHS,
|
||||
paths,
|
||||
cost
|
||||
});
|
||||
|
||||
export const setBestPath = (path, cost) => ({
|
||||
type: SET_BEST_PATH,
|
||||
path,
|
||||
cost
|
||||
});
|
||||
|
||||
//
|
||||
// POINT CONTROLS
|
||||
//
|
||||
const setDefaultMapAction = () => ({
|
||||
type: SET_DEFAULT_MAP
|
||||
});
|
||||
|
||||
const setPointsAction = points => ({
|
||||
type: SET_POINTS,
|
||||
points
|
||||
});
|
||||
|
||||
const setPointCountAction = count => ({
|
||||
type: SET_POINT_COUNT,
|
||||
count
|
||||
});
|
||||
|
||||
const startDefiningPointsAction = () => ({
|
||||
type: START_DEFINING_POINTS
|
||||
});
|
||||
|
||||
export const startDefiningPoints = () => dispatch => {
|
||||
dispatch(resetSolverState());
|
||||
dispatch(startDefiningPointsAction());
|
||||
};
|
||||
|
||||
export const addDefinedPoint = point => ({
|
||||
type: ADD_DEFINED_POINT,
|
||||
point
|
||||
});
|
||||
|
||||
export const stopDefiningPoints = () => ({
|
||||
type: STOP_DEFINING_POINTS
|
||||
});
|
||||
|
||||
export const setPointCount = count => dispatch => {
|
||||
dispatch(resetSolverState());
|
||||
dispatch(setPointCountAction(count));
|
||||
};
|
||||
|
||||
export const randomizePoints = bounds => (dispatch, getState) => {
|
||||
const { pointCount } = getState();
|
||||
const { top, bottom, left, right } = bounds;
|
||||
const points = Array.from({ length: pointCount }).map(_ => [
|
||||
getRandomPoint(right, left),
|
||||
getRandomPoint(top, bottom)
|
||||
]);
|
||||
dispatch(resetSolverState());
|
||||
dispatch(setPointsAction(points));
|
||||
};
|
||||
|
||||
export const setDefaultMap = (...args) => dispatch => {
|
||||
dispatch(resetSolverState());
|
||||
dispatch(setDefaultMapAction());
|
||||
};
|
7
src/store/emitCustomEvent.js
Normal file
7
src/store/emitCustomEvent.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default ev => {
|
||||
if (typeof window !== "undefined" && window.dataLayer) {
|
||||
window.dataLayer.push(ev);
|
||||
} else {
|
||||
console.log(ev);
|
||||
}
|
||||
};
|
242
src/store/reducer.js
Normal file
242
src/store/reducer.js
Normal file
|
@ -0,0 +1,242 @@
|
|||
import * as actions from "./actions";
|
||||
|
||||
const usTop12 = [
|
||||
[-73.85835427500902, 40.56507951957753],
|
||||
[-77.54976052500858, 38.772432514145194],
|
||||
[-78.91206521250587, 42.66742768420476],
|
||||
[-70.95796365000933, 42.66742768420476],
|
||||
[-80.27436990000314, 26.176558881220437],
|
||||
[-84.4052292750001, 34.108547937473524],
|
||||
[-82.55952615000031, 28.24770207922181],
|
||||
[-84.66890115000008, 30.089457425014395],
|
||||
[-89.89839333750201, 29.746655988569763],
|
||||
[-96.62202615000125, 32.640688397241334],
|
||||
[-95.3036667750014, 29.287759374472813],
|
||||
[-97.76460427500368, 30.089457425014395],
|
||||
[-101.89546365000065, 34.97727964358472],
|
||||
[-112.22261208749687, 33.23080293029681],
|
||||
[-111.38765114999953, 35.01327961148759],
|
||||
[-115.56245583750162, 36.08588188690158],
|
||||
[-118.63862771249869, 33.999320468363095],
|
||||
[-117.2323777124963, 32.97311239658548],
|
||||
[-123.12104958749816, 38.222145234071036],
|
||||
[-124.26362771250061, 41.13019627380825],
|
||||
[-120.13276833749595, 39.72528830651809],
|
||||
[-111.82710427499693, 41.13019627380825],
|
||||
[-105.2353073999977, 39.961475963760066],
|
||||
[-87.43745583749975, 41.69048709677229],
|
||||
[-93.1064011499991, 45.29144400095841],
|
||||
[-90.20601052499944, 38.772432514145194],
|
||||
[-117.27632302500142, 47.50341272285311],
|
||||
[-122.72554177499823, 45.8757982618686],
|
||||
[-122.81343240000076, 48.152468818056875]
|
||||
];
|
||||
|
||||
const initialViewport = {
|
||||
latitude: 39.8097343,
|
||||
longitude: -98.5556199,
|
||||
zoom: 4
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
points: usTop12.sort(() => Math.random() + 0.5),
|
||||
viewport: initialViewport,
|
||||
algorithm: "convexHull",
|
||||
delay: 100,
|
||||
evaluatingDetailLevel: 2,
|
||||
maxEvaluatingDetailLevel: 2,
|
||||
showBestPath: true,
|
||||
|
||||
bestPath: [],
|
||||
bestDisplaySegments: [],
|
||||
bestCost: null,
|
||||
|
||||
evaluatingPaths: [],
|
||||
evaluatingCost: null,
|
||||
running: false,
|
||||
fullSpeed: false,
|
||||
paused: false,
|
||||
startedRunningAt: null,
|
||||
|
||||
pointCount: usTop12.length,
|
||||
definingPoints: false,
|
||||
|
||||
siteInfoOpen: false,
|
||||
algInfoOpen: false
|
||||
};
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.TOGGLE_SITE_INFO_OPEN:
|
||||
return {
|
||||
...state,
|
||||
siteInfoOpen: !state.siteInfoOpen
|
||||
};
|
||||
|
||||
case actions.TOGGLE_ALG_INFO_OPEN:
|
||||
return {
|
||||
...state,
|
||||
algInfoOpen: !state.algInfoOpen
|
||||
};
|
||||
|
||||
case actions.SET_VIEWPORT_STATE:
|
||||
return {
|
||||
...state,
|
||||
viewport: action.viewport
|
||||
};
|
||||
|
||||
case actions.RESET_EVALUATING_STATE:
|
||||
return {
|
||||
...state,
|
||||
evaluatingPaths: [],
|
||||
evaluatingCost: null
|
||||
};
|
||||
|
||||
case actions.RESET_BEST_PATH_STATE:
|
||||
return {
|
||||
...state,
|
||||
bestPath: [],
|
||||
bestCost: null
|
||||
};
|
||||
|
||||
//
|
||||
// SOLVER CONTROLS
|
||||
//
|
||||
case actions.SET_ALGORITHM:
|
||||
return {
|
||||
...state,
|
||||
...action.defaults,
|
||||
algorithm: action.algorithm
|
||||
};
|
||||
|
||||
case actions.SET_DELAY:
|
||||
return {
|
||||
...state,
|
||||
delay: action.delay
|
||||
};
|
||||
|
||||
case actions.SET_EVALUATING_DETAIL_LEVEL:
|
||||
return {
|
||||
...state,
|
||||
evaluatingDetailLevel: action.level,
|
||||
evaluatingPaths: action.level ? state.evaluatingPaths : [],
|
||||
evaluatingCost: action.level ? state.evaluatingCost : null
|
||||
};
|
||||
|
||||
case actions.SET_SHOW_BEST_PATH:
|
||||
return {
|
||||
...state,
|
||||
showBestPath: action.show
|
||||
};
|
||||
|
||||
case actions.START_SOLVING:
|
||||
return {
|
||||
...state,
|
||||
showBestPath: false,
|
||||
running: true,
|
||||
startedRunningAt: Date.now(),
|
||||
pointCount: state.points.length
|
||||
};
|
||||
|
||||
case actions.GO_FULL_SPEED:
|
||||
return {
|
||||
...state,
|
||||
showBestPath: true,
|
||||
evaluatingDetailLevel: 0,
|
||||
evaluatingPaths: [],
|
||||
fullSpeed: true
|
||||
};
|
||||
|
||||
case actions.PAUSE:
|
||||
return {
|
||||
...state,
|
||||
paused: true,
|
||||
running: false
|
||||
};
|
||||
|
||||
case actions.UNPAUSE:
|
||||
return {
|
||||
...state,
|
||||
paused: false,
|
||||
running: true
|
||||
};
|
||||
|
||||
case actions.STOP_SOLVING:
|
||||
return {
|
||||
...state,
|
||||
points:
|
||||
state.bestPath.length > 0
|
||||
? state.bestPath.slice(0, state.bestPath.length - 1)
|
||||
: state.points,
|
||||
showBestPath: true,
|
||||
running: false,
|
||||
paused: false,
|
||||
fullSpeed: false,
|
||||
startedRunningAt: null
|
||||
};
|
||||
|
||||
//
|
||||
// SOLVER ACTIONS
|
||||
//
|
||||
case actions.SET_EVALUATING_PATHS:
|
||||
return {
|
||||
...state,
|
||||
evaluatingPaths: state.evaluatingDetailLevel ? action.paths : [],
|
||||
evaluatingCost: state.evaluatingDetailLevel ? action.cost : null
|
||||
};
|
||||
|
||||
case actions.SET_BEST_PATH:
|
||||
return {
|
||||
...state,
|
||||
bestPath: action.path,
|
||||
bestCost: action.cost
|
||||
};
|
||||
|
||||
//
|
||||
// POINT CONTROLS
|
||||
//
|
||||
case actions.SET_POINT_COUNT:
|
||||
return {
|
||||
...state,
|
||||
pointCount: action.count
|
||||
};
|
||||
|
||||
case actions.SET_POINTS:
|
||||
return {
|
||||
...state,
|
||||
points: action.points
|
||||
};
|
||||
|
||||
case actions.START_DEFINING_POINTS:
|
||||
return {
|
||||
...state,
|
||||
points: [],
|
||||
definingPoints: true,
|
||||
pointCount: 0
|
||||
};
|
||||
|
||||
case actions.ADD_DEFINED_POINT:
|
||||
return {
|
||||
...state,
|
||||
points: [...state.points, action.point],
|
||||
pointCount: state.pointCount + 1
|
||||
};
|
||||
|
||||
case actions.STOP_DEFINING_POINTS:
|
||||
return {
|
||||
...state,
|
||||
definingPoints: false
|
||||
};
|
||||
|
||||
case actions.SET_DEFAULT_MAP:
|
||||
return {
|
||||
...state,
|
||||
viewport: initialViewport,
|
||||
points: usTop12,
|
||||
pointCount: usTop12.length
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
94
src/store/selectors.js
Normal file
94
src/store/selectors.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { createSelector } from "reselect";
|
||||
import {
|
||||
START_POINT_COLOR,
|
||||
POINT_COLOR,
|
||||
BEST_PATH_COLOR,
|
||||
EVALUATING_PATH_COLOR
|
||||
} from "../constants";
|
||||
|
||||
//
|
||||
// FOR UI
|
||||
//
|
||||
export const selectSiteInfoOpen = state => state.siteInfoOpen;
|
||||
export const selectAlgInfoOpen = state => state.algInfoOpen;
|
||||
|
||||
//
|
||||
// FOR SOLVER CONTROLS
|
||||
//
|
||||
export const selectAlgorithm = state => state.algorithm;
|
||||
|
||||
export const selectDelay = state => state.delay;
|
||||
|
||||
export const selectEvaluatingDetailLevel = state => state.evaluatingDetailLevel;
|
||||
|
||||
export const selectMaxEvaluatingDetailLevel = state =>
|
||||
state.maxEvaluatingDetailLevel;
|
||||
|
||||
export const selectRunning = state => state.running;
|
||||
|
||||
export const selectFullSpeed = state => state.fullSpeed;
|
||||
|
||||
export const selectPaused = state => state.paused;
|
||||
|
||||
export const selectStartedRunningAt = state => state.startedRunningAt;
|
||||
|
||||
//
|
||||
// FOR POINT CONTROLS
|
||||
//
|
||||
export const selectDefiningPoints = state => state.definingPoints;
|
||||
|
||||
export const selectPointCount = state => state.pointCount;
|
||||
|
||||
//
|
||||
// FOR PLOT
|
||||
//
|
||||
|
||||
export const selectViewport = state => state.viewport;
|
||||
|
||||
export const selectPoints = state => state.points;
|
||||
export const selectPointsDisplay = createSelector(selectPoints, points =>
|
||||
points.map((p, idx) => ({
|
||||
position: p,
|
||||
color: idx === 0 ? START_POINT_COLOR : POINT_COLOR
|
||||
}))
|
||||
);
|
||||
|
||||
export const selectShowBestPath = state => state.showBestPath;
|
||||
export const selectBestPath = state => state.bestPath;
|
||||
export const selectBestPathDisplay = createSelector(
|
||||
selectBestPath,
|
||||
selectShowBestPath,
|
||||
(path, show) => ({
|
||||
path: show ? path : [],
|
||||
color: BEST_PATH_COLOR,
|
||||
width: 20
|
||||
})
|
||||
);
|
||||
|
||||
export const selectBestCost = state => state.bestCost;
|
||||
export const selectBestCostDisplay = createSelector(selectBestCost, cost =>
|
||||
cost ? cost.toFixed(2) : ""
|
||||
);
|
||||
|
||||
export const selectEvaluatingPaths = state => state.evaluatingPaths;
|
||||
export const selectEvaluatingPathsDisplay = createSelector(
|
||||
selectEvaluatingPaths,
|
||||
paths =>
|
||||
paths.map(({ path, color }) => ({
|
||||
path,
|
||||
color: color || EVALUATING_PATH_COLOR,
|
||||
width: 5
|
||||
}))
|
||||
);
|
||||
|
||||
export const selectEvaluatingCost = state => state.evaluatingCost;
|
||||
export const selectEvaluatingCostDisplay = createSelector(
|
||||
selectEvaluatingCost,
|
||||
cost => (cost ? cost.toFixed(2) : "")
|
||||
);
|
||||
|
||||
export const selectPlotPaths = createSelector(
|
||||
selectBestPathDisplay,
|
||||
selectEvaluatingPathsDisplay,
|
||||
(bestPath, evaluatingPaths) => [...evaluatingPaths, bestPath]
|
||||
);
|
5
src/store/store.js
Normal file
5
src/store/store.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createStore, applyMiddleware } from "redux";
|
||||
import thunk from "redux-thunk";
|
||||
import reducer from "./reducer";
|
||||
|
||||
export default createStore(reducer, applyMiddleware(thunk));
|
4
static/.well-known/brave-rewards-verification.txt
Normal file
4
static/.well-known/brave-rewards-verification.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
This is a Brave Rewards publisher verification file.
|
||||
|
||||
Domain: tspvis.com
|
||||
Token: b687d7e75fc19d979ab3ab8b264164a91d8206d0bb2ffa33334069d9092406b1
|
1
static/CNAME
Normal file
1
static/CNAME
Normal file
|
@ -0,0 +1 @@
|
|||
tspvis.com
|
2
static/robots.txt
Normal file
2
static/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Allow: /
|
Loading…
Add table
Reference in a new issue