first and last commit
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
commit
4a37207905
56 changed files with 32326 additions and 0 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
.cache/
|
||||||
|
public/
|
15
.woodpecker.yml
Normal file
15
.woodpecker.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: push
|
||||||
|
- event: tag
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: deploy
|
||||||
|
image: node
|
||||||
|
commands:
|
||||||
|
- npm i
|
||||||
|
- npm run build
|
||||||
|
- rm -rf /mnt/caddy-sites/tsib.techtransthai.org/*
|
||||||
|
- cp -r public/* /mnt/caddy-sites/tsib.techtransthai.org/
|
||||||
|
volumes:
|
||||||
|
- /media/core/Data1/Apps/caddy/sites:/mnt/caddy-sites
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
FROM node:latest
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025
|
||||||
|
|
||||||
|
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.
|
9
compose.yaml
Normal file
9
compose.yaml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
nodejs-app:
|
||||||
|
image: node:latest
|
||||||
|
working_dir: /usr/src/app
|
||||||
|
volumes:
|
||||||
|
- .:/usr/src/app
|
||||||
|
command: sh -cx "npm install && npx nodemon"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
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-inter";
|
||||||
|
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" />]);
|
||||||
|
};
|
6
nodemon.json
Normal file
6
nodemon.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"watch": ["src"],
|
||||||
|
"ext": "js,css,html,json,jsx",
|
||||||
|
"exec": "npm run build && serve --ssl-cert tsp.test.pem --ssl-key tsp.test-key.pem -s public"
|
||||||
|
}
|
||||||
|
|
30050
package-lock.json
generated
Normal file
30050
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
59
package.json
Normal file
59
package.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"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-inter": "^3.18.1",
|
||||||
|
"typeface-roboto": "0.0.75",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-react": "^7.23.2",
|
||||||
|
"nodemon": "^3.1.9",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"serve": "^14.2.4"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"gatsby"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "gatsby build",
|
||||||
|
"develop": "npx gatsby develop --host=0.0.0.0",
|
||||||
|
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
|
||||||
|
"start": "npm run develop",
|
||||||
|
"serve": "gatsby serve --host=0.0.0.0",
|
||||||
|
"clean": "gatsby clean"
|
||||||
|
}
|
||||||
|
}
|
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";
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
|
console.log(plotPoints.map(x => x.position));
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
});
|
35
src/components/MemoryUsage.jsx
Normal file
35
src/components/MemoryUsage.jsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
|
||||||
|
export const MemoryUsage = () => {
|
||||||
|
const [memoryUsage, setMemoryUsage] = useState("Loading...");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function updateMemoryUsage() {
|
||||||
|
if (performance.memory) {
|
||||||
|
const memoryInfo = performance.memory;
|
||||||
|
const usedMB = (memoryInfo.usedJSHeapSize / 1024 / 1024).toFixed(2);
|
||||||
|
setMemoryUsage(`Used: ${usedMB} MB`);
|
||||||
|
} else {
|
||||||
|
setMemoryUsage("Memory info not supported in this browser. <br/> Please use Chrome");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setInterval(updateMemoryUsage, 500);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "10px" }}>
|
||||||
|
<Typography color="textPrimary" component="div">
|
||||||
|
<Typography color="textSecondary" component="div">
|
||||||
|
Client-Side Memory Usage
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography color="textSecondary" component="div" style={{ fontSize: "1.2em", fontWeight: "bold" }}>
|
||||||
|
{memoryUsage}
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
65
src/components/Menu.jsx
Normal file
65
src/components/Menu.jsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { makeStyles } from "@material-ui/styles";
|
||||||
|
import { Paper, Divider } from "@material-ui/core";
|
||||||
|
|
||||||
|
import { MenuSolverControls } from "./MenuSolverControls";
|
||||||
|
import { MenuMetrics } from "./MenuMetrics";
|
||||||
|
import { MemoryUsage } from "./MemoryUsage";
|
||||||
|
import { useThemeContext } from "../context";
|
||||||
|
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,
|
||||||
|
|
||||||
|
onStop,
|
||||||
|
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { muiTheme, colorMode } = useThemeContext();
|
||||||
|
const backgroundStyle = useMemo(() =>
|
||||||
|
colorMode === "dark" ? "#101010" : "#ffffff",
|
||||||
|
[colorMode]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper classes={{ root: classes.wrapper }} style={{
|
||||||
|
position: "fixed",
|
||||||
|
width: "370px",
|
||||||
|
height: "625px",
|
||||||
|
top: "80px",
|
||||||
|
left: "20px",
|
||||||
|
color: "white",
|
||||||
|
padding: "15px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
boxShadow: "2px 2px 10px rgba(0, 0, 0, 0.3)",
|
||||||
|
backgroundColor: backgroundStyle,
|
||||||
|
}}>
|
||||||
|
<MenuMetrics />
|
||||||
|
<Divider />
|
||||||
|
<MenuSolverControls
|
||||||
|
onStart={onStart}
|
||||||
|
onPause={onPause}
|
||||||
|
onUnPause={onUnPause}
|
||||||
|
onStop={onStop}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<MemoryUsage />
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
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(Number(((Date.now() - startedRunningAt) / 1000).toFixed(3)));
|
||||||
|
}, 50);
|
||||||
|
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.toFixed(3) || ""}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
classes={{ root: classes.unit }}
|
||||||
|
align="right"
|
||||||
|
display="inline"
|
||||||
|
variant="button"
|
||||||
|
>
|
||||||
|
s
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuSection>
|
||||||
|
);
|
||||||
|
};
|
24
src/components/MenuSection.jsx
Normal file
24
src/components/MenuSection.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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),
|
||||||
|
border: ({ highlight = false }) =>
|
||||||
|
highlight ? `2px solid ${theme.palette.grey[100]}` : "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>
|
||||||
|
);
|
||||||
|
};
|
135
src/components/MenuSolverControls.jsx
Normal file
135
src/components/MenuSolverControls.jsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
onStop
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const algorithms = useAlgorithmInfo();
|
||||||
|
const selectedAlgorithm = useSelector(selectors.selectAlgorithm);
|
||||||
|
const delay = useSelector(selectors.selectDelay);
|
||||||
|
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 onReset = () => {
|
||||||
|
onStop();
|
||||||
|
dispatch(actions.resetSolverState());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuSection highlight>
|
||||||
|
<MenuItem title="Algorithm">
|
||||||
|
<Grid container alignItems="center">
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<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>Exhaustive</ListSubheader>
|
||||||
|
{algorithms
|
||||||
|
.filter(alg => alg.type === "exhaustive")
|
||||||
|
.map(alg => (
|
||||||
|
<SelectItem value={alg.solverKey} key={alg.solverKey}>
|
||||||
|
{alg.friendlyName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem title="Controls">
|
||||||
|
<ButtonGroup
|
||||||
|
fullWidth
|
||||||
|
variant="filled"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={paused ? onUnPause : running ? onPause : onStart}
|
||||||
|
disabled={definingPoints || fullSpeed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={paused ? faPlay : running ? faPause : faPlay}
|
||||||
|
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="primary"
|
||||||
|
disabled={definingPoints || fullSpeed}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuSection>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
69
src/components/Navbar.jsx
Normal file
69
src/components/Navbar.jsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from "gatsby";
|
||||||
|
import logo from "../images/favicon.png";
|
||||||
|
|
||||||
|
export const Navbar = () => {
|
||||||
|
const navbarStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '10px 20px',
|
||||||
|
backgroundColor: '#101010',
|
||||||
|
color: 'white',
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
zIndex: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '15px',
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '20px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontSize: '16px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav style={navbarStyle}>
|
||||||
|
<div style={leftStyle}>
|
||||||
|
{/* Wrap both the logo and text inside the same Link */}
|
||||||
|
<Link to="/" style={linkStyle}>
|
||||||
|
<img src={logo} alt="Logo" style={{ height: '30px' }} />
|
||||||
|
TSP Visualization
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div style={rightStyle}>
|
||||||
|
<Link
|
||||||
|
to="/about"
|
||||||
|
style={linkStyle}
|
||||||
|
onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
|
||||||
|
onMouseOut={(e) => e.target.style.textDecoration = 'none'}>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="https://forge.techtransthai.org/deepseekers/traveling-salesman-bangkok"
|
||||||
|
style={linkStyle}
|
||||||
|
onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
|
||||||
|
onMouseOut={(e) => e.target.style.textDecoration = 'none'}>
|
||||||
|
Source Code
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
26
src/components/SEO.jsx
Normal file
26
src/components/SEO.jsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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://tsib.techtransthai.org/" />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
</Helmet>
|
||||||
|
);
|
||||||
|
};
|
13
src/components/index.js
Normal file
13
src/components/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export * from "./AlgorithmModals";
|
||||||
|
export * from "./InformationModal";
|
||||||
|
export * from "./IntroductionModal";
|
||||||
|
export * from "./Layout";
|
||||||
|
export * from "./MapPlot";
|
||||||
|
export * from "./Menu";
|
||||||
|
export * from "./MenuItem";
|
||||||
|
export * from "./MenuMetrics";
|
||||||
|
export * from "./MenuSection";
|
||||||
|
export * from "./MenuSolverControls";
|
||||||
|
export * from "../context/PreSetTheme";
|
||||||
|
export * from "./SEO";
|
||||||
|
export * from "./Navbar";
|
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";
|
9
src/content/exhaustive/depthFirstSearch.md
Normal file
9
src/content/exhaustive/depthFirstSearch.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
type: exhaustive
|
||||||
|
order: 1
|
||||||
|
solverKey: depthFirstSearch
|
||||||
|
friendlyName: Depth First Search (Brute Force)
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 2
|
||||||
|
maxEvaluatingDetailLevel: 2
|
||||||
|
---
|
9
src/content/heurisitc-construction/nearestNeighbor.md
Normal file
9
src/content/heurisitc-construction/nearestNeighbor.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
type: heuristic-construction
|
||||||
|
order: 1
|
||||||
|
solverKey: nearestNeighbor
|
||||||
|
friendlyName: Nearest Neighbor
|
||||||
|
defaults:
|
||||||
|
evaluatingDetailLevel: 1
|
||||||
|
maxEvaluatingDetailLevel: 1
|
||||||
|
---
|
4
src/content/introduction.md
Normal file
4
src/content/introduction.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
type: introduction
|
||||||
|
---
|
||||||
|
|
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);
|
||||||
|
})();
|
||||||
|
`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
53
src/context/ThemeContext.jsx
Normal file
53
src/context/ThemeContext.jsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
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: {
|
||||||
|
type: colorMode,
|
||||||
|
primary: blue,
|
||||||
|
secondary: orange,
|
||||||
|
// backgroundColor: colorMode === "dark" ? "#101010" : "#ffffff",
|
||||||
|
// background: colorMode === "dark" ? "#ffffff" : "#ffffff",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[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";
|
44
src/hooks/useAlgorithmInfo.js
Normal file
44
src/hooks/useAlgorithmInfo.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { useStaticQuery, graphql } from "gatsby";
|
||||||
|
|
||||||
|
export const useAlgorithmInfo = () => {
|
||||||
|
const {
|
||||||
|
allMarkdownRemark: { edges: algorithms }
|
||||||
|
} = useStaticQuery(graphql`
|
||||||
|
query AlgorithmModalsQuery {
|
||||||
|
allMarkdownRemark(
|
||||||
|
filter: {
|
||||||
|
frontmatter: {
|
||||||
|
type: {
|
||||||
|
in: [
|
||||||
|
"exhaustive"
|
||||||
|
"heuristic-construction"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: 4.1 KiB |
59
src/pages/about.js
Normal file
59
src/pages/about.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Navbar
|
||||||
|
} from "../components";
|
||||||
|
|
||||||
|
|
||||||
|
const AboutPage = () => {
|
||||||
|
const containerStyle = {
|
||||||
|
maxWidth: '900px',
|
||||||
|
margin: '80px auto 20px',
|
||||||
|
padding: '0 20px',
|
||||||
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
<div style={containerStyle}>
|
||||||
|
<h1>About the Traveling Salesman Problem (TSP)</h1>
|
||||||
|
<h2>What is the Traveling Salesman Problem?</h2>
|
||||||
|
<p>The Traveling Salesman Problem (TSP) is a classic optimization problem in computer science and operations research. It asks:</p>
|
||||||
|
<p><em>"Given a list of cities and the distances between them, what is the shortest possible route that visits each city exactly once and returns to the starting point?"</em></p>
|
||||||
|
<p>TSP has applications in logistics, manufacturing, and route planning. However, solving it efficiently becomes difficult as the number of cities increases.</p>
|
||||||
|
|
||||||
|
<h2>Solving TSP with Blind Search</h2>
|
||||||
|
<p>Blind search methods explore solutions without using problem-specific knowledge:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Brute Force:</strong> Generates all routes and picks the shortest. It guarantees an optimal solution but has a factorial time complexity.</li>
|
||||||
|
<li><strong>Breadth-First Search (BFS):</strong> Explores routes level by level but grows exponentially in complexity.</li>
|
||||||
|
<li><strong>Depth-First Search (DFS):</strong> Traverses full paths before backtracking but may not be optimal.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Blind search methods are inefficient for large-scale TSP instances.</p>
|
||||||
|
|
||||||
|
<h2>Solving TSP with Heuristic Search</h2>
|
||||||
|
<p>Heuristic search methods use problem-specific knowledge to find solutions efficiently:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Greedy Algorithm:</strong> Chooses the nearest unvisited city but does not guarantee the best solution.</li>
|
||||||
|
<li><strong>A* Search:</strong> Uses cost estimation to optimize the search.</li>
|
||||||
|
<li><strong>Genetic Algorithms:</strong> Uses evolution-based optimization.</li>
|
||||||
|
<li><strong>Simulated Annealing:</strong> Uses randomization to escape local optima.</li>
|
||||||
|
<li><strong>Ant Colony Optimization:</strong> Mimics how ants find paths efficiently.</li>
|
||||||
|
</ul>
|
||||||
|
<p>These methods balance accuracy and computational efficiency, making them suitable for real-world applications.</p>
|
||||||
|
|
||||||
|
<h2>Conclusion</h2>
|
||||||
|
<p>The Traveling Salesman Problem is a fundamental challenge in optimization and AI. While blind search guarantees the optimal route, its cost is too high for large problems. Heuristic search algorithms provide practical alternatives that yield near-optimal solutions efficiently.</p>
|
||||||
|
<h4>Created by</h4>
|
||||||
|
<ul>
|
||||||
|
<li>64010823 วีรภัทร อินอุดม</li>
|
||||||
|
<li>64010543 พงศ์ภีระ วงศประสิทธิพร</li>
|
||||||
|
<li>64011106 ณรงค์พล กิจรังสรรค์</li>
|
||||||
|
<li>64011160 นนทัช มุกลีมาศ</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutPage;
|
91
src/pages/index.js
Normal file
91
src/pages/index.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
useMemo
|
||||||
|
} from "react";
|
||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import {
|
||||||
|
AlgorithmModals,
|
||||||
|
IntroductionModal,
|
||||||
|
Layout,
|
||||||
|
MapPlot,
|
||||||
|
Menu,
|
||||||
|
SEO,
|
||||||
|
ThemeToggle,
|
||||||
|
Navbar
|
||||||
|
} 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 solver = useSolverWorker(dispatch, algorithm);
|
||||||
|
|
||||||
|
|
||||||
|
const start = useCallback(() => {
|
||||||
|
dispatch(actions.startSolving(points, delay, evaluatingDetailLevel));
|
||||||
|
solver.postMessage(
|
||||||
|
actions.startSolvingAction(points, delay, evaluatingDetailLevel)
|
||||||
|
);
|
||||||
|
}, [solver, dispatch, delay, points, evaluatingDetailLevel]);
|
||||||
|
|
||||||
|
|
||||||
|
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 />
|
||||||
|
<Navbar />
|
||||||
|
<Menu
|
||||||
|
onStart={start}
|
||||||
|
onPause={pause}
|
||||||
|
onUnPause={unpause}
|
||||||
|
onStop={stop}
|
||||||
|
/>
|
||||||
|
<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));
|
||||||
|
};
|
91
src/solvers/exhaustive/depthFirstSearch.worker.js
Normal file
91
src/solvers/exhaustive/depthFirstSearch.worker.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
const available = setDifference(points, visited);
|
||||||
|
|
||||||
|
if (available.size === 0) {
|
||||||
|
const backToStart = [...path, path[0]];
|
||||||
|
|
||||||
|
const cost = pathCost(backToStart);
|
||||||
|
|
||||||
|
self.setEvaluatingPath(
|
||||||
|
() => ({
|
||||||
|
path: { path: backToStart, color: EVALUATING_SEGMENT_COLOR }
|
||||||
|
}),
|
||||||
|
cost
|
||||||
|
);
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
return [cost, backToStart];
|
||||||
|
}
|
||||||
|
|
||||||
|
let [bestCost, bestPath] = [null, null];
|
||||||
|
|
||||||
|
for (const p of available) {
|
||||||
|
visited.add(p);
|
||||||
|
path.push(p);
|
||||||
|
|
||||||
|
const [curCost, curPath] = await dfs(points, path, visited, overallBest);
|
||||||
|
|
||||||
|
if (bestCost === null || curCost < bestCost) {
|
||||||
|
[bestCost, bestPath] = [curCost, curPath];
|
||||||
|
|
||||||
|
if (overallBest === null || bestCost < overallBest) {
|
||||||
|
overallBest = bestCost;
|
||||||
|
self.setBestPath(bestPath, bestCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.delete(p);
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
self.setEvaluatingPath(
|
||||||
|
() => ({
|
||||||
|
path: { path, color: EVALUATING_SEGMENT_COLOR }
|
||||||
|
}),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
return [bestCost, bestPath];
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(dfs);
|
35
src/solvers/heuristic-construction/nearestNeighbor.worker.js
Normal file
35
src/solvers/heuristic-construction/nearestNeighbor.worker.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import makeSolver from "../makeSolver";
|
||||||
|
import { pathCost, distance } from "../cost";
|
||||||
|
|
||||||
|
const nearestNeighbor = async points => {
|
||||||
|
const path = [points.shift()];
|
||||||
|
|
||||||
|
while (points.length > 0) {
|
||||||
|
points.sort(
|
||||||
|
(a, b) =>
|
||||||
|
distance(path[path.length - 1], b) - distance(path[path.length - 1], a)
|
||||||
|
);
|
||||||
|
|
||||||
|
path.push(points.pop());
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost: pathCost(path)
|
||||||
|
}));
|
||||||
|
|
||||||
|
await self.sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
path.push(path[0]);
|
||||||
|
const cost = pathCost(path);
|
||||||
|
|
||||||
|
self.setEvaluatingPaths(() => ({
|
||||||
|
paths: [{ path }],
|
||||||
|
cost
|
||||||
|
}));
|
||||||
|
await self.sleep();
|
||||||
|
|
||||||
|
self.setBestPath(path, cost);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeSolver(nearestNeighbor);
|
7
src/solvers/index.js
Normal file
7
src/solvers/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import depthFirstSearch from "./exhaustive/depthFirstSearch.worker";
|
||||||
|
import nearestNeighbor from "./heuristic-construction/nearestNeighbor.worker";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
depthFirstSearch,
|
||||||
|
nearestNeighbor,
|
||||||
|
};
|
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);
|
||||||
|
}
|
||||||
|
};
|
223
src/store/reducer.js
Normal file
223
src/store/reducer.js
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
import * as actions from "./actions";
|
||||||
|
|
||||||
|
const usTop12 = [
|
||||||
|
[100.47966551545956, 13.763377348238809],
|
||||||
|
// [95.99777586767772, 20.659057411016644], //myanmar
|
||||||
|
// [102.61447417504604, 18.354304323036597], //laos
|
||||||
|
// [104.82860225771425, 12.260559642146927], //cambodia
|
||||||
|
[105.61972922924626, 21.337690594700124],
|
||||||
|
[101.61025278140067, 3.852198947284515],
|
||||||
|
[106.83275037018893, -6.209465092032497],
|
||||||
|
[103.81336272752452, 1.3468280345835395],
|
||||||
|
[114.92250057366596, 4.945297365972065],
|
||||||
|
[121.00856338015852, 14.602156304250775]
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialViewport = {
|
||||||
|
latitude: 8.880258536666247,
|
||||||
|
longitude: 113.01211067669622,
|
||||||
|
zoom: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
points: usTop12,
|
||||||
|
viewport: initialViewport,
|
||||||
|
algorithm: "depthFirstSearch",
|
||||||
|
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));
|
28
tsp.test-key.pem
Normal file
28
tsp.test-key.pem
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDKNFnAZsFXsOg4
|
||||||
|
UFXFSIvG/9P60FQLqoeUwmf87f2dWUSwbeX36alE+SjJt4MFipdnuMe5OzGrTdvq
|
||||||
|
6PGwt70aLI8Ox6QPtk3/2AL7EmCXesgJ5V1kpf0WLZX8j2vE7q7FK+lG/MYXvnJ9
|
||||||
|
5FDGWMuzyPoyu5LU/dQwwTYAPMZRUm/bis/yYh/310Hr7JYoWRVGd7HiSRIKMOKZ
|
||||||
|
CZ5IHReAwpXYPjvvbuULnxDbjULquMfDbQUsuWJexMEoEHIof2/ZXfxSH6PfMRaX
|
||||||
|
zKz5lxsW8BjV5RoejUjSDyxSsOlM0vh3eVMBZFQDCbEzlbw0D9aSIVEbNwUbUYj5
|
||||||
|
pBPTWLtTAgMBAAECggEBAI+Arb21bzu7ymLE6Mo1XTXis9+J4EvTP5uciM5hXJ9C
|
||||||
|
DlSj+hSCmOXhakgWW/8fx6oN4nicAPkRLaU+ouCG1cbwnqqfltiryhlrhVoIRdLb
|
||||||
|
iYI0bJ6UitQlkA+I/bPqrNA0BL+jfza0q26bDZRmylKSrLY6ls9gQSpExP1QJHLr
|
||||||
|
KobE0xAkR2T/ShOdngwXhYZphnNimnDoJvBbl9ptEtfDevLr39Z5HJkGbguT+6Sf
|
||||||
|
eNu9Z9LHOMjjpvqa59XdSCsRRzOvuQsoPXJmkechbM1D+eZ/oAiOScRjytLbkUYH
|
||||||
|
YBla7zmdMah6A9FOEylZAQT9tZjwsuRKrxaJpoB3TnECgYEA31UmTDJE5r5mpiOl
|
||||||
|
FIS6COsFGc58wopvjlpxopgrE9JwKckyy9ds6YWGlzJJpxBopZ/NA0f5dzzTOUz1
|
||||||
|
pGEoGdE70hefINQBF01x5MQ/JW0SjY9d4uLkrC8RHad+HoiSOM7pL61V/AtQk1Oi
|
||||||
|
9jhfdImBM9i2e2Txb3WLO1+VCGkCgYEA58gLFds1deTum32729/h29x2fd94dmHs
|
||||||
|
46Da4OYbGc7ngGTkh+hyg07+gbJ0q8AvPm3Ot6Xoz4dvqxSeDptfvC96yNnDRx2y
|
||||||
|
NZRAker3GWtx5mMoUSw5M73nfUqjZjFg7ichCPoxt4DybdNBq/UjY3hkhiF+FuzL
|
||||||
|
5+gUewaBDlsCgYEAnfVqxf/UDePjVGTnsJCDyCT6EZujUDF735KGxvqblUSFAnkE
|
||||||
|
zXoL1UsUu8HcqCYJ7gMNjOGOR1ClEOUm5GG3bDM5/Umpyh1IvEORZ72J8B2qPqeF
|
||||||
|
PyE9na8YiwHZSR1NVpK6CXeu1jrmfZ1tKHsMwK80zAfeYX4u6aeYl6DuFukCgYAO
|
||||||
|
IehrIL6Vvau+11/I/FGtMjgXXLTfowDqsDgoVl94p2D+NyioEMhKsVpbViI/Bqza
|
||||||
|
xZ9BG2CipsNsTwmEIn0n4E1ASebaQzlGgw+c1hLS/fYn8gvXRzcFrKKcxRxJcuFS
|
||||||
|
JBijj33QjpA5mhP7BCtwOTsH4qrpgu676S62gaME2QKBgQCanh8rBB2j7bCVdEoL
|
||||||
|
BXCs6OBJllbEPDZtgdECAcMYpaskwWrsHT5638uxLAVQsH42+bCzKfYLwcQqZK8r
|
||||||
|
FIJESgR+Ud6/M5J3s6IGxWGSJ49VYWQDOYd9SiWtGxX1JykzGx55J7P4yba3rNO/
|
||||||
|
ZnMxIBIcFEcgkUWSDzUjw6Gnvg==
|
||||||
|
-----END PRIVATE KEY-----
|
24
tsp.test.pem
Normal file
24
tsp.test.pem
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID8zCCAlugAwIBAgIQeylEAT5Jp8jAylxRtHypdzANBgkqhkiG9w0BAQsFADBP
|
||||||
|
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExEjAQBgNVBAsMCXJvb3RA
|
||||||
|
a2FsaTEZMBcGA1UEAwwQbWtjZXJ0IHJvb3RAa2FsaTAeFw0yNTAyMjgxNzE0MjJa
|
||||||
|
Fw0yNzA1MjgxNjE0MjJaMD0xJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj
|
||||||
|
ZXJ0aWZpY2F0ZTESMBAGA1UECwwJcm9vdEBrYWxpMIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAyjRZwGbBV7DoOFBVxUiLxv/T+tBUC6qHlMJn/O39nVlE
|
||||||
|
sG3l9+mpRPkoybeDBYqXZ7jHuTsxq03b6ujxsLe9GiyPDsekD7ZN/9gC+xJgl3rI
|
||||||
|
CeVdZKX9Fi2V/I9rxO6uxSvpRvzGF75yfeRQxljLs8j6MruS1P3UMME2ADzGUVJv
|
||||||
|
24rP8mIf99dB6+yWKFkVRnex4kkSCjDimQmeSB0XgMKV2D47727lC58Q241C6rjH
|
||||||
|
w20FLLliXsTBKBByKH9v2V38Uh+j3zEWl8ys+ZcbFvAY1eUaHo1I0g8sUrDpTNL4
|
||||||
|
d3lTAWRUAwmxM5W8NA/WkiFRGzcFG1GI+aQT01i7UwIDAQABo10wWzAOBgNVHQ8B
|
||||||
|
Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAU9Dqj2NAN
|
||||||
|
hBbYm6GrIOwnbF6U01owEwYDVR0RBAwwCoIIdHNwLnRlc3QwDQYJKoZIhvcNAQEL
|
||||||
|
BQADggGBAFGlI1vrNiqDJDY00C5QUU9Yi6Vz+IUfbSfwAK4jgV0l1My4/S8jMZn0
|
||||||
|
MTTYHCRatsDfBgwgoTbZnC9tHI5aU+rXXcgERG7bhQNuyTWIFUu+ZGBxXQDIMC/w
|
||||||
|
a++4ZpZsOAdHS5gwTL0qBZxER7bwfjhvsmweA8/2RizRQg4+r/byfIwryyPh9AAT
|
||||||
|
KnH96gymaboWpVJgf+5BEHihI8i/hQzDf2NLo3GTYrsV/dOS/xmkfnr1O7uacFCo
|
||||||
|
k5/Or1LIkogKVGSnjtCQci5fGJkfRiVrlbDYVppjHPBsqArdugKAUBblcAV/2Qq4
|
||||||
|
kjwupnrkCqNolN7lZ6Pj6Nu1aOkZCuLcByfoE6QSwS4uecFN6rjSFjnZHNYvlorV
|
||||||
|
6qeRRGjfYD9QrsergpnZ0Iln/U4+ixEkQHcqLcP3xXtH8IOane1eJgy3kJXMCTYT
|
||||||
|
2XfnnAb4FoenEZ3sGItrDmxsjR0NO+Q76I1tGKvaQ/8QBhMzcZ62fkaqOJwAW8EJ
|
||||||
|
SDKxtQ2McA==
|
||||||
|
-----END CERTIFICATE-----
|
Loading…
Add table
Reference in a new issue