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