├── .gitignore
├── .dockerignore
├── simulation
├── .gitignore
├── .prettierignore
├── .eslintrc.json
├── dist
│ ├── types.js
│ ├── getDest.js
│ ├── types.js.map
│ ├── getDest.js.map
│ ├── routePlanner.js
│ ├── getDestination.js
│ ├── routePlanner.js.map
│ ├── getDestination.js.map
│ ├── index.js
│ ├── index.js.map
│ ├── dbInit.js.map
│ ├── dbInit.js
│ ├── dispatcher.js.map
│ ├── dispatcher.js
│ ├── global.js
│ ├── global.js.map
│ ├── data.js.map
│ ├── Customer.js.map
│ ├── data.js
│ ├── methods.test.js
│ ├── Customer.js
│ ├── Driver.js.map
│ ├── methods.js
│ ├── methods.test.js.map
│ ├── methods.js.map
│ ├── paths.js
│ ├── paths.js.map
│ └── Driver.js
├── .DS_Store
├── .prettierrc.json
├── src
│ ├── .DS_Store
│ ├── types.ts
│ ├── getDestination.ts
│ ├── routePlanner.ts
│ ├── dbInit.ts
│ ├── index.ts
│ ├── dispatcher.ts
│ ├── global.ts
│ ├── data.ts
│ ├── methods.test.ts
│ ├── Customer.ts
│ ├── methods.ts
│ └── Driver.ts
├── babel.config.json
├── tsconfig.eslint.json
├── Dockerfile
├── tsconfig.json
└── package.json
├── .gitattributes
├── frontend
├── .prettierignore
├── .prettierrc.json
├── .eslintrc.json
├── build
│ ├── favicon.ico
│ ├── robots.txt
│ ├── static
│ │ ├── media
│ │ │ ├── bio.eca26d5962009ce3d30f.jpg
│ │ │ ├── diagram-6.b47a66677a3499543226.png
│ │ │ ├── fa-brands-400.150de8eaa454d669c405.ttf
│ │ │ ├── fa-regular-400.d87474231f4192884802.ttf
│ │ │ ├── fa-solid-900.4a2cd718d7031b732e76.ttf
│ │ │ ├── fa-solid-900.bb975c966c37455a1bc3.woff2
│ │ │ ├── fa-brands-400.e033a13ee751afc1860c.woff2
│ │ │ ├── fa-regular-400.3223dc79c1adee56370b.woff2
│ │ │ ├── fa-v4compatibility.0e3a648be390bd8cb094.ttf
│ │ │ └── fa-v4compatibility.68577e40f3e70067b5da.woff2
│ │ └── js
│ │ │ └── main.84f9deb3.js.LICENSE.txt
│ ├── asset-manifest.json
│ └── index.html
├── public
│ ├── favicon.ico
│ ├── robots.txt
│ └── index.html
├── assets
│ ├── images
│ │ ├── bio.jpg
│ │ └── diagram-6.png
│ ├── webfonts
│ │ ├── fa-brands-400.ttf
│ │ ├── fa-solid-900.ttf
│ │ ├── fa-brands-400.woff2
│ │ ├── fa-regular-400.ttf
│ │ ├── fa-solid-900.woff2
│ │ ├── fa-regular-400.woff2
│ │ ├── fa-v4compatibility.ttf
│ │ └── fa-v4compatibility.woff2
│ └── css
│ │ ├── solid.min.css
│ │ ├── regular.min.css
│ │ ├── solid.css
│ │ ├── regular.css
│ │ ├── v5-font-face.min.css
│ │ ├── v5-font-face.css
│ │ ├── v4-font-face.min.css
│ │ └── v4-font-face.css
├── tsconfig.json
├── src
│ ├── Link.tsx
│ ├── MonitorView.tsx
│ ├── error-page.jsx
│ ├── api.tsx
│ ├── index.tsx
│ ├── disableReactDevTools.js
│ ├── Description.tsx
│ ├── Bio.tsx
│ ├── ListItem.tsx
│ ├── App.tsx
│ ├── movement.tsx
│ ├── index.css
│ ├── Mobile.tsx
│ ├── Nav.tsx
│ ├── DocsView.tsx
│ ├── movement.test.ts
│ ├── Car.tsx
│ ├── Tour.tsx
│ └── GeoMap.tsx
├── config-overrides.js
├── .gitignore
├── tailwind.config.js
└── package.json
├── shared
├── package.json
├── utils.js
├── methods.js
├── config.js
├── firstNames.js
└── lastNames.js
├── .DS_Store
├── go.mod
├── deploy.sh
├── monitor
├── postgres_exporter-queries.yml
├── prometheus-local.yml
├── prometheus-prod.yml
├── loki-local-config.yaml
├── promtail-config.yaml
├── docker-compose-macos.yml
└── docker-compose-linux.yml
├── server
├── Dockerfile
└── main.go
├── go.sum
├── db
├── db.go
└── queries
├── prod_deploy.sh
├── docker-compose.yml
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.env
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
--------------------------------------------------------------------------------
/simulation/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.css linguist-vendored
--------------------------------------------------------------------------------
/frontend/.prettierignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | **/*.html
3 |
--------------------------------------------------------------------------------
/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/simulation/.prettierignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | src/*.json
--------------------------------------------------------------------------------
/frontend/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/simulation/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/.DS_Store
--------------------------------------------------------------------------------
/simulation/dist/types.js:
--------------------------------------------------------------------------------
1 | export {};
2 | //# sourceMappingURL=types.js.map
--------------------------------------------------------------------------------
/simulation/dist/getDest.js:
--------------------------------------------------------------------------------
1 | export {};
2 | //# sourceMappingURL=getDest.js.map
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "react-app/jest", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/simulation/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/simulation/.DS_Store
--------------------------------------------------------------------------------
/simulation/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "operator-linebreak": "before"
4 | }
5 |
--------------------------------------------------------------------------------
/simulation/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/simulation/src/.DS_Store
--------------------------------------------------------------------------------
/frontend/build/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/favicon.ico
--------------------------------------------------------------------------------
/frontend/build/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/assets/images/bio.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/images/bio.jpg
--------------------------------------------------------------------------------
/frontend/assets/images/diagram-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/images/diagram-6.png
--------------------------------------------------------------------------------
/simulation/dist/types.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/simulation/dist/getDest.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"getDest.js","sourceRoot":"","sources":["../src/getDest.ts"],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-v4compatibility.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-v4compatibility.ttf
--------------------------------------------------------------------------------
/frontend/assets/webfonts/fa-v4compatibility.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/assets/webfonts/fa-v4compatibility.woff2
--------------------------------------------------------------------------------
/frontend/build/static/media/bio.eca26d5962009ce3d30f.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/bio.eca26d5962009ce3d30f.jpg
--------------------------------------------------------------------------------
/simulation/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", { "targets": { "node": "current" } }],
4 | "@babel/preset-typescript"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/build/static/media/diagram-6.b47a66677a3499543226.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/diagram-6.b47a66677a3499543226.png
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-brands-400.150de8eaa454d669c405.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-brands-400.150de8eaa454d669c405.ttf
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-regular-400.d87474231f4192884802.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-regular-400.d87474231f4192884802.ttf
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-solid-900.4a2cd718d7031b732e76.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-solid-900.4a2cd718d7031b732e76.ttf
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-solid-900.bb975c966c37455a1bc3.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-solid-900.bb975c966c37455a1bc3.woff2
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-brands-400.e033a13ee751afc1860c.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-brands-400.e033a13ee751afc1860c.woff2
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-regular-400.3223dc79c1adee56370b.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-regular-400.3223dc79c1adee56370b.woff2
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "jsx": "react-jsx",
5 | "target": "ESNext",
6 | "moduleResolution": "node"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/simulation/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true
5 | },
6 | "include": ["./src", "./test", ".eslintrc.js"]
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-v4compatibility.0e3a648be390bd8cb094.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-v4compatibility.0e3a648be390bd8cb094.ttf
--------------------------------------------------------------------------------
/frontend/build/static/media/fa-v4compatibility.68577e40f3e70067b5da.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jurajmajerik/rides/HEAD/frontend/build/static/media/fa-v4compatibility.68577e40f3e70067b5da.woff2
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module app
2 |
3 | go 1.18
4 |
5 | require github.com/lib/pq v1.10.7
6 |
7 | require (
8 | github.com/gorilla/mux v1.8.0 // indirect
9 | github.com/joho/godotenv v1.5.1 // indirect
10 | )
11 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd frontend
3 | npm run build
4 | cd ..
5 | git add .
6 | git commit -m "build"
7 | git push
8 | sshcmd="ssh -t juraj@rides.jurajmajerik.com"
9 | $sshcmd screen -S "deployment" /home/juraj/rides/prod_deploy.sh
10 |
--------------------------------------------------------------------------------
/simulation/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:19-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY shared ./shared
6 | COPY simulation ./simulation
7 | RUN cd simulation && npm install && npm run build
8 |
9 | CMD cd simulation && cd dist && node index.js
10 |
--------------------------------------------------------------------------------
/simulation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "NodeNext",
4 | "moduleResolution": "NodeNext",
5 | "target": "ES2020",
6 | "sourceMap": true,
7 | "outDir": "./dist"
8 | },
9 | "include": ["src/**/*"]
10 | }
11 |
--------------------------------------------------------------------------------
/simulation/src/types.ts:
--------------------------------------------------------------------------------
1 | export type Obstacle = [number, number, number, number, string?];
2 | export type Obstacles = Obstacle[];
3 |
4 | export type Graph = (0 | 1)[][];
5 | export type Coord = number;
6 | export type CoordPair = [number, number];
7 | export type Path = CoordPair[];
8 |
--------------------------------------------------------------------------------
/frontend/src/Link.tsx:
--------------------------------------------------------------------------------
1 | const Link = ({ url, text }) => (
2 |
8 | {text}
9 |
10 | );
11 | export default Link;
12 |
--------------------------------------------------------------------------------
/monitor/postgres_exporter-queries.yml:
--------------------------------------------------------------------------------
1 | pg_stat_activity:
2 | query: "SELECT COUNT(*) AS active_connections, now() as timestamp FROM pg_stat_activity WHERE state = 'active'"
3 | metrics:
4 | - active_connections:
5 | usage: "COUNTER"
6 | description: "Number of active connections"
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.18-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY go.mod ./
6 | COPY go.sum ./
7 |
8 | COPY server ./server
9 | COPY db ./db
10 | COPY frontend ./frontend
11 |
12 | RUN cd server && go build -o main
13 |
14 | EXPOSE 8080
15 |
16 | CMD cd server && ./main
17 |
--------------------------------------------------------------------------------
/shared/utils.js:
--------------------------------------------------------------------------------
1 | export const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min) + min);
2 |
3 | export const decide = probability => getRandomInt(1, 100) < probability;
4 |
5 | export const wait = (t) => new Promise((res) => {
6 | setTimeout(() => {
7 | res();
8 | }, t);
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/config-overrides.js:
--------------------------------------------------------------------------------
1 | const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
2 |
3 | module.exports = function override(config, env) {
4 | config.resolve.plugins = config.resolve.plugins.filter(
5 | (plugin) => !(plugin instanceof ModuleScopePlugin)
6 | );
7 |
8 | return config;
9 | };
10 |
--------------------------------------------------------------------------------
/frontend/src/MonitorView.tsx:
--------------------------------------------------------------------------------
1 | const MonitorView = () => (
2 |
3 |
10 |
11 | );
12 | export default MonitorView;
13 |
--------------------------------------------------------------------------------
/frontend/src/error-page.jsx:
--------------------------------------------------------------------------------
1 | import { useRouteError } from 'react-router-dom';
2 |
3 | export default function ErrorPage() {
4 | const error = useRouteError();
5 | console.error(error);
6 |
7 | return (
8 |
9 |
Sorry, an unexpected error has occurred.
10 |
11 | {error.statusText || error.message}
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/api.tsx:
--------------------------------------------------------------------------------
1 | const baseUrl: string =
2 | process.env.REACT_APP_ENV === 'dev'
3 | ? 'http://localhost:8080'
4 | : 'https://rides.jurajmajerik.com';
5 |
6 | export const api: { [key: string]: any } = {};
7 |
8 | api.get = async (endpoint: string): Promise => {
9 | const res = await fetch(`${baseUrl}/api${endpoint}`);
10 | if (res.json) return (await res.json()) || [];
11 | return res;
12 | };
13 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | # /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/shared/methods.js:
--------------------------------------------------------------------------------
1 | import obstacles from './obstacles.js';
2 |
3 | export const getObstaclesMap = () => {
4 | const obstaclesMap = new Map();
5 | obstacles.forEach(([xStart, xEnd, yStart, yEnd, color]) => {
6 | let x = xStart;
7 | while (x <= xEnd) {
8 | let y = yStart;
9 | while (y <= yEnd) {
10 | obstaclesMap.set(`${x}:${y}`, color || '#e1e3eb');
11 | y += 1;
12 | }
13 | x += 1;
14 | }
15 | });
16 | return obstaclesMap;
17 | };
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import { BrowserRouter as Router } from 'react-router-dom';
6 | import disableReactDevTools from './disableReactDevTools.js';
7 |
8 | disableReactDevTools();
9 |
10 | ReactDOM.createRoot(document.getElementById('root')).render(
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
2 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
3 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
4 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
5 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
6 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
7 |
--------------------------------------------------------------------------------
/monitor/prometheus-local.yml:
--------------------------------------------------------------------------------
1 | global:
2 | scrape_interval: 10s
3 |
4 | scrape_configs:
5 | - job_name: 'prometheus'
6 | metrics_path: /metrics
7 | static_configs:
8 | - targets: ['localhost:9090']
9 |
10 | - job_name: 'node_exporter'
11 | static_configs:
12 | - targets: ['node_exporter:9100']
13 |
14 | - job_name: 'cadvisor'
15 | static_configs:
16 | - targets: ['cadvisor:8080']
17 |
18 | - job_name: 'postgres_exporter'
19 | static_configs:
20 | - targets: ['postgres_exporter:9187']
--------------------------------------------------------------------------------
/monitor/prometheus-prod.yml:
--------------------------------------------------------------------------------
1 | global:
2 | scrape_interval: 4s
3 |
4 | scrape_configs:
5 | - job_name: 'prometheus'
6 | metrics_path: /prometheus/metrics
7 | static_configs:
8 | - targets: ['localhost:9090']
9 |
10 | - job_name: 'node_exporter'
11 | static_configs:
12 | - targets: ['node_exporter:9100']
13 |
14 | - job_name: 'cadvisor'
15 | static_configs:
16 | - targets: ['cadvisor:8080']
17 |
18 | - job_name: 'postgres_exporter'
19 | static_configs:
20 | - targets: ['postgres_exporter:9187']
--------------------------------------------------------------------------------
/shared/config.js:
--------------------------------------------------------------------------------
1 | const gridSize = 1000;
2 | const gridCount = 100;
3 | const squareSize = gridSize / gridCount;
4 | const fetchInterval = 1500;
5 | const refreshInterval = 16;
6 | const turnDuration = refreshInterval * 8;
7 | const animationOverhead = 200;
8 | const maxActiveCustomers = 15;
9 | const maxActiveDrivers = 12;
10 |
11 | const config = {
12 | maxActiveCustomers,
13 | maxActiveDrivers,
14 | gridSize,
15 | gridCount,
16 | fetchInterval,
17 | refreshInterval,
18 |
19 | squareSize,
20 | turnDuration,
21 | animationOverhead,
22 | };
23 |
24 | export default config;
25 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
4 | theme: {
5 | extend: {
6 | height: {
7 | transparent: 'transparent',
8 | 10: '10px',
9 | 30: '30px',
10 | 40: '40px',
11 | 50: '50px',
12 | 125: '125px',
13 | 135: '135px',
14 | 140: '140px',
15 | 150: '150px',
16 | 160: '160px',
17 | },
18 | colors: {
19 | transparent: 'transparent',
20 | },
21 | },
22 | },
23 | plugins: [],
24 | };
25 |
--------------------------------------------------------------------------------
/frontend/assets/css/solid.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | :host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
--------------------------------------------------------------------------------
/frontend/assets/css/regular.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | :host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}
--------------------------------------------------------------------------------
/frontend/assets/css/solid.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | :root, :host {
7 | --fa-style-family-classic: 'Font Awesome 6 Free';
8 | --fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
9 |
10 | @font-face {
11 | font-family: 'Font Awesome 6 Free';
12 | font-style: normal;
13 | font-weight: 900;
14 | font-display: block;
15 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
16 |
17 | .fas,
18 | .fa-solid {
19 | font-weight: 900; }
20 |
--------------------------------------------------------------------------------
/frontend/assets/css/regular.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | :root, :host {
7 | --fa-style-family-classic: 'Font Awesome 6 Free';
8 | --fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
9 |
10 | @font-face {
11 | font-family: 'Font Awesome 6 Free';
12 | font-style: normal;
13 | font-weight: 400;
14 | font-display: block;
15 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
16 |
17 | .far,
18 | .fa-regular {
19 | font-weight: 400; }
20 |
--------------------------------------------------------------------------------
/simulation/dist/routePlanner.js:
--------------------------------------------------------------------------------
1 | import { wait } from '../../shared/utils.js';
2 | import { getShortestPath } from './methods.js';
3 | const queue = [];
4 | process.on('message', ({ driverId, startingPosition, destination }) => {
5 | queue.push({ driverId, startingPosition, destination });
6 | });
7 | const main = async () => {
8 | while (true) {
9 | if (queue.length) {
10 | const { driverId, startingPosition, destination } = queue.shift();
11 | let path = getShortestPath(startingPosition, destination);
12 | process.send({ driverId, path });
13 | }
14 | if (queue.length)
15 | continue;
16 | else
17 | await wait(200);
18 | }
19 | };
20 | main();
21 | //# sourceMappingURL=routePlanner.js.map
--------------------------------------------------------------------------------
/simulation/dist/getDestination.js:
--------------------------------------------------------------------------------
1 | import { wait } from '../../shared/utils.js';
2 | import { generateDestination } from './methods.js';
3 | const queue = [];
4 | process.on('message', ({ customerId, location }) => {
5 | queue.push({ customerId, location });
6 | });
7 | const main = async () => {
8 | while (true) {
9 | if (queue.length) {
10 | const { customerId, location } = queue.shift();
11 | const [x, y] = location;
12 | let [destX, destY] = generateDestination([x, y]);
13 | process.send({ customerId, destination: [destX, destY] });
14 | }
15 | if (queue.length)
16 | continue;
17 | else
18 | await wait(200);
19 | }
20 | };
21 | main();
22 | //# sourceMappingURL=getDestination.js.map
--------------------------------------------------------------------------------
/simulation/dist/routePlanner.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"routePlanner.js","sourceRoot":"","sources":["../src/routePlanner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAS/C,MAAM,KAAK,GAAc,EAAE,CAAC;AAE5B,OAAO,CAAC,EAAE,CACR,SAAS,EACT,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAW,EAAE,EAAE;IACvD,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,CAAC,CAAC;AAC1D,CAAC,CACF,CAAC;AAEF,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;IACtB,OAAO,IAAI,EAAE;QACX,IAAI,KAAK,CAAC,MAAM,EAAE;YAChB,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;YAClE,IAAI,IAAI,GAAG,eAAe,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;YAE1D,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;SAClC;QAED,IAAI,KAAK,CAAC,MAAM;YAAE,SAAS;;YACtB,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;KACtB;AACH,CAAC,CAAC;AACF,IAAI,EAAE,CAAC"}
--------------------------------------------------------------------------------
/frontend/assets/css/v5-font-face.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | @font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}
--------------------------------------------------------------------------------
/simulation/src/getDestination.ts:
--------------------------------------------------------------------------------
1 | import { wait } from '../../shared/utils.js';
2 | import { generateDestination } from './methods.js';
3 | import { CoordPair } from './types.js';
4 |
5 | interface Message {
6 | customerId: string;
7 | location: CoordPair;
8 | }
9 |
10 | const queue: Message[] = [];
11 |
12 | process.on('message', ({ customerId, location }: Message) => {
13 | queue.push({ customerId, location });
14 | });
15 |
16 | const main = async () => {
17 | while (true) {
18 | if (queue.length) {
19 | const { customerId, location } = queue.shift();
20 | const [x, y] = location;
21 |
22 | let [destX, destY] = generateDestination([x, y]);
23 | process.send({ customerId, destination: [destX, destY] });
24 | }
25 |
26 | if (queue.length) continue;
27 | else await wait(200);
28 | }
29 | };
30 | main();
31 |
--------------------------------------------------------------------------------
/monitor/loki-local-config.yaml:
--------------------------------------------------------------------------------
1 | auth_enabled: false
2 |
3 | server:
4 | http_listen_port: 3100
5 | grpc_listen_port: 9096
6 |
7 | common:
8 | path_prefix: /tmp/loki
9 | storage:
10 | filesystem:
11 | chunks_directory: /tmp/loki/chunks
12 | rules_directory: /tmp/loki/rules
13 | replication_factor: 1
14 | ring:
15 | instance_addr: 127.0.0.1
16 | kvstore:
17 | store: inmemory
18 |
19 | query_range:
20 | results_cache:
21 | cache:
22 | embedded_cache:
23 | enabled: true
24 | max_size_mb: 100
25 |
26 | schema_config:
27 | configs:
28 | - from: 2020-10-24
29 | store: boltdb-shipper
30 | object_store: filesystem
31 | schema: v11
32 | index:
33 | prefix: index_
34 | period: 24h
35 |
36 | ruler:
37 | alertmanager_url: http://localhost:9093
38 |
39 | analytics:
40 | reporting_enabled: false
--------------------------------------------------------------------------------
/simulation/dist/getDestination.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"getDestination.js","sourceRoot":"","sources":["../src/getDestination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAQnD,MAAM,KAAK,GAAc,EAAE,CAAC;AAE5B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAW,EAAE,EAAE;IAC1D,KAAK,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;AACvC,CAAC,CAAC,CAAC;AAEH,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;IACtB,OAAO,IAAI,EAAE;QACX,IAAI,KAAK,CAAC,MAAM,EAAE;YAChB,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/C,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC;YAExB,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;SAC3D;QAED,IAAI,KAAK,CAAC,MAAM;YAAE,SAAS;;YACtB,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;KACtB;AACH,CAAC,CAAC;AACF,IAAI,EAAE,CAAC"}
--------------------------------------------------------------------------------
/simulation/dist/index.js:
--------------------------------------------------------------------------------
1 | import g from './global.js';
2 | const main = async () => {
3 | await g.init();
4 | const { db, driverInstances, customerInstances, getDestination, dispatcher, routePlanner, } = g;
5 | await db.query('DELETE FROM drivers;');
6 | await db.query('DELETE FROM customers;');
7 | getDestination.on('message', ({ customerId, destination, }) => {
8 | customerInstances[customerId].handleDestinationResult(destination);
9 | });
10 | dispatcher.on('message', ({ customerId, driverId, location, }) => {
11 | customerInstances[customerId].handleDispatcherResult(driverId);
12 | driverInstances[driverId].handleDispatcherResult(customerId, location);
13 | });
14 | routePlanner.on('message', ({ driverId, path }) => {
15 | driverInstances[driverId].handleRoutePlannerResult(path);
16 | });
17 | };
18 | main();
19 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------
/simulation/src/routePlanner.ts:
--------------------------------------------------------------------------------
1 | import { wait } from '../../shared/utils.js';
2 | import { getShortestPath } from './methods.js';
3 | import { CoordPair } from './types.js';
4 |
5 | interface Message {
6 | driverId: string;
7 | startingPosition: CoordPair;
8 | destination: CoordPair;
9 | }
10 |
11 | const queue: Message[] = [];
12 |
13 | process.on(
14 | 'message',
15 | ({ driverId, startingPosition, destination }: Message) => {
16 | queue.push({ driverId, startingPosition, destination });
17 | }
18 | );
19 |
20 | const main = async () => {
21 | while (true) {
22 | if (queue.length) {
23 | const { driverId, startingPosition, destination } = queue.shift();
24 | let path = getShortestPath(startingPosition, destination);
25 |
26 | process.send({ driverId, path });
27 | }
28 |
29 | if (queue.length) continue;
30 | else await wait(200);
31 | }
32 | };
33 | main();
34 |
--------------------------------------------------------------------------------
/simulation/dist/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,aAAa,CAAC;AAG5B,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;IACtB,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAEf,MAAM,EACJ,EAAE,EACF,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,UAAU,EACV,YAAY,GACb,GAAG,CAAC,CAAC;IAEN,MAAM,EAAE,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACvC,MAAM,EAAE,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAEzC,cAAc,CAAC,EAAE,CACf,SAAS,EACT,CAAC,EACC,UAAU,EACV,WAAW,GAIZ,EAAE,EAAE;QACH,iBAAiB,CAAC,UAAU,CAAC,CAAC,uBAAuB,CAAC,WAAW,CAAC,CAAC;IACrE,CAAC,CACF,CAAC;IAEF,UAAU,CAAC,EAAE,CACX,SAAS,EACT,CAAC,EACC,UAAU,EACV,QAAQ,EACR,QAAQ,GAKT,EAAE,EAAE;QACH,iBAAiB,CAAC,UAAU,CAAC,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC;QAC/D,eAAe,CAAC,QAAQ,CAAC,CAAC,sBAAsB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACzE,CAAC,CACF,CAAC;IAEF,YAAY,CAAC,EAAE,CACb,SAAS,EACT,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAoC,EAAE,EAAE;QACvD,eAAe,CAAC,QAAQ,CAAC,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC,CACF,CAAC;AACJ,CAAC,CAAC;AACF,IAAI,EAAE,CAAC"}
--------------------------------------------------------------------------------
/frontend/assets/css/v5-font-face.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | @font-face {
7 | font-family: 'Font Awesome 5 Brands';
8 | font-display: block;
9 | font-weight: 400;
10 | src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
11 |
12 | @font-face {
13 | font-family: 'Font Awesome 5 Free';
14 | font-display: block;
15 | font-weight: 900;
16 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
17 |
18 | @font-face {
19 | font-family: 'Font Awesome 5 Free';
20 | font-display: block;
21 | font-weight: 400;
22 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
23 |
--------------------------------------------------------------------------------
/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "os"
5 | "database/sql"
6 | "fmt"
7 | "log"
8 | "strconv"
9 |
10 | _ "github.com/lib/pq"
11 | "github.com/joho/godotenv"
12 | )
13 |
14 | // Global DB variable
15 | var Connection *sql.DB
16 |
17 | // initDB creates a new instance of DB
18 | func InitDB() {
19 | err := godotenv.Load("../.env")
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 |
24 | user := os.Getenv("POSTGRES_USER")
25 | password := os.Getenv("POSTGRES_PASSWORD")
26 | host := os.Getenv("POSTGRES_HOST")
27 | dbname := os.Getenv("POSTGRES_DBNAME")
28 | portStr := os.Getenv("POSTGRES_PORT")
29 | port, err := strconv.Atoi(portStr)
30 | if err != nil {
31 | log.Fatal(err)
32 | }
33 |
34 | connStr := fmt.Sprintf(
35 | "user=%s password=%s host=%s port=%d dbname=%s sslmode=disable",
36 | user, password, host, port, dbname,
37 | )
38 |
39 | var connErr error
40 | Connection, connErr = sql.Open("postgres", connStr)
41 | if connErr != nil {
42 | log.Fatal(connErr)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/prod_deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | SECONDS=0
3 |
4 | cd $HOME/rides
5 |
6 | msg () {
7 | echo -e "\n$1\n--------------------\n"
8 | }
9 |
10 | msg "Pulling from Github"
11 | git pull
12 |
13 | msg "App | Building images"
14 | sudo docker compose build
15 |
16 | msg "App | Stopping containers"
17 | sudo docker compose down --remove-orphans
18 |
19 | msg "App | Starting containers"
20 | sudo docker compose up -d
21 |
22 | # Prometheus + Grafana
23 | cd $HOME/rides/monitor
24 |
25 | msg "Monitor | Building images"
26 | sudo docker compose -f ./docker-compose-linux.yml build
27 |
28 | msg "Monitor | Stopping containers"
29 | sudo docker compose -f ./docker-compose-linux.yml down --remove-orphans
30 |
31 | msg "Monitor | Starting containers"
32 | sudo docker compose -f ./docker-compose-linux.yml up -d
33 |
34 | msg "Monitor | Pruning stale Docker images"
35 | sudo docker image prune -f
36 |
37 | cd $HOME/rides
38 |
39 | duration=$SECONDS
40 |
41 | echo
42 | msg "Deploy finished in $(($duration % 60)) seconds."
43 | msg "Press Enter to exit"
44 | read
--------------------------------------------------------------------------------
/db/queries:
--------------------------------------------------------------------------------
1 | CREATE USER ridesuser WITH PASSWORD '';
2 | CREATE DATABASE ridesdb;
3 |
4 | -- /c ridesdb
5 |
6 | GRANT ALL PRIVILEGES ON DATABASE ridesdb TO ridesuser;
7 |
8 | CREATE TYPE status_enum AS ENUM ('idle', 'pickup', 'enroute');
9 | CREATE TABLE customers (
10 | id BIGSERIAL PRIMARY KEY,
11 | customer_id uuid UNIQUE,
12 | name VARCHAR(255) NOT NULL,
13 | active BOOLEAN,
14 | location VARCHAR(5) NOT NULL,
15 | destination VARCHAR(5) NOT NULL,
16 | driver_id uuid UNIQUE
17 | );
18 |
19 | CREATE TABLE drivers (
20 | id BIGSERIAL PRIMARY KEY,
21 | driver_id uuid UNIQUE,
22 | name VARCHAR(255) NOT NULL,
23 | status status_enum NOT NULL,
24 | location VARCHAR(5) NOT NULL,
25 | path TEXT,
26 | path_index INTEGER,
27 | customer_id uuid UNIQUE,
28 | customer_name VARCHAR(255)
29 | );
30 |
31 | GRANT SELECT, INSERT, UPDATE, DELETE ON customers TO ridesuser;
32 | GRANT SELECT, INSERT, UPDATE, DELETE ON drivers TO ridesuser;
33 | GRANT USAGE, SELECT ON SEQUENCE drivers_id_seq TO ridesuser;
34 | GRANT USAGE, SELECT ON SEQUENCE customers_id_seq TO ridesuser;
35 |
--------------------------------------------------------------------------------
/simulation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simulation",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "jest --watchAll",
9 | "build": "tsc"
10 | },
11 | "jest": {
12 | "transform": {
13 | "^.+\\.[t|j]sx?$": "babel-jest"
14 | },
15 | "watchPathIgnorePatterns": [
16 | "node_modules",
17 | "dist"
18 | ]
19 | },
20 | "author": "",
21 | "license": "ISC",
22 | "dependencies": {
23 | "dotenv": "^16.0.3",
24 | "md5": "^2.3.0",
25 | "pg": "^8.9.0"
26 | },
27 | "devDependencies": {
28 | "@babel/plugin-transform-typescript": "^7.21.0",
29 | "@babel/preset-env": "^7.20.2",
30 | "@babel/preset-typescript": "^7.21.0",
31 | "@types/jest": "^29.4.0",
32 | "@types/node": "^18.14.5",
33 | "@typescript-eslint/eslint-plugin": "^5.54.0",
34 | "@typescript-eslint/parser": "^5.54.0",
35 | "babel-jest": "^29.5.0",
36 | "eslint": "^8.35.0",
37 | "jest": "^29.5.0",
38 | "prettier": "^2.8.4",
39 | "typescript": "^4.9.5"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/monitor/promtail-config.yaml:
--------------------------------------------------------------------------------
1 | server:
2 | http_listen_port: 9080
3 | grpc_listen_port: 0
4 |
5 | positions:
6 | filename: /tmp/positions.yaml
7 |
8 | clients:
9 | - url: http://loki:3100/loki/api/v1/push
10 |
11 | scrape_configs:
12 | - job_name: local
13 | static_configs:
14 | - targets:
15 | - localhost
16 | labels:
17 | job: varlogs
18 | __path__: /var/log/*log
19 |
20 | - job_name: docker
21 | docker_sd_configs:
22 | - host: unix:///run/docker.sock
23 | refresh_interval: 5s
24 | pipeline_stages:
25 | - match:
26 | selector: '{job="docker"}'
27 | stages:
28 | - regex:
29 | expression: '.*level=(?P[a-zA-Z]+).*ts=(?P[T\d-:.Z]*).*msg=(?P[a-zA-Z]+).*err=(?P[a-zA-Z]+)'
30 | - labels:
31 | level:
32 | msg:
33 | err:
34 | - timestamp:
35 | format: RFC3339Nano
36 | source: timestamp
37 | relabel_configs:
38 | - source_labels: ['__meta_docker_container_name']
39 | regex: '/(.*)'
40 | target_label: 'container'
41 | - source_labels: ['level']
42 | target_label: 'level'
--------------------------------------------------------------------------------
/frontend/src/disableReactDevTools.js:
--------------------------------------------------------------------------------
1 | export function isFunction(obj) {
2 | return typeof obj == 'function';
3 | }
4 |
5 | export function isObject(obj) {
6 | var type = typeof obj;
7 | return type === 'function' || (type === 'object' && !!obj);
8 | }
9 |
10 | export function hasWindowObject() {
11 | return typeof window !== 'undefined' && window.document;
12 | }
13 |
14 | export default function disableReactDevTools() {
15 | if (hasWindowObject()) {
16 | // Ensure the React Developer Tools global hook exists
17 | if (!isObject(window.__REACT_DEVTOOLS_GLOBAL_HOOK__)) {
18 | return;
19 | }
20 |
21 | // Replace all global hook properties with a no-op function or a null value
22 | for (const prop in window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
23 | if (prop === 'renderers') {
24 | // prevents console error when dev tools try to iterate of renderers
25 | window.__REACT_DEVTOOLS_GLOBAL_HOOK__[prop] = new Map();
26 | continue;
27 | }
28 | window.__REACT_DEVTOOLS_GLOBAL_HOOK__[prop] = isFunction(
29 | window.__REACT_DEVTOOLS_GLOBAL_HOOK__[prop]
30 | )
31 | ? Function.prototype
32 | : null;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/simulation/dist/dbInit.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dbInit.js","sourceRoot":"","sources":["../src/dbInit.ts"],"names":[],"mappings":"AAAA,YAAY;AACZ,OAAO,EAAc,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;AAEtC,MAAM,EACJ,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,eAAe,GAChB,GAAG,OAAO,CAAC,GAAG,CAAC;AAEhB,IAAI,OAAO,GAAG,CAAC,CAAC;AAChB,MAAM,UAAU,GAAG,IAAI,CAAC;AAExB,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,MAAM;IAClC,OAAO,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QACtB,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC;YACpB,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,aAAa;YACnB,QAAQ,EAAE,iBAAiB;YAC3B,QAAQ,EAAE,eAAe;SAC1B,CAAC,CAAC;QAEH,OAAO,OAAO,GAAG,CAAC,EAAE;YAClB,IAAI;gBACF,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;gBACnB,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;gBAC5B,OAAO,CAAC,EAAE,CAAC,CAAC;gBACZ,OAAO;aACR;YAAC,OAAO,GAAG,EAAE;gBACZ,OAAO,CAAC,KAAK,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;aACjE;YACD,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,yCAAyC,OAAO,EAAE,CAAC,CAAC;YAChE,MAAM,IAAI,CAAC,UAAU,CAAC,CAAC;SACxB;QAED,MAAM,CAAC,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC"}
--------------------------------------------------------------------------------
/simulation/src/dbInit.ts:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import pg, { Client } from 'pg';
3 | import { wait } from '../../shared/utils.js';
4 |
5 | import * as dotenv from 'dotenv';
6 | dotenv.config({ path: '../../.env' });
7 |
8 | const {
9 | POSTGRES_USER,
10 | POSTGRES_PASSWORD,
11 | POSTGRES_HOST,
12 | POSTGRES_PORT,
13 | POSTGRES_DBNAME,
14 | } = process.env;
15 |
16 | let retries = 3;
17 | const retryDelay = 2000;
18 |
19 | export default async function dbInit(): Promise {
20 | return new Promise(async (resolve, reject) => {
21 | const { Client } = pg;
22 | const db = new Client({
23 | host: POSTGRES_HOST,
24 | port: POSTGRES_PORT,
25 | user: POSTGRES_USER,
26 | password: POSTGRES_PASSWORD,
27 | database: POSTGRES_DBNAME,
28 | });
29 |
30 | while (retries > 0) {
31 | try {
32 | await db.connect();
33 | console.log('DB connected');
34 | resolve(db);
35 | return;
36 | } catch (err) {
37 | console.error(`Connection error: ${JSON.stringify(err.stack)}`);
38 | }
39 | retries--;
40 | console.log(`Connection failed, retries remaining: ${retries}`);
41 | await wait(retryDelay);
42 | }
43 |
44 | reject(new Error('Failed to connect to database'));
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | server:
3 | image: server
4 | build:
5 | context: .
6 | dockerfile: ./server/Dockerfile
7 | ports:
8 | - ${SERVER_PORT}:${SERVER_PORT}
9 | volumes:
10 | - /etc/letsencrypt:/etc/letsencrypt
11 | - ./.env:/app/.env
12 | environment:
13 | SERVER_ENV: ${SERVER_ENV}
14 | restart: unless-stopped
15 | deploy:
16 | resources:
17 | limits:
18 | cpus: '0.2'
19 | memory: 100M
20 | simulation:
21 | build:
22 | context: .
23 | dockerfile: ./simulation/Dockerfile
24 | volumes:
25 | - ./.env:/app/.env
26 | restart: unless-stopped
27 | deploy:
28 | resources:
29 | limits:
30 | cpus: '0.2'
31 | memory: 200M
32 | db:
33 | image: postgres:15.1-alpine
34 | ports:
35 | - 5432:5432
36 | volumes:
37 | - rides-db:/var/lib/postgresql/data
38 | environment:
39 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
40 | command:
41 | - "-c"
42 | - "shared_buffers=64MB"
43 | - "-c"
44 | - "effective_cache-size=64MB"
45 | restart: unless-stopped
46 | deploy:
47 | resources:
48 | limits:
49 | cpus: '0.2'
50 | memory: 300M
51 |
52 | volumes:
53 | rides-db:
54 | external: true
--------------------------------------------------------------------------------
/simulation/dist/dbInit.js:
--------------------------------------------------------------------------------
1 | //@ts-ignore
2 | import pg from 'pg';
3 | import { wait } from '../../shared/utils.js';
4 | import * as dotenv from 'dotenv';
5 | dotenv.config({ path: '../../.env' });
6 | const { POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DBNAME, } = process.env;
7 | let retries = 3;
8 | const retryDelay = 2000;
9 | export default async function dbInit() {
10 | return new Promise(async (resolve, reject) => {
11 | const { Client } = pg;
12 | const db = new Client({
13 | host: POSTGRES_HOST,
14 | port: POSTGRES_PORT,
15 | user: POSTGRES_USER,
16 | password: POSTGRES_PASSWORD,
17 | database: POSTGRES_DBNAME,
18 | });
19 | while (retries > 0) {
20 | try {
21 | await db.connect();
22 | console.log('DB connected');
23 | resolve(db);
24 | return;
25 | }
26 | catch (err) {
27 | console.error(`Connection error: ${JSON.stringify(err.stack)}`);
28 | }
29 | retries--;
30 | console.log(`Connection failed, retries remaining: ${retries}`);
31 | await wait(retryDelay);
32 | }
33 | reject(new Error('Failed to connect to database'));
34 | });
35 | }
36 | //# sourceMappingURL=dbInit.js.map
--------------------------------------------------------------------------------
/simulation/dist/dispatcher.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"dispatcher.js","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAmBvD,MAAM,aAAa,GAIb,EAAE,CAAC;AACT,MAAM,OAAO,GAA8D,EAAE,CAAC;AAE9E,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAW,EAAE,EAAE;IAChD,IAAI,IAAI,KAAK,UAAU,EAAE;QACvB,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAoB,CAAC;QAC5D,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;KACpD;SAAM,IAAI,IAAI,KAAK,QAAQ,EAAE;QAC5B,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAkB,CAAC;QACxD,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;KAC5C;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;IACtB,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IAEjB,OAAO,IAAI,EAAE;QACX,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,aAAa,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,EAAE;YAC1C,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,KAAK,EAAE,CAAC;YAEvD,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;gBAChC,OAAO,CACL,uBAAuB,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC;oBACnD,uBAAuB,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACpD,CAAC;YACJ,CAAC,CAAC,CAAC;YAEH,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;YACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC;YAEnC,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;SAClD;QAED,IAAI,aAAa,CAAC,MAAM;YAAE,SAAS;;YAC9B,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;KACvB;AACH,CAAC,CAAC;AACF,IAAI,EAAE,CAAC"}
--------------------------------------------------------------------------------
/simulation/dist/dispatcher.js:
--------------------------------------------------------------------------------
1 | import { wait } from '../../shared/utils.js';
2 | import { getStraightLineDistance } from './methods.js';
3 | const customerQueue = [];
4 | const drivers = [];
5 | process.on('message', ({ from, data }) => {
6 | if (from === 'customer') {
7 | const { customerId, name, location } = data;
8 | customerQueue.push({ customerId, name, location });
9 | }
10 | else if (from === 'driver') {
11 | const { driverId, name, location } = data;
12 | drivers.push({ driverId, name, location });
13 | }
14 | });
15 | const main = async () => {
16 | await wait(5000);
17 | while (true) {
18 | await wait(500);
19 | if (customerQueue.length && drivers.length) {
20 | const { customerId, location } = customerQueue.shift();
21 | drivers.sort((driverA, driverB) => {
22 | return (getStraightLineDistance(driverB.location, location) -
23 | getStraightLineDistance(driverA.location, location));
24 | });
25 | const matchedDriver = drivers.pop();
26 | const { driverId } = matchedDriver;
27 | process.send({ customerId, driverId, location });
28 | }
29 | if (customerQueue.length)
30 | continue;
31 | else
32 | await wait(1000);
33 | }
34 | };
35 | main();
36 | //# sourceMappingURL=dispatcher.js.map
--------------------------------------------------------------------------------
/simulation/dist/global.js:
--------------------------------------------------------------------------------
1 | import { fork } from 'child_process';
2 | import dbInit from './dbInit.js';
3 | import Driver from './Driver.js';
4 | import Customer from './Customer.js';
5 | import { getRoadNodes } from './methods.js';
6 | import { drivers, customers } from './data.js';
7 | const g = {
8 | db: null,
9 | driverInstances: null,
10 | customerInstances: null,
11 | getDestination: null,
12 | dispatcher: null,
13 | routePlanner: null,
14 | activeCustomers: null,
15 | activeDrivers: null,
16 | roadNodes: null,
17 | init: null,
18 | };
19 | const init = async () => {
20 | g.db = await dbInit();
21 | g.getDestination = fork('getDestination.js');
22 | g.dispatcher = fork('dispatcher.js');
23 | g.routePlanner = fork('routePlanner.js');
24 | g.activeCustomers = new Set();
25 | g.activeDrivers = new Set();
26 | g.roadNodes = getRoadNodes();
27 | g.driverInstances = {};
28 | drivers.forEach(({ driverId }) => {
29 | g.driverInstances[driverId] = new Driver({ driverId });
30 | });
31 | g.customerInstances = {};
32 | customers.forEach(({ customerId, name }) => {
33 | g.customerInstances[customerId] = new Customer({ customerId, name });
34 | });
35 | setInterval(() => {
36 | console.log(g.activeDrivers);
37 | }, 10000);
38 | };
39 | g.init = init;
40 | export default g;
41 | //# sourceMappingURL=global.js.map
--------------------------------------------------------------------------------
/frontend/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.css": "/static/css/main.6e623da4.css",
4 | "main.js": "/static/js/main.84f9deb3.js",
5 | "static/media/fa-solid-900.ttf": "/static/media/fa-solid-900.4a2cd718d7031b732e76.ttf",
6 | "static/media/fa-brands-400.ttf": "/static/media/fa-brands-400.150de8eaa454d669c405.ttf",
7 | "static/media/diagram-6.png": "/static/media/diagram-6.b47a66677a3499543226.png",
8 | "static/media/fa-solid-900.woff2": "/static/media/fa-solid-900.bb975c966c37455a1bc3.woff2",
9 | "static/media/fa-brands-400.woff2": "/static/media/fa-brands-400.e033a13ee751afc1860c.woff2",
10 | "static/media/fa-regular-400.ttf": "/static/media/fa-regular-400.d87474231f4192884802.ttf",
11 | "static/media/bio.jpg": "/static/media/bio.eca26d5962009ce3d30f.jpg",
12 | "static/media/fa-regular-400.woff2": "/static/media/fa-regular-400.3223dc79c1adee56370b.woff2",
13 | "static/media/fa-v4compatibility.ttf": "/static/media/fa-v4compatibility.0e3a648be390bd8cb094.ttf",
14 | "static/media/fa-v4compatibility.woff2": "/static/media/fa-v4compatibility.68577e40f3e70067b5da.woff2",
15 | "index.html": "/index.html",
16 | "main.6e623da4.css.map": "/static/css/main.6e623da4.css.map",
17 | "main.84f9deb3.js.map": "/static/js/main.84f9deb3.js.map"
18 | },
19 | "entrypoints": [
20 | "static/css/main.6e623da4.css",
21 | "static/js/main.84f9deb3.js"
22 | ]
23 | }
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fvilers/disable-react-devtools": "^1.3.0",
7 | "@reactour/tour": "^3.4.0",
8 | "react": "^18.2.0",
9 | "react-dom": "^18.2.0",
10 | "react-markdown": "^8.0.7",
11 | "react-router-dom": "^6.10.0",
12 | "react-scripts": "5.0.1",
13 | "web-vitals": "^2.1.4"
14 | },
15 | "scripts": {
16 | "start": "REACT_APP_ENV=dev react-app-rewired start",
17 | "build": "REACT_APP_ENV=prod react-app-rewired build",
18 | "test": "react-app-rewired test",
19 | "eject": "react-scripts eject"
20 | },
21 | "browserslist": {
22 | "production": [
23 | ">0.2%",
24 | "not dead",
25 | "not op_mini all"
26 | ],
27 | "development": [
28 | "last 1 chrome version",
29 | "last 1 firefox version",
30 | "last 1 safari version"
31 | ]
32 | },
33 | "devDependencies": {
34 | "@heroicons/react": "^2.0.17",
35 | "@testing-library/jest-dom": "^5.16.5",
36 | "@testing-library/react": "^13.4.0",
37 | "@testing-library/user-event": "^13.5.0",
38 | "@types/jest": "^29.4.0",
39 | "@types/node": "^18.14.6",
40 | "@types/react": "^18.0.28",
41 | "@types/react-dom": "^18.0.11",
42 | "eslint-config-prettier": "^8.6.0",
43 | "prettier": "2.8.4",
44 | "react-app-rewired": "^2.2.1",
45 | "tailwindcss": "^3.2.7",
46 | "typescript": "^4.9.5"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/Description.tsx:
--------------------------------------------------------------------------------
1 | import { TourStart } from './Tour';
2 |
3 | const Description = ({ setStep }) => (
4 |
5 |
6 |
7 |
8 |
9 | rides
10 | {' '}
11 | is a full-stack simulation of a ridesharing app.
12 |
13 |
14 | This project is my take on building and visualizing a scalable system.
15 | My motivation was to implement various concepts from the domain of
16 | system design, including{' '}
17 | containerization ,{' '}
18 | multiprocessing and{' '}
19 | observability .
20 |
21 |
22 | This project took approximately 300 hours of work. Read about the
23 | journey of building it on
24 |
30 | my blog
31 |
32 | .
33 |
34 |
35 |
36 | );
37 | export default Description;
38 |
--------------------------------------------------------------------------------
/simulation/src/index.ts:
--------------------------------------------------------------------------------
1 | import g from './global.js';
2 | import { CoordPair, Path } from './types.js';
3 |
4 | const main = async () => {
5 | await g.init();
6 |
7 | const {
8 | db,
9 | driverInstances,
10 | customerInstances,
11 | getDestination,
12 | dispatcher,
13 | routePlanner,
14 | } = g;
15 |
16 | await db.query('DELETE FROM drivers;');
17 | await db.query('DELETE FROM customers;');
18 |
19 | getDestination.on(
20 | 'message',
21 | ({
22 | customerId,
23 | destination,
24 | }: {
25 | customerId: string;
26 | destination: CoordPair;
27 | }) => {
28 | customerInstances[customerId].handleDestinationResult(destination);
29 | }
30 | );
31 |
32 | dispatcher.on(
33 | 'message',
34 | ({
35 | customerId,
36 | driverId,
37 | customerName,
38 | location,
39 | }: {
40 | customerId: string;
41 | driverId: string;
42 | customerName: string;
43 | location: CoordPair;
44 | }) => {
45 | customerInstances[customerId].handleDispatcherResult(driverId);
46 | driverInstances[driverId].handleDispatcherResult(
47 | customerId,
48 | customerName,
49 | location
50 | );
51 | }
52 | );
53 |
54 | routePlanner.on(
55 | 'message',
56 | ({ driverId, path }: { driverId: string; path: Path }) => {
57 | driverInstances[driverId].handleRoutePlannerResult(path);
58 | }
59 | );
60 | };
61 | main();
62 |
--------------------------------------------------------------------------------
/simulation/dist/global.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"global.js","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,eAAe,CAAC;AAEnD,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,QAAQ,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAe/C,MAAM,CAAC,GAAW;IAChB,EAAE,EAAE,IAAI;IACR,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,IAAI;IACvB,cAAc,EAAE,IAAI;IACpB,UAAU,EAAE,IAAI;IAChB,YAAY,EAAE,IAAI;IAClB,eAAe,EAAE,IAAI;IACrB,aAAa,EAAE,IAAI;IACnB,SAAS,EAAE,IAAI;IACf,IAAI,EAAE,IAAI;CACX,CAAC;AAEF,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;IACtB,CAAC,CAAC,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IACtB,CAAC,CAAC,cAAc,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAC7C,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACzC,CAAC,CAAC,eAAe,GAAG,IAAI,GAAG,EAAE,CAAC;IAC9B,CAAC,CAAC,aAAa,GAAG,IAAI,GAAG,EAAE,CAAC;IAC5B,CAAC,CAAC,SAAS,GAAG,YAAY,EAAE,CAAC;IAE7B,CAAC,CAAC,eAAe,GAAG,EAAE,CAAC;IACvB,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC/B,CAAC,CAAC,eAAe,CAAC,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,CAAC,CAAC,iBAAiB,GAAG,EAAE,CAAC;IACzB,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE;QACzC,CAAC,CAAC,iBAAiB,CAAC,UAAU,CAAC,GAAG,IAAI,QAAQ,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,WAAW,CAAC,GAAG,EAAE;QACf,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC/B,CAAC,EAAE,KAAK,CAAC,CAAC;AACZ,CAAC,CAAC;AAEF,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;AAEd,eAAe,CAAC,CAAC"}
--------------------------------------------------------------------------------
/frontend/build/index.html:
--------------------------------------------------------------------------------
1 | Rides | Juraj Majerik You need to enable JavaScript to run this app.
--------------------------------------------------------------------------------
/frontend/assets/css/v4-font-face.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | @font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
--------------------------------------------------------------------------------
/frontend/src/Bio.tsx:
--------------------------------------------------------------------------------
1 | const Bio = () => (
2 |
6 |
7 |
23 |
24 |
25 |
26 |
27 | Juraj Majerik
28 |
29 |
52 |
53 |
54 |
55 | );
56 | export default Bio;
57 |
--------------------------------------------------------------------------------
/simulation/src/dispatcher.ts:
--------------------------------------------------------------------------------
1 | import { wait } from '../../shared/utils.js';
2 | import { CoordPair } from './types.js';
3 | import { getStraightLineDistance } from './methods.js';
4 |
5 | interface CustomerData {
6 | customerId: string;
7 | name: string;
8 | location: CoordPair;
9 | }
10 |
11 | interface DriverData {
12 | driverId: string;
13 | name: string;
14 | location: CoordPair;
15 | }
16 |
17 | interface Message {
18 | from: string;
19 | data: CustomerData | DriverData;
20 | }
21 |
22 | const customerQueue: {
23 | customerId: string;
24 | name: string;
25 | location: CoordPair;
26 | }[] = [];
27 | const drivers: { driverId: string; name: string; location: CoordPair }[] = [];
28 |
29 | process.on('message', ({ from, data }: Message) => {
30 | if (from === 'customer') {
31 | const { customerId, name, location } = data as CustomerData;
32 | customerQueue.push({ customerId, name, location });
33 | } else if (from === 'driver') {
34 | const { driverId, name, location } = data as DriverData;
35 | drivers.push({ driverId, name, location });
36 | }
37 | });
38 |
39 | const main = async () => {
40 | await wait(5000);
41 |
42 | while (true) {
43 | await wait(500);
44 | if (customerQueue.length && drivers.length) {
45 | const { customerId, name, location } = customerQueue.shift();
46 |
47 | drivers.sort((driverA, driverB) => {
48 | return (
49 | getStraightLineDistance(driverB.location, location) -
50 | getStraightLineDistance(driverA.location, location)
51 | );
52 | });
53 |
54 | const matchedDriver = drivers.pop();
55 | const { driverId } = matchedDriver;
56 |
57 | process.send({ customerId, customerName: name, driverId, location });
58 | }
59 |
60 | if (customerQueue.length) continue;
61 | else await wait(1000);
62 | }
63 | };
64 | main();
65 |
--------------------------------------------------------------------------------
/shared/firstNames.js:
--------------------------------------------------------------------------------
1 | const firstNames = [
2 | "Sophia",
3 | "Oliver",
4 | "Olivia",
5 | "Noah",
6 | "Emma",
7 | "Liam",
8 | "Ava",
9 | "Sophie",
10 | "Mia",
11 | "Amelia",
12 | "Isabella",
13 | "Charlotte",
14 | "Emily",
15 | "Harper",
16 | "Lily",
17 | "Grace",
18 | "Lucas",
19 | "Jack",
20 | "William",
21 | "Alexander",
22 | "Benjamin",
23 | "Daniel",
24 | "Mason",
25 | "Samuel",
26 | "Henry",
27 | "Leo",
28 | "Max",
29 | "Ella",
30 | "Eva",
31 | "Hannah",
32 | "Anna",
33 | "Lea",
34 | "Lena",
35 | "Maria",
36 | "Laura",
37 | "Julia",
38 | "Emma",
39 | "Irina",
40 | "Ivana",
41 | "Matej",
42 | "Katarina",
43 | "Mohammad",
44 | "Aya",
45 | "Takeshi",
46 | "Jin",
47 | "Ravi",
48 | "Aliya",
49 | "Ahmed",
50 | "Priya",
51 | "Muhammad",
52 | "Kazuki",
53 | "Fatemeh",
54 | "Hiroshi",
55 | "Siti",
56 | "Saeed",
57 | "Mai",
58 | "Hassan",
59 | "Yuki",
60 | "Lee",
61 | "Binh",
62 | "Rahul",
63 | "Koji",
64 | "Aisha",
65 | "Abdul",
66 | "Xiao",
67 | "Arun",
68 | "Chen",
69 | "Yoshiko",
70 | "Nur",
71 | "Haruki",
72 | "Nori",
73 | "Jung",
74 | "Anushka",
75 | "Hiroko",
76 | "Jia",
77 | "Minh",
78 | "Pranav",
79 | "Amara",
80 | "Ishita",
81 | "Alok",
82 | "Ivan",
83 | "Elena",
84 | "Juan",
85 | "Maria",
86 | "Carlos",
87 | "Luis",
88 | "Jose",
89 | "Ana",
90 | "Pedro",
91 | "Fernando",
92 | "Manuel",
93 | "Diego",
94 | "Gabriel",
95 | "Alejandro",
96 | "Rafael",
97 | "Eduardo",
98 | "Andres",
99 | "Cristian",
100 | "Patricia",
101 | "Gustavo",
102 | "Roberto",
103 | "Rodrigo",
104 | "Lorena",
105 | "Mauricio",
106 | "Felipe",
107 | "Silvia",
108 | "Hernandez",
109 | "Leonardo"
110 | ];
111 | export default firstNames;
--------------------------------------------------------------------------------
/simulation/src/global.ts:
--------------------------------------------------------------------------------
1 | import { fork, ChildProcess } from 'child_process';
2 | import { CoordPair } from './types.js';
3 | import dbInit from './dbInit.js';
4 | import Driver from './Driver.js';
5 | import Customer from './Customer.js';
6 | import { getRoadNodes } from './methods.js';
7 | import { drivers, customers } from './data.js';
8 |
9 | export interface Global {
10 | db: any;
11 | driverInstances: { [driverId: string]: Driver };
12 | customerInstances: { [customerId: string]: Customer };
13 | dispatcher: ChildProcess;
14 | getDestination: ChildProcess;
15 | routePlanner: ChildProcess;
16 | activeCustomers: Set;
17 | activeDrivers: Set;
18 | roadNodes: CoordPair[];
19 | init: () => Promise;
20 | }
21 |
22 | const g: Global = {
23 | db: null,
24 | driverInstances: null,
25 | customerInstances: null,
26 | getDestination: null,
27 | dispatcher: null,
28 | routePlanner: null,
29 | activeCustomers: null,
30 | activeDrivers: null,
31 | roadNodes: null,
32 | init: null,
33 | };
34 |
35 | const init = async () => {
36 | g.db = await dbInit();
37 | g.getDestination = fork('getDestination.js');
38 | g.dispatcher = fork('dispatcher.js');
39 | g.routePlanner = fork('routePlanner.js');
40 | g.activeCustomers = new Set();
41 | g.activeDrivers = new Set();
42 | g.roadNodes = getRoadNodes();
43 |
44 | g.driverInstances = {};
45 | drivers.forEach(({ driverId }) => {
46 | g.driverInstances[driverId] = new Driver({ driverId });
47 | });
48 |
49 | g.customerInstances = {};
50 | customers.forEach(({ customerId }) => {
51 | g.customerInstances[customerId] = new Customer({ customerId });
52 | });
53 |
54 | setInterval(() => {
55 | console.log(`Active drivers: ${g.activeDrivers.size}`);
56 | console.log(`Active customers: ${g.activeCustomers.size}`);
57 | }, 10000);
58 | };
59 |
60 | g.init = init;
61 |
62 | export default g;
63 |
--------------------------------------------------------------------------------
/frontend/assets/css/v4-font-face.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | * Copyright 2023 Fonticons, Inc.
5 | */
6 | @font-face {
7 | font-family: 'FontAwesome';
8 | font-display: block;
9 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
10 |
11 | @font-face {
12 | font-family: 'FontAwesome';
13 | font-display: block;
14 | src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
15 |
16 | @font-face {
17 | font-family: 'FontAwesome';
18 | font-display: block;
19 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
20 | unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
21 |
22 | @font-face {
23 | font-family: 'FontAwesome';
24 | font-display: block;
25 | src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
26 | unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }
27 |
--------------------------------------------------------------------------------
/simulation/dist/data.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"data.js","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IACpE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACnE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,SAAS,EAAE;IAErE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACnE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IACpE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IAEpE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,MAAM,EAAE;IAClE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,UAAU,EAAE;IACtE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,UAAU,EAAE;IAEtE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACnE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,WAAW,EAAE;IACvE,EAAE,QAAQ,EAAE,sCAAsC,EAAE,IAAI,EAAE,SAAS,EAAE;CACtE,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACrE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,SAAS,EAAE;IACvE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,MAAM,EAAE;IACpE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,MAAM,EAAE;IACpE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACrE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IAEtE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACrE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,SAAS,EAAE;IACvE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACrE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,aAAa,EAAE;IAC3E,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,MAAM,EAAE;IAEpE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACrE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,SAAS,EAAE;IACvE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,OAAO,EAAE;IACrE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,SAAS,EAAE;IACvE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,UAAU,EAAE;IAExE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,MAAM,EAAE;IACpE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,SAAS,EAAE;IACvE,EAAE,UAAU,EAAE,sCAAsC,EAAE,IAAI,EAAE,UAAU,EAAE;CACzE,CAAC"}
--------------------------------------------------------------------------------
/frontend/src/ListItem.tsx:
--------------------------------------------------------------------------------
1 | import { SteeringWheelIcon, ListCustomerIcon } from './Icons';
2 |
3 | const ListItem = ({
4 | driverId,
5 | customerId,
6 | driverName,
7 | customerName,
8 | progress,
9 | status,
10 | }) => {
11 | const tagClasses = {
12 | pickup:
13 | 'm-auto mt-3 w-24 rounded text-xs p-0.5 font-semibold bg-zinc-150 text-slate-700 text-center uppercase status-tag',
14 | enroute:
15 | 'm-auto mt-3 w-24 rounded text-xs p-0.5 font-semibold bg-zinc-150 text-slate-700 text-center uppercase status-tag',
16 | };
17 | const statusClasses = {
18 | pickup: 'bg-cyan-500 inline-block mr-2',
19 | enroute: 'bg-emerald-500 inline-block mr-2',
20 | };
21 |
22 | return (
23 |
24 |
25 |
26 |
{customerName.split(' ')[0]}
27 |
28 |
29 |
30 |
31 |
{driverName.split(' ')[0]}
32 |
33 |
34 |
38 |
47 | {status}
48 |
49 |
59 |
60 | );
61 | };
62 | export default ListItem;
63 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Routes, Route, Navigate } from 'react-router';
3 | import { useNavigate } from 'react-router-dom';
4 | import { TourProvider } from '@reactour/tour';
5 | import Nav from './Nav';
6 | import Bio from './Bio';
7 | import { steps } from './Tour';
8 | import '../assets/css/all.min.css';
9 | import GeoMap from './GeoMap';
10 | import MonitorView from './MonitorView';
11 | import DocsView from './DocsView';
12 | import Mobile from './Mobile';
13 | import Description from './Description';
14 |
15 | const App = () => {
16 | const redirect = useNavigate();
17 | const [step, setStep] = useState(0);
18 |
19 | const setCurrentStep = (step) => {
20 | const redirects = {
21 | 0: '/map',
22 | 1: '/map',
23 | 2: '/monitor',
24 | 3: '/system-design',
25 | };
26 | redirect(redirects[step]);
27 | setStep(step);
28 | };
29 |
30 | return (
31 | redirect('/map')}
36 | >
37 |
38 |
39 |
57 |
58 |
59 | } />
60 | } />
61 | } />
62 | } />
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default App;
71 |
--------------------------------------------------------------------------------
/shared/lastNames.js:
--------------------------------------------------------------------------------
1 | const lastNames = [
2 | "Smith",
3 | "Johnson",
4 | "Brown",
5 | "Miller",
6 | "Taylor",
7 | "Jones",
8 | "Anderson",
9 | "Thompson",
10 | "Davis",
11 | "Wilson",
12 | "Williams",
13 | "Jackson",
14 | "Moore",
15 | "Lee",
16 | "Lewis",
17 | "Hall",
18 | "Clark",
19 | "Harris",
20 | "Young",
21 | "King",
22 | "Smith",
23 | "Müller",
24 | "Garcia",
25 | "Rossi",
26 | "Andersen",
27 | "Dubois",
28 | "Jansen",
29 | "Nowak",
30 | "Petrov",
31 | "Kovačević",
32 | "O''Connor",
33 | "Ivanov",
34 | "Wagner",
35 | "Fernandez",
36 | "Silva",
37 | "Johansson",
38 | "Lefebvre",
39 | "De Luca",
40 | "Kowalski",
41 | "Popescu",
42 | "Schneider",
43 | "Fischer",
44 | "Santos",
45 | "Petrova",
46 | "López",
47 | "Jensen",
48 | "García Pérez",
49 | "Kuznetsov",
50 | "Fernández García",
51 | "Kováč",
52 | "Alić",
53 | "Tóth",
54 | "Rasmussen",
55 | "Novak",
56 | "Ilić",
57 | "Zimmermann",
58 | "Jensen",
59 | "Mueller",
60 | "Johnson",
61 | "Kim",
62 | "Li",
63 | "Wang",
64 | "Suzuki",
65 | "Chen",
66 | "Tanaka",
67 | "Nguyen",
68 | "Khan",
69 | "Park",
70 | "Rahman",
71 | "Singh",
72 | "Huang",
73 | "Sharma",
74 | "Wu",
75 | "Ahmed",
76 | "Takahashi",
77 | "Joshi",
78 | "Mohammed",
79 | "Abdullah",
80 | "Kumar",
81 | "Hassan",
82 | "Ma",
83 | "Islam",
84 | "Kang",
85 | "Hori",
86 | "Liu",
87 | "Gupta",
88 | "Zhang",
89 | "Yamamoto",
90 | "Ali",
91 | "Choi",
92 | "Raj",
93 | "Chung",
94 | "Kaur",
95 | "Shin",
96 | "Ko",
97 | "Liou",
98 | "Nakamura",
99 | "Hong",
100 | "Yuan",
101 | "Abdullah",
102 | "Adeyemi",
103 | "Bouabid",
104 | "Diop",
105 | "Gaye",
106 | "Kamara",
107 | "Mokgosi",
108 | "Nkrumah",
109 | "Nzuzi",
110 | "Rodriguez",
111 | "Silva",
112 | "Santos",
113 | "Fernandez",
114 | "Gomez",
115 | "Perez",
116 | "Gonzalez",
117 | "Lopez",
118 | "Martinez",
119 | "Alvarez",
120 | "Dos Santos",
121 | "Souza",
122 | "Da Silva",
123 | "Pereira",
124 | "Ferreira",
125 | "Silva Santos",
126 | "Castro",
127 | "Oliveira",
128 | "Andrade",
129 | "Costa"
130 | ];
131 | export default lastNames;
--------------------------------------------------------------------------------
/monitor/docker-compose-macos.yml:
--------------------------------------------------------------------------------
1 | services:
2 | prometheus:
3 | image: prom/prometheus:latest
4 | container_name: prometheus
5 | ports:
6 | - "9090:9090"
7 | volumes:
8 | - ./prometheus-local.yml:/etc/prometheus/prometheus.yml
9 | - prometheus-data:/prometheus
10 | restart: unless-stopped
11 | command:
12 | - "--config.file=/etc/prometheus/prometheus.yml"
13 |
14 | node_exporter:
15 | image: quay.io/prometheus/node-exporter:latest
16 | container_name: node_exporter
17 | command:
18 | - '--path.rootfs=/host'
19 | restart: unless-stopped
20 | volumes:
21 | - '/:/host:ro'
22 | - '/private/var:/host/private/var:ro'
23 | user: "0:0"
24 |
25 | postgres-exporter:
26 | image: quay.io/prometheuscommunity/postgres-exporter:v0.11.1
27 | container_name: postgres_exporter
28 | environment:
29 | - DATA_SOURCE_NAME=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${SERVER_IP}:5432/${POSTGRES_DBNAME}?sslmode=disable
30 | volumes:
31 | - ./postgres_exporter.yml:/postgres_exporter.yml
32 | - ./.env:/app/.env
33 | ports:
34 | - 9187:9187
35 |
36 | cadvisor:
37 | image: gcr.io/cadvisor/cadvisor:v0.45.0
38 | container_name: cadvisor
39 | ports:
40 | - "9092:8080"
41 | volumes:
42 | - /:/rootfs:ro
43 | - /var/run:/var/run:ro
44 | - /sys:/sys:ro
45 | - /var/lib/docker:/var/lib/docker:ro
46 | restart: unless-stopped
47 | privileged: true
48 |
49 | grafana:
50 | image: grafana/grafana-oss:latest
51 | container_name: grafana
52 | ports:
53 | - "3000:3000"
54 | volumes:
55 | - grafana-data:/var/lib/grafana
56 | - ./grafana.ini:/etc/grafana/grafana.ini
57 | restart: unless-stopped
58 |
59 | loki:
60 | image: grafana/loki:latest
61 | container_name: loki
62 | ports:
63 | - "3100:3100"
64 | command: -config.file=/etc/loki/local-config.yaml
65 | volumes:
66 | - ./loki-local-config.yaml:/etc/loki/local-config.yaml
67 | restart: unless-stopped
68 |
69 | promtail:
70 | image: grafana/promtail:latest
71 | container_name: promtail
72 | volumes:
73 | - /var/log:/var/log
74 | - ./promtail-config.yaml:/etc/promtail/config.yml
75 | command: -config.file=/etc/promtail/config.yml
76 | restart: unless-stopped
77 |
78 | volumes:
79 | prometheus-data:
80 | driver: local
81 | grafana-data:
82 | driver: local
--------------------------------------------------------------------------------
/frontend/src/movement.tsx:
--------------------------------------------------------------------------------
1 | export const isBetween = (val: number, curr: number, prev: number): boolean =>
2 | (val <= curr && val >= prev) || (val >= curr && val <= prev);
3 |
4 | export const getNextCoordIndex = (
5 | currX: number,
6 | currY: number,
7 | path: number[][]
8 | ): number => {
9 | return path.findIndex(([x, y], i, path) => {
10 | if (currX === path[i][0] && currY === path[i][1]) return true;
11 | if (i === 0) return false;
12 |
13 | const xMatches = x === currX;
14 | const yMatches = y === currY;
15 |
16 | return (
17 | (xMatches && isBetween(currY, path[i][1], path[i - 1][1])) ||
18 | (yMatches && isBetween(currX, path[i][0], path[i - 1][0]))
19 | );
20 | });
21 | };
22 |
23 | export const advanceCoord = (
24 | curr: number,
25 | next: number,
26 | increment: number
27 | ): number => {
28 | if (next > curr) {
29 | curr = curr + increment;
30 | if (curr + increment > next) curr = next;
31 | } else {
32 | curr = curr - increment;
33 | if (curr - increment < next) curr = next;
34 | }
35 |
36 | return curr;
37 | };
38 |
39 | export const getDirection = (section: number[][], i: number): 'x' | 'y' => {
40 | const x0 = section[i - 1][0];
41 | const x1 = section[i][0];
42 | return x1 !== x0 ? 'x' : 'y';
43 | };
44 |
45 | export const countTurns = (section: number[][]): number => {
46 | let count = 0;
47 | let currDirection = getDirection(section, 1);
48 |
49 | for (let i = 2; i < section.length; i++) {
50 | let newDirection = getDirection(section, i);
51 | if (newDirection !== currDirection) {
52 | currDirection = newDirection;
53 | count++;
54 | }
55 | }
56 |
57 | return count;
58 | };
59 |
60 | export const getRotation = (path: number[][], i: number): number => {
61 | const [x0, y0] = path[i - 1];
62 | const [x1, y1] = path[i];
63 | const direction = x1 !== x0 ? 'x' : 'y';
64 |
65 | if (direction === 'x' && x1 > x0) return 90;
66 | else if (direction === 'x' && x0 > x1) return 270;
67 | else if (direction === 'y' && y1 > y0) return 180;
68 | else return 0;
69 | };
70 |
71 | interface TurnDistance {
72 | distClockwise: number;
73 | distCounterclockwise: number;
74 | }
75 |
76 | export const getTurnDistance = (
77 | curr: number,
78 | target: number
79 | ): TurnDistance => ({
80 | distClockwise:
81 | target > curr && target <= 360 ? target - curr : 360 - curr + target,
82 | distCounterclockwise:
83 | target >= 0 && target < curr ? curr - target : curr + 360 - target,
84 | });
85 |
--------------------------------------------------------------------------------
/frontend/build/static/js/main.84f9deb3.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * Determine if an object is a Buffer
3 | *
4 | * @author Feross Aboukhadijeh
5 | * @license MIT
6 | */
7 |
8 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
9 |
10 | /**
11 | * @license React
12 | * react-dom.production.min.js
13 | *
14 | * Copyright (c) Facebook, Inc. and its affiliates.
15 | *
16 | * This source code is licensed under the MIT license found in the
17 | * LICENSE file in the root directory of this source tree.
18 | */
19 |
20 | /**
21 | * @license React
22 | * react-is.production.min.js
23 | *
24 | * Copyright (c) Facebook, Inc. and its affiliates.
25 | *
26 | * This source code is licensed under the MIT license found in the
27 | * LICENSE file in the root directory of this source tree.
28 | */
29 |
30 | /**
31 | * @license React
32 | * react-jsx-runtime.production.min.js
33 | *
34 | * Copyright (c) Facebook, Inc. and its affiliates.
35 | *
36 | * This source code is licensed under the MIT license found in the
37 | * LICENSE file in the root directory of this source tree.
38 | */
39 |
40 | /**
41 | * @license React
42 | * react.production.min.js
43 | *
44 | * Copyright (c) Facebook, Inc. and its affiliates.
45 | *
46 | * This source code is licensed under the MIT license found in the
47 | * LICENSE file in the root directory of this source tree.
48 | */
49 |
50 | /**
51 | * @license React
52 | * scheduler.production.min.js
53 | *
54 | * Copyright (c) Facebook, Inc. and its affiliates.
55 | *
56 | * This source code is licensed under the MIT license found in the
57 | * LICENSE file in the root directory of this source tree.
58 | */
59 |
60 | /**
61 | * @remix-run/router v1.5.0
62 | *
63 | * Copyright (c) Remix Software Inc.
64 | *
65 | * This source code is licensed under the MIT license found in the
66 | * LICENSE.md file in the root directory of this source tree.
67 | *
68 | * @license MIT
69 | */
70 |
71 | /**
72 | * React Router DOM v6.10.0
73 | *
74 | * Copyright (c) Remix Software Inc.
75 | *
76 | * This source code is licensed under the MIT license found in the
77 | * LICENSE.md file in the root directory of this source tree.
78 | *
79 | * @license MIT
80 | */
81 |
82 | /**
83 | * React Router v6.10.0
84 | *
85 | * Copyright (c) Remix Software Inc.
86 | *
87 | * This source code is licensed under the MIT license found in the
88 | * LICENSE.md file in the root directory of this source tree.
89 | *
90 | * @license MIT
91 | */
92 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | code {
6 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
7 | monospace;
8 | }
9 |
10 | .content {
11 | width: 100%;
12 | }
13 |
14 | .view-map {
15 | flex: 1;
16 | display: flex;
17 | flex-direction: row-reverse;
18 | max-height: 100%;
19 | height: 100%;
20 | }
21 |
22 | .view-map .list {
23 | position: relative;
24 | flex: 1;
25 | overflow: auto;
26 | height: 100%;
27 | max-height: 100%;
28 | }
29 |
30 | .view-map .map {
31 | position: relative;
32 | border: solid 0.5px lightgray;
33 | flex: 0 0 auto;
34 | width: 100vh;
35 | height: 100%;
36 | }
37 |
38 | @media only screen and (max-width: 768px) {
39 | .view-map {
40 | width: 100%;
41 | max-width: 100%;
42 | }
43 |
44 | .view-map .list {
45 | position: relative;
46 | width: 100%;
47 | max-width: 100%;
48 | }
49 |
50 | .view-map .map {
51 | width: 100%;
52 | max-width: 100%;
53 | position: relative;
54 | border: solid 0.5px lightgray;
55 | }
56 | }
57 |
58 | .map-inner {
59 | position: relative;
60 | }
61 |
62 | .map-inner svg {
63 | height: 100%;
64 | width: 100%;
65 | background: white;
66 | }
67 |
68 | .map-refresh.active {
69 | position: absolute;
70 | top: 0;
71 | left: 0;
72 | right: 0;
73 | bottom: 0;
74 | background-color: rgba(255, 255, 255, 0.8);
75 | z-index: 1000;
76 | }
77 |
78 | #error-page {
79 | display: flex;
80 | flex-direction: column;
81 | align-items: center;
82 | justify-content: center;
83 | width: 100%;
84 | }
85 |
86 | @keyframes blink {
87 | 0% {
88 | opacity: 1;
89 | }
90 | 50% {
91 | opacity: 0.5;
92 | }
93 | 100% {
94 | opacity: 1;
95 | }
96 | }
97 |
98 | .status-tag {
99 | transition: background-color 0.3s ease-in-out;
100 | animation: blink 0.2s ease-in-out 2;
101 | }
102 |
103 | .blink {
104 | animation-name: blink;
105 | animation-duration: 0.2s;
106 | animation-timing-function: ease-in-out;
107 | animation-iteration-count: 2;
108 | }
109 |
110 | .docs {
111 | color: #1e293b;
112 | }
113 |
114 | .docs h1 {
115 | font-size: 24px;
116 | font-weight: 600;
117 | }
118 |
119 | .docs h2 {
120 | margin-top: 20px;
121 | font-size: 20px;
122 | font-weight: 600;
123 | }
124 |
125 | .docs h3 {
126 | margin-top: 20px;
127 | font-size: 18px;
128 | font-weight: 600;
129 | }
130 |
131 | .docs p {
132 | margin-top: 15px;
133 | }
134 |
135 | .docs ul {
136 | margin-top: 10px;
137 | }
138 |
139 | .docs ul li {
140 | list-style-position: inside;
141 | list-style-type: disc;
142 | }
143 |
--------------------------------------------------------------------------------
/monitor/docker-compose-linux.yml:
--------------------------------------------------------------------------------
1 | services:
2 | prometheus:
3 | image: prom/prometheus:latest
4 | container_name: prometheus
5 | ports:
6 | - "9090:9090"
7 | volumes:
8 | - ./prometheus-prod.yml:/etc/prometheus/prometheus.yml
9 | - prometheus-data:/prometheus
10 | restart: unless-stopped
11 | command:
12 | - "--config.file=/etc/prometheus/prometheus.yml"
13 | - "--web.external-url=/prometheus/"
14 | - "--web.route-prefix=/prometheus/"
15 |
16 | node_exporter:
17 | image: quay.io/prometheus/node-exporter:latest
18 | container_name: node_exporter
19 | command:
20 | - '--path.rootfs=/host'
21 | pid: host
22 | restart: unless-stopped
23 | volumes:
24 | - '/:/host:ro,rslave'
25 |
26 | postgres-exporter:
27 | image: quay.io/prometheuscommunity/postgres-exporter:v0.11.1
28 | container_name: postgres_exporter
29 | environment:
30 | - DATA_SOURCE_NAME=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${SERVER_IP}:5432/${POSTGRES_DBNAME}?sslmode=disable
31 | volumes:
32 | - ./postgres_exporter.yml:/postgres_exporter.yml
33 | - ./postgres_exporter-queries.yml:/queries.yml
34 | ports:
35 | - 9187:9187
36 | command:
37 | - '--extend.query-path=/queries.yml'
38 |
39 | cadvisor:
40 | image: gcr.io/cadvisor/cadvisor:v0.45.0
41 | container_name: cadvisor
42 | ports:
43 | - "9092:8080"
44 | volumes:
45 | - /:/rootfs:ro
46 | - /var/run:/var/run:ro
47 | - /sys:/sys:ro
48 | - /var/lib/docker/:/var/lib/docker:ro
49 | - /dev/disk/:/dev/disk:ro
50 | devices:
51 | - /dev/kmsg
52 | restart: unless-stopped
53 | privileged: true
54 |
55 | grafana:
56 | image: grafana/grafana-oss:latest
57 | user: "0:0"
58 | container_name: grafana
59 | ports:
60 | - "3000:3000"
61 | volumes:
62 | - grafana-data:/var/lib/grafana
63 | - ./grafana.ini:/etc/grafana/grafana.ini
64 | - /etc/letsencrypt:/etc/letsencrypt
65 | restart: unless-stopped
66 |
67 | loki:
68 | image: grafana/loki:latest
69 | container_name: loki
70 | ports:
71 | - "3100:3100"
72 | command: -config.file=/etc/loki/local-config.yaml
73 | volumes:
74 | - ./loki-local-config.yaml:/etc/loki/local-config.yaml
75 | restart: unless-stopped
76 |
77 | promtail:
78 | image: grafana/promtail:latest
79 | container_name: promtail
80 | volumes:
81 | - /var/log:/var/log
82 | - ./promtail-config.yaml:/etc/promtail/config.yml
83 | - /run/docker.sock:/run/docker.sock
84 | command: -config.file=/etc/promtail/config.yml
85 | restart: unless-stopped
86 |
87 | volumes:
88 | prometheus-data:
89 | driver: local
90 | grafana-data:
91 | driver: local
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
55 |
56 |
57 |
58 |
59 |
60 | Rides | Juraj Majerik
61 |
62 |
63 | You need to enable JavaScript to run this app.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/simulation/src/data.ts:
--------------------------------------------------------------------------------
1 | export const drivers = [
2 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703959', name: 'Steven' },
3 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca57', name: 'Frank' },
4 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d0', name: 'William' },
5 |
6 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703950', name: 'James' },
7 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca58', name: 'Robert' },
8 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d1', name: 'Marcus' },
9 |
10 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703951', name: 'Mary' },
11 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca59', name: 'Patricia' },
12 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d2', name: 'Jennifer' },
13 |
14 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703952', name: 'Linda' },
15 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca60', name: 'Elizabeth' },
16 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d3', name: 'Barbara' },
17 | ];
18 |
19 | export const customers = [
20 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a13', name: 'Alice' },
21 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c86', name: 'Michael' },
22 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765a', name: 'Kate' },
23 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c4', name: 'Paul' },
24 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa18', name: 'Susan' },
25 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108763', name: 'Andrew' },
26 |
27 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a14', name: 'Thomas' },
28 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c87', name: 'Sarah' },
29 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765b', name: 'Charles' },
30 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c3', name: 'Karen' },
31 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa19', name: 'Christopher' },
32 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108764', name: 'Lisa' },
33 |
34 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a15', name: 'Daniel' },
35 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c88', name: 'Nancy' },
36 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765c', name: 'Matthew' },
37 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c5', name: 'Betty' },
38 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa20', name: 'Anthony' },
39 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108765', name: 'Margaret' },
40 |
41 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a16', name: 'Mark' },
42 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c89', name: 'Sandra' },
43 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765d', name: 'Donald' },
44 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c6', name: 'Ashley' },
45 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa21', name: 'Stewart' },
46 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108766', name: 'Kimberly' },
47 | ];
48 |
--------------------------------------------------------------------------------
/simulation/dist/Customer.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"Customer.js","sourceRoot":"","sources":["../src/Customer.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/simulation/dist/data.js:
--------------------------------------------------------------------------------
1 | export const drivers = [
2 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703959', name: 'Steven' },
3 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca57', name: 'Frank' },
4 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d0', name: 'William' },
5 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703950', name: 'James' },
6 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca58', name: 'Robert' },
7 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d1', name: 'Marcus' },
8 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703951', name: 'Mary' },
9 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca59', name: 'Patricia' },
10 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d2', name: 'Jennifer' },
11 | { driverId: 'd0c600a2-298c-44dc-b9d7-85f790703952', name: 'Linda' },
12 | { driverId: '1cfa84fc-1991-4a5d-8e66-ff067876ca60', name: 'Elizabeth' },
13 | { driverId: '768e9685-01f6-4a64-a718-68373ca2f9d3', name: 'Barbara' },
14 | ];
15 | export const customers = [
16 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a13', name: 'Alice' },
17 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c86', name: 'Michael' },
18 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765a', name: 'Kate' },
19 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c4', name: 'Paul' },
20 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa18', name: 'Susan' },
21 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108763', name: 'Andrew' },
22 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a14', name: 'Thomas' },
23 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c87', name: 'Sarah' },
24 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765b', name: 'Charles' },
25 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c3', name: 'Karen' },
26 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa19', name: 'Christopher' },
27 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108764', name: 'Lisa' },
28 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a15', name: 'Daniel' },
29 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c88', name: 'Nancy' },
30 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765c', name: 'Matthew' },
31 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c5', name: 'Betty' },
32 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa20', name: 'Anthony' },
33 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108765', name: 'Margaret' },
34 | { customerId: 'a033590f-daf2-408c-a07f-fdd37a8a6a16', name: 'Mark' },
35 | { customerId: '454652dc-e13a-480e-a505-ebe8758b5c89', name: 'Sandra' },
36 | { customerId: 'd90ebf24-70ad-47c9-a80d-9075c14c765d', name: 'Donald' },
37 | { customerId: 'e98a4069-41fd-4961-86ac-bffb9eea08c6', name: 'Ashley' },
38 | { customerId: '1c67131b-aa8d-42cc-87b9-5ed7cc88fa21', name: 'Stewart' },
39 | { customerId: 'cce3fd42-e97c-4164-8e88-f3b3d0108766', name: 'Kimberly' },
40 | ];
41 | //# sourceMappingURL=data.js.map
--------------------------------------------------------------------------------
/frontend/src/Mobile.tsx:
--------------------------------------------------------------------------------
1 | const Mobile = () => (
2 |
3 |
4 |
5 | rides
6 | {' '}
7 | is a full-stack simulation of a ridesharing app such as{' '}
8 | Uber or{' '}
9 | Bolt .
10 |
11 |
12 | This project is my take on building and visualizing a scalable system. My
13 | motivation was to implement various concepts from the domain of system
14 | design, including containerization ,{' '}
15 | multiprocessing and{' '}
16 | observability .
17 |
18 |
19 | This project took approximately 300 hours of work. Read about the journey
20 | of building it on
21 |
27 | my blog
28 |
29 | .
30 |
31 |
32 |
33 |
49 |
50 |
51 |
52 |
Juraj Majerik
53 |
76 |
77 |
78 |
79 |
80 | A note to mobile users: this app is not yet optimized for small screens.
81 | To get the full experience, please view it on a large screen.
82 |
83 |
84 | );
85 | export default Mobile;
86 |
--------------------------------------------------------------------------------
/frontend/src/Nav.tsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from 'react-router-dom';
2 |
3 | const paths = {
4 | map: (
5 |
10 | ),
11 | 'system-design': (
12 |
17 | ),
18 | monitor: (
19 |
24 | ),
25 | blog: (
26 |
31 | ),
32 | };
33 |
34 | const Icon = ({ type }) => (
35 |
36 |
45 | {paths[type]}
46 |
47 |
48 | );
49 |
50 | const Nav = () => {
51 | const Li = ({ path, label }) => (
52 | {
55 | let className = 'inline-flex w-full rounded-md ';
56 | if (isActive) {
57 | className += 'bg-blue-50x text-blue-700';
58 | } else {
59 | className += 'text-slate-800';
60 | }
61 | return className;
62 | }}
63 | >
64 |
68 |
69 | {label}
70 |
71 |
72 | );
73 |
74 | const config = [
75 | ['map', 'Map'],
76 | ['monitor', 'Monitor'],
77 | ['system-design', 'System Design'],
78 | ];
79 |
80 | const lis = config.map(([key, label]) => (
81 |
82 | ));
83 |
84 | return (
85 |
94 | );
95 | };
96 | export default Nav;
97 |
--------------------------------------------------------------------------------
/simulation/src/methods.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | buildGraph,
3 | getDestinationRange,
4 | generateDestination,
5 | getClosestRoadNode,
6 | getShortestPath,
7 | // @ts-ignore
8 | } from './methods';
9 | import config from '../../shared/config.js';
10 | import { CoordPair, Graph } from './types.js';
11 |
12 | const { gridCount } = config;
13 |
14 | test('return a graph represented as n x n matrix', () => {
15 | let gridCount = 3;
16 | let obstaclesMap = new Map();
17 | obstaclesMap.set('0:0', '#');
18 | obstaclesMap.set('1:1', '#');
19 | obstaclesMap.set('2:2', '#');
20 |
21 | let expected = [
22 | [0, 1, 1],
23 | [1, 0, 1],
24 | [1, 1, 0],
25 | ];
26 | expect(buildGraph(obstaclesMap, gridCount)).toEqual(expected);
27 |
28 | gridCount = 6;
29 | obstaclesMap = new Map();
30 | obstaclesMap.set('0:2', '#');
31 | obstaclesMap.set('0:3', '#');
32 | obstaclesMap.set('1:2', '#');
33 | obstaclesMap.set('1:3', '#');
34 | obstaclesMap.set('5:5', '#');
35 | obstaclesMap.set('4:5', '#');
36 |
37 | expected = [
38 | [1, 1, 1, 1, 1, 1],
39 | [1, 1, 1, 1, 1, 1],
40 | [0, 0, 1, 1, 1, 1],
41 | [0, 0, 1, 1, 1, 1],
42 | [1, 1, 1, 1, 1, 1],
43 | [1, 1, 1, 1, 0, 0],
44 | ];
45 | expect(buildGraph(obstaclesMap, gridCount)).toEqual(expected);
46 | });
47 |
48 | test('return a range of possible destinations for a starting coordinate', () => {
49 | expect(getDestinationRange(0)).toEqual([50, 100]);
50 | expect(getDestinationRange(49)).toEqual([74, 100]);
51 | expect(getDestinationRange(50)).toEqual([0, 25]);
52 | expect(getDestinationRange(99)).toEqual([0, 50]);
53 | });
54 |
55 | test('return destination coordinates', () => {
56 | let [destX, destY] = generateDestination([0, 0]);
57 | expect(destX).toBeGreaterThanOrEqual(gridCount / 2);
58 | expect(destX).toBeLessThanOrEqual(gridCount);
59 | expect(destY).toBeGreaterThanOrEqual(gridCount / 2);
60 | expect(destY).toBeLessThanOrEqual(gridCount);
61 |
62 | [destX, destY] = generateDestination([50, 50]);
63 |
64 | expect(destX).toBeGreaterThanOrEqual(0);
65 | expect(destX).toBeLessThanOrEqual(25);
66 | expect(destY).toBeGreaterThanOrEqual(0);
67 | expect(destY).toBeLessThanOrEqual(25);
68 |
69 | [destX, destY] = generateDestination([6, 71]);
70 | });
71 |
72 | test('return the closest road node (can be the node itself)', () => {
73 | const graph: Graph = [
74 | [0, 1, 1, 1, 0],
75 | [1, 0, 1, 1, 0],
76 | [1, 0, 0, 1, 0],
77 | [1, 0, 1, 1, 0],
78 | [1, 1, 0, 0, 0],
79 | ];
80 | expect(getClosestRoadNode(2, 2, graph)).toEqual([2, 1]);
81 | expect(getClosestRoadNode(1, 3, graph)).toEqual([2, 3]);
82 | });
83 |
84 | test('return the shortest path between two points', () => {
85 | let graph: Graph = [
86 | [1, 0, 0, 0, 0, 1],
87 | [1, 1, 1, 0, 1, 1],
88 | [0, 0, 1, 0, 1, 0],
89 | [0, 0, 1, 1, 1, 0],
90 | [0, 0, 0, 0, 0, 0],
91 | ];
92 | let startingPosition: CoordPair = [0, 0];
93 | let destination: CoordPair = [5, 0];
94 | let path = getShortestPath(startingPosition, destination, graph);
95 |
96 | expect(path).toEqual([
97 | [0, 0],
98 | [0, 1],
99 | [1, 1],
100 | [2, 1],
101 | [2, 2],
102 | [2, 3],
103 | [3, 3],
104 | [4, 3],
105 | [4, 2],
106 | [4, 1],
107 | [5, 1],
108 | [5, 0],
109 | ]);
110 |
111 | graph = [
112 | [0, 0, 0, 0, 1],
113 | [0, 0, 0, 0, 1],
114 | [0, 0, 0, 0, 1],
115 | [0, 0, 0, 0, 1],
116 | [0, 0, 0, 1, 1],
117 | ];
118 | startingPosition = [4, 0];
119 | destination = [3, 4];
120 | path = getShortestPath(startingPosition, destination, graph);
121 |
122 | expect(path).toEqual([
123 | [4, 0],
124 | [4, 1],
125 | [4, 2],
126 | [4, 3],
127 | [4, 4],
128 | [3, 4],
129 | ]);
130 | });
131 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Rides simulates the basic flow of a ride-hailing app. It consists of five main components:
2 |
3 | - The __simulation engine__ generates the system's state and updates the database.
4 | - The __database__ persists the state.
5 | - The __web server__ exposes the state via a RESTful API to the client.
6 | - The __UI__ presents the real-time state of the system.
7 | - The __monitoring & logging__ stack provides observability of the system.
8 |
9 | Technologies used include __Node.js__ (simulation), __Go__ (web server) and __PostgreSQL__. The frontend is written in __React__. All JavaScript is typed with __TypeScript__. Monitoring is implemented with __Prometheus and Grafana__. All backend components are containerized with __Docker__.
10 |
11 | 
12 |
13 | The system is running on a single machine with 2 GiB of memory. The distributed nature of the system is already somewhat simulated by using containers. My plan eventually is to spread it across multiple virtual servers, thus achieving a true distributed system.
14 |
15 | ## Simulation engine
16 |
17 | The key components of the simulation are the __*Driver*__ and __*Customer*__ instances. These instances make periodical decisions in an infinite loop. These decisions include, among others:
18 |
19 | - Spawn and deactivation
20 | - Request for the optimal route
21 | - Request for a new destination
22 | - Passenger pick-up
23 | - Advancement of the car on the map
24 | - Passenger drop-off
25 |
26 | Multiple __*Driver*__ and __*Customer*__ instances are running simultaneously within the same process. The simulation process is lightweight - its purpose is only to run the simulation loops and update the state.
27 |
28 | To ensure the simulation stays performant and the updates happen within the predefined interval, it is imperative that the more expensive calculations do not block the main process. Such calculations include:
29 |
30 | - Generating destinations
31 | - Calculating the optimal route
32 | - Matching customers with drivers
33 |
34 | For this reason, these calculations are running as child processes. Each of these processes maintains a queue. __*Customer*__ and __*Driver*__ entities request data from these services as needed and await a response. They might complete several simulation cycles before the response arrives, at which point they start acting according to the new information.
35 |
36 | ## Database
37 |
38 | The database is PostgreSQL and stores the data in __*customers*__ and __*drivers*__ tables:
39 |
40 | __*customers*__: id, customer_id, name, active, location, destination, driver_id
41 | __*drivers*__: id, driver_id, name, status, location, path, text, path_index, customer_id, customer_name
42 |
43 | *While a NoSQL database is probably more suitable for this project due to schema flexibility and better write throughput, I wanted more hands-on experience with SQL, since in my previous projects and work experience, NoSQL has been prevalent. This is a common theme in this project - I might choose a particular technology simply because I’m interested in exploring it.*
44 |
45 | ## Web server
46 | The web server is written in Go. It exposes the data via a RESTful API at the __*/customers*__ and __*/drivers*__ endpoints and servers the frontend application. It also exposes the Grafana dashboard via a proxy.
47 |
48 | ## UI
49 |
50 | The frontend application is written in React. The frontend polls the server for updates in 1 second intervals. The map visualization is implemented from scratch. On receiving an update, it spreads the movement of the car until the next update, achieving a smooth animation in small increments.
51 |
52 | ## Monitoring & logging
53 |
54 | Monitoring & logging stack is implemented with Prometheus & Grafana. It provides observability of the system parameters such as disk space and memory, as well as container-specific metrics.
--------------------------------------------------------------------------------
/simulation/dist/methods.test.js:
--------------------------------------------------------------------------------
1 | import { buildGraph, getDestinationRange, generateDestination, getClosestRoadNode, getShortestPath,
2 | // @ts-ignore
3 | } from './methods';
4 | import config from '../../shared/config.js';
5 | const { gridCount } = config;
6 | test('return a graph represented as n x n matrix', () => {
7 | let gridCount = 3;
8 | let obstaclesMap = new Map();
9 | obstaclesMap.set('0:0', '#');
10 | obstaclesMap.set('1:1', '#');
11 | obstaclesMap.set('2:2', '#');
12 | let expected = [
13 | [0, 1, 1],
14 | [1, 0, 1],
15 | [1, 1, 0],
16 | ];
17 | expect(buildGraph(obstaclesMap, gridCount)).toEqual(expected);
18 | gridCount = 6;
19 | obstaclesMap = new Map();
20 | obstaclesMap.set('0:2', '#');
21 | obstaclesMap.set('0:3', '#');
22 | obstaclesMap.set('1:2', '#');
23 | obstaclesMap.set('1:3', '#');
24 | obstaclesMap.set('5:5', '#');
25 | obstaclesMap.set('4:5', '#');
26 | expected = [
27 | [1, 1, 1, 1, 1, 1],
28 | [1, 1, 1, 1, 1, 1],
29 | [0, 0, 1, 1, 1, 1],
30 | [0, 0, 1, 1, 1, 1],
31 | [1, 1, 1, 1, 1, 1],
32 | [1, 1, 1, 1, 0, 0],
33 | ];
34 | expect(buildGraph(obstaclesMap, gridCount)).toEqual(expected);
35 | });
36 | test('return a range of possible destinations for a starting coordinate', () => {
37 | expect(getDestinationRange(0)).toEqual([50, 100]);
38 | expect(getDestinationRange(49)).toEqual([74, 100]);
39 | expect(getDestinationRange(50)).toEqual([0, 25]);
40 | expect(getDestinationRange(99)).toEqual([0, 50]);
41 | });
42 | test('return destination coordinates', () => {
43 | let [destX, destY] = generateDestination([0, 0]);
44 | expect(destX).toBeGreaterThanOrEqual(gridCount / 2);
45 | expect(destX).toBeLessThanOrEqual(gridCount);
46 | expect(destY).toBeGreaterThanOrEqual(gridCount / 2);
47 | expect(destY).toBeLessThanOrEqual(gridCount);
48 | [destX, destY] = generateDestination([50, 50]);
49 | expect(destX).toBeGreaterThanOrEqual(0);
50 | expect(destX).toBeLessThanOrEqual(25);
51 | expect(destY).toBeGreaterThanOrEqual(0);
52 | expect(destY).toBeLessThanOrEqual(25);
53 | [destX, destY] = generateDestination([6, 71]);
54 | });
55 | test('return the closest road node (can be the node itself)', () => {
56 | const graph = [
57 | [0, 1, 1, 1, 0],
58 | [1, 0, 1, 1, 0],
59 | [1, 0, 0, 1, 0],
60 | [1, 0, 1, 1, 0],
61 | [1, 1, 0, 0, 0],
62 | ];
63 | expect(getClosestRoadNode(2, 2, graph)).toEqual([2, 1]);
64 | expect(getClosestRoadNode(1, 3, graph)).toEqual([2, 3]);
65 | });
66 | test('return the shortest path between two points', () => {
67 | let graph = [
68 | [1, 0, 0, 0, 0, 1],
69 | [1, 1, 1, 0, 1, 1],
70 | [0, 0, 1, 0, 1, 0],
71 | [0, 0, 1, 1, 1, 0],
72 | [0, 0, 0, 0, 0, 0],
73 | ];
74 | let startingPosition = [0, 0];
75 | let destination = [5, 0];
76 | let path = getShortestPath(startingPosition, destination, graph);
77 | expect(path).toEqual([
78 | [0, 0],
79 | [0, 1],
80 | [1, 1],
81 | [2, 1],
82 | [2, 2],
83 | [2, 3],
84 | [3, 3],
85 | [4, 3],
86 | [4, 2],
87 | [4, 1],
88 | [5, 1],
89 | [5, 0],
90 | ]);
91 | graph = [
92 | [0, 0, 0, 0, 1],
93 | [0, 0, 0, 0, 1],
94 | [0, 0, 0, 0, 1],
95 | [0, 0, 0, 0, 1],
96 | [0, 0, 0, 1, 1],
97 | ];
98 | startingPosition = [4, 0];
99 | destination = [3, 4];
100 | path = getShortestPath(startingPosition, destination, graph);
101 | expect(path).toEqual([
102 | [4, 0],
103 | [4, 1],
104 | [4, 2],
105 | [4, 3],
106 | [4, 4],
107 | [3, 4],
108 | ]);
109 | });
110 | //# sourceMappingURL=methods.test.js.map
--------------------------------------------------------------------------------
/simulation/dist/Customer.js:
--------------------------------------------------------------------------------
1 | import g from './global.js';
2 | import { wait, getRandomInt, decide } from '../../shared/utils.js';
3 | import config from '../../shared/config.js';
4 | const { maxActiveCustomers } = config;
5 | export default class Customer {
6 | constructor({ customerId, name }) {
7 | this.busy = false;
8 | this.active = false;
9 | this.location = null;
10 | this.destination = null;
11 | this.driverId = null;
12 | this.customerId = customerId;
13 | this.name = name;
14 | this.deactivate = this.deactivate.bind(this);
15 | this.handleDestinationResult = this.handleDestinationResult.bind(this);
16 | this.simulate();
17 | }
18 | async updateDB() {
19 | try {
20 | g.db.query(`
21 | INSERT INTO customers (customer_id, name, active, location, destination, driver_id)
22 | VALUES (
23 | '${this.customerId}',
24 | '${this.name}',
25 | ${this.active},
26 | '${this.location && `${this.location[0]}:${this.location[1]}`}',
27 | '${this.destination && `${this.destination[0]}:${this.destination[1]}`}',
28 | ${this.driverId ? `'${this.driverId}'` : null}
29 | )
30 | ON CONFLICT (name)
31 | DO UPDATE SET
32 | name = EXCLUDED.name,
33 | active = EXCLUDED.active,
34 | location = EXCLUDED.location,
35 | destination = EXCLUDED.destination,
36 | driver_id = EXCLUDED.driver_id
37 | `);
38 | }
39 | catch (error) {
40 | console.error(error);
41 | }
42 | return;
43 | }
44 | deactivate() {
45 | g.activeCustomers.delete(this.customerId);
46 | this.active = false;
47 | this.location = null;
48 | this.destination = null;
49 | this.driverId = null;
50 | this.updateDB();
51 | }
52 | async simulate() {
53 | // Refresh every 200ms
54 | while (true) {
55 | await wait(200);
56 | // Only make new decisions if not currently waiting for a result
57 | // of an asynchronous operation
58 | if (!this.busy) {
59 | // Not active, reactivate with some probability
60 | if (!this.active) {
61 | if (g.activeCustomers.size < maxActiveCustomers) {
62 | let newActive = false;
63 | newActive = decide(5);
64 | if (newActive) {
65 | this.active = true;
66 | this.updateDB();
67 | }
68 | }
69 | }
70 | else if (this.active && !this.location) {
71 | // No location yet -> set location, request destination
72 | this.busy = true;
73 | const location = g.roadNodes[getRandomInt(0, g.roadNodes.length - 1)];
74 | this.location = location;
75 | g.activeCustomers.add(this.customerId);
76 | g.getDestination.send({
77 | customerId: this.customerId,
78 | location: this.location,
79 | });
80 | }
81 | else if (this.active && !this.driverId) {
82 | // Match with a driver
83 | this.busy = true;
84 | g.dispatcher.send({
85 | from: 'customer',
86 | data: {
87 | customerId: this.customerId,
88 | name: this.name,
89 | location: this.location,
90 | },
91 | });
92 | }
93 | }
94 | }
95 | }
96 | handleDestinationResult(destination) {
97 | this.destination = destination;
98 | this.busy = false;
99 | this.updateDB();
100 | }
101 | handleDispatcherResult(driverId) {
102 | this.driverId = driverId;
103 | this.busy = false;
104 | this.updateDB();
105 | }
106 | }
107 | //# sourceMappingURL=Customer.js.map
--------------------------------------------------------------------------------
/frontend/src/DocsView.tsx:
--------------------------------------------------------------------------------
1 | import ReactMarkdown from 'react-markdown';
2 | const markdown1 = `
3 | # System design
4 | Rides simulates the basic flow of a ridesharing app. It consists of five main components:
5 |
6 | - The __simulation engine__ generates the system's state and updates the database.
7 | - The __database__ persists the state.
8 | - The __web server__ exposes the state via a RESTful API to the client.
9 | - The __UI__ presents the real-time state of the system.
10 | - The __monitoring & logging__ stack provides observability of the system.
11 |
12 | Technologies used include __Node.js__ (simulation), __Go__ (web server) and __PostgreSQL__. The frontend is written in __React__. All JavaScript is typed with __TypeScript__. Monitoring is implemented with __Prometheus and Grafana__. All backend components are containerized with __Docker__.
13 | `;
14 |
15 | const markdown2 = `
16 | The system is running on a single machine with 2 GiB of memory. The distributed nature of the system is already somewhat simulated by using containers. My plan eventually is to spread it across multiple virtual servers, thus achieving a true distributed system.
17 |
18 | ## Simulation engine
19 |
20 | The key components of the simulation are the __*Driver*__ and __*Customer*__ instances. These instances make periodical decisions in an infinite loop. These decisions include, among others:
21 |
22 | - Spawn and deactivation
23 | - Request for the optimal route
24 | - Request for a new destination
25 | - Passenger pick-up
26 | - Advancement of the car on the map
27 | - Passenger drop-off
28 |
29 | Multiple __*Driver*__ and __*Customer*__ instances are running simultaneously within the same process. The simulation process is lightweight - its purpose is only to run the simulation loops and update the state.
30 |
31 | To ensure the simulation stays performant and the updates happen within the predefined interval, it is imperative that the more expensive calculations do not block the main process. Such calculations include:
32 |
33 | - Generating destinations
34 | - Calculating the optimal route
35 | - Matching customers with drivers
36 |
37 | For this reason, these calculations are running as child processes. Each of these processes maintains a queue. __*Customer*__ and __*Driver*__ entities request data from these services as needed and await a response. They might complete several simulation cycles before the response arrives, at which point they start acting according to the new information.
38 |
39 | ## Database
40 |
41 | The database is PostgreSQL and stores the data in __*customers*__ and __*drivers*__ tables:
42 |
43 | __*customers*__: id, customer_id, name, active, location, destination, driver_id
44 | __*drivers*__: id, driver_id, name, status, location, path, text, path_index, customer_id, customer_name
45 |
46 | *While a NoSQL database is probably more suitable for this project due to schema flexibility and better write throughput, I wanted more hands-on experience with SQL, since in my previous projects and work experience, NoSQL has been prevalent. This is a common theme in this project - I might choose a particular technology simply because I’m interested in exploring it.*
47 |
48 | ## Web server
49 | The web server is written in Go. It exposes the data via a RESTful API at the __*/customers*__ and __*/drivers*__ endpoints and servers the frontend application. It also exposes the Grafana dashboard via a proxy.
50 |
51 | ## UI
52 |
53 | The frontend application is written in React. The frontend polls the server for updates in 1 second intervals. The map visualization is implemented from scratch. On receiving an update, it spreads the movement of the car until the next update, achieving a smooth animation in small increments.
54 |
55 | ## Monitoring & logging
56 |
57 | Monitoring & logging stack is implemented with Prometheus & Grafana. It provides observability of the system parameters such as disk space and memory, as well as container-specific metrics.
58 | `;
59 |
60 | const DocsView = () => {
61 | return (
62 |
66 |
67 |
68 |
73 |
74 |
75 |
76 | );
77 | };
78 | export default DocsView;
79 |
--------------------------------------------------------------------------------
/simulation/src/Customer.ts:
--------------------------------------------------------------------------------
1 | import g from './global.js';
2 | import { wait, getRandomInt, decide } from '../../shared/utils.js';
3 | import { CoordPair } from './types.js';
4 | import config from '../../shared/config.js';
5 | import firstNames from '../../shared/firstNames.js';
6 | import lastNames from '../../shared/lastNames.js';
7 |
8 | const { maxActiveCustomers } = config;
9 |
10 | export default class Customer {
11 | private busy = false;
12 |
13 | public customerId: string;
14 | public name: string;
15 | public active = false;
16 | public location: CoordPair | null = null;
17 | public destination: CoordPair | null = null;
18 | private driverId: string | null = null;
19 |
20 | constructor({ customerId }: { customerId: string }) {
21 | this.customerId = customerId;
22 | this.name = `${firstNames[getRandomInt(0, firstNames.length - 1)]} ${
23 | lastNames[getRandomInt(0, lastNames.length - 1)]
24 | }`;
25 |
26 | this.deactivate = this.deactivate.bind(this);
27 | this.handleDestinationResult = this.handleDestinationResult.bind(this);
28 |
29 | this.simulate();
30 | }
31 |
32 | private async updateDB(): Promise {
33 | try {
34 | g.db.query(
35 | `
36 | INSERT INTO customers (customer_id, name, active, location, destination, driver_id)
37 | VALUES (
38 | '${this.customerId}',
39 | '${this.name}',
40 | ${this.active},
41 | '${this.location && `${this.location[0]}:${this.location[1]}`}',
42 | '${
43 | this.destination && `${this.destination[0]}:${this.destination[1]}`
44 | }',
45 | ${this.driverId ? `'${this.driverId}'` : null}
46 | )
47 | ON CONFLICT (customer_id)
48 | DO UPDATE SET
49 | name = EXCLUDED.name,
50 | active = EXCLUDED.active,
51 | location = EXCLUDED.location,
52 | destination = EXCLUDED.destination,
53 | driver_id = EXCLUDED.driver_id
54 | `
55 | );
56 | } catch (error) {
57 | console.error(error);
58 | }
59 |
60 | return;
61 | }
62 |
63 | public deactivate(): void {
64 | g.activeCustomers.delete(this.customerId);
65 |
66 | this.active = false;
67 | this.location = null;
68 | this.destination = null;
69 | this.driverId = null;
70 | this.updateDB();
71 | }
72 |
73 | private async simulate(): Promise {
74 | // Refresh every 200ms
75 | while (true) {
76 | await wait(200);
77 |
78 | // Only make new decisions if not currently waiting for a result
79 | // of an asynchronous operation
80 | if (!this.busy) {
81 | // Not active, reactivate with some probability
82 | if (!this.active) {
83 | if (g.activeCustomers.size < maxActiveCustomers) {
84 | let newActive = false;
85 | newActive = decide(5);
86 | if (newActive) {
87 | this.active = true;
88 | this.name = `${
89 | firstNames[getRandomInt(0, firstNames.length - 1)]
90 | } ${lastNames[getRandomInt(0, lastNames.length - 1)]}`;
91 | this.updateDB();
92 | }
93 | }
94 | } else if (this.active && !this.location) {
95 | // No location yet -> set location, request destination
96 | this.busy = true;
97 |
98 | const location = g.roadNodes[getRandomInt(0, g.roadNodes.length - 1)];
99 | this.location = location;
100 | g.activeCustomers.add(this.customerId);
101 |
102 | g.getDestination.send({
103 | customerId: this.customerId,
104 | location: this.location,
105 | });
106 | } else if (this.active && !this.driverId) {
107 | // Match with a driver
108 | this.busy = true;
109 | g.dispatcher.send({
110 | from: 'customer',
111 | data: {
112 | customerId: this.customerId,
113 | name: this.name,
114 | location: this.location,
115 | },
116 | });
117 | }
118 | }
119 | }
120 | }
121 |
122 | public handleDestinationResult(destination: CoordPair): void {
123 | this.destination = destination;
124 | this.busy = false;
125 | this.updateDB();
126 | }
127 |
128 | public handleDispatcherResult(driverId: string): void {
129 | this.driverId = driverId;
130 | this.busy = false;
131 | this.updateDB();
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/simulation/dist/Driver.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"Driver.js","sourceRoot":"","sources":["../src/Driver.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/simulation/dist/methods.js:
--------------------------------------------------------------------------------
1 | import obstacles from '../../shared/obstacles.js';
2 | import { getRandomInt } from '../../shared/utils.js';
3 | import config from '../../shared/config.js';
4 | import { getObstaclesMap } from '../../shared/methods.js';
5 | const { gridCount } = config;
6 | const cache = { graph: null };
7 | export const getRoadNodes = () => {
8 | const obstaclesMap = getObstaclesMap(obstacles);
9 | const roadNodes = [];
10 | for (let x = 0; x < gridCount; x++) {
11 | for (let y = 0; y < gridCount; y++) {
12 | if (!obstaclesMap.get(`${x}:${y}`)) {
13 | roadNodes.push([x, y]);
14 | }
15 | }
16 | }
17 | return roadNodes.filter(([x, y]) => {
18 | return x !== 0 && x !== gridCount - 1 && y !== 0 && y !== gridCount - 1;
19 | });
20 | };
21 | export const buildGraph = (obstaclesMap, gridCount) => {
22 | const graph = [];
23 | for (let y = 0; y < gridCount; y++) {
24 | graph[y] = [];
25 | for (let x = 0; x < gridCount; x++) {
26 | if (obstaclesMap.get(`${x}:${y}`))
27 | graph[y][x] = 0;
28 | else
29 | graph[y][x] = 1;
30 | }
31 | }
32 | return graph;
33 | };
34 | export const getGraph = () => {
35 | if (cache.graph)
36 | return cache.graph;
37 | cache.graph = buildGraph(getObstaclesMap(obstacles), gridCount);
38 | return cache.graph;
39 | };
40 | export const getDestinationRange = (coord) => coord < gridCount / 2
41 | ? [gridCount / 2 + Math.floor(coord / 2), gridCount]
42 | : [0, gridCount / 2 - Math.floor((gridCount - coord) / 2)];
43 | export const getClosestRoadNode = (x, y, graph = getGraph()) => {
44 | const isValid = (y, x) => y >= 0 && y < graph.length && x >= 0 && x < graph[y].length;
45 | if (isValid(y, x) && graph[y][x] === 1)
46 | return [x, y];
47 | const directions = [
48 | [0, -1],
49 | [1, 0],
50 | [0, 1],
51 | [-1, 0],
52 | ];
53 | let queue = [[y, x]];
54 | const seen = new Set([`${y}:${x}`]);
55 | while (queue.length) {
56 | const nextQueue = [];
57 | for (let i = 0; i < queue.length; i++) {
58 | const [y, x] = queue[i];
59 | for (const [dx, dy] of directions) {
60 | const nextY = y + dy;
61 | const nextX = x + dx;
62 | if (isValid(nextY, nextX) && !seen.has(`${nextY}:${nextX}`)) {
63 | if (graph[nextY][nextX] === 1) {
64 | return [nextX, nextY];
65 | }
66 | seen.add(`${nextY}:${nextX}`);
67 | nextQueue.push([nextY, nextX]);
68 | }
69 | }
70 | }
71 | queue = nextQueue;
72 | }
73 | };
74 | export const generateDestination = (coordPair) => {
75 | const [startX, startY] = coordPair;
76 | const rangeX = getDestinationRange(startX);
77 | const rangeY = getDestinationRange(startY);
78 | const destX = getRandomInt(rangeX[0], rangeX[1]);
79 | const destY = getRandomInt(rangeY[0], rangeY[1]);
80 | let destination = getClosestRoadNode(destX, destY);
81 | return destination;
82 | };
83 | export const getStraightLineDistance = (coordsA, coordsB) => {
84 | const [xA, yA] = coordsA;
85 | const [xB, yB] = coordsB;
86 | return Math.sqrt(Math.pow(xB - xA, 2) + Math.pow(yB - yA, 2));
87 | };
88 | export const getShortestPath = (startingPosition, destination, graph = getGraph()) => {
89 | const isValid = (y, x) => y >= 0 &&
90 | y < graph.length &&
91 | x >= 0 &&
92 | x < graph[y].length &&
93 | graph[y][x] === 1;
94 | const directions = [
95 | [1, 0],
96 | [-1, 0],
97 | [0, 1],
98 | [0, -1],
99 | ];
100 | const [col, row] = startingPosition;
101 | let queue = [[row, col, [startingPosition]]];
102 | const seen = new Set([`${row}:${col}`]);
103 | while (queue.length) {
104 | const nextQueue = [];
105 | for (let i = 0; i < queue.length; i++) {
106 | const [row, col, currPath] = queue[i];
107 | if (row === destination[1] && col === destination[0]) {
108 | return currPath;
109 | }
110 | for (let j = 0; j < directions.length; j++) {
111 | const [dx, dy] = directions[j];
112 | const nextRow = row + dy;
113 | const nextCol = col + dx;
114 | if (isValid(nextRow, nextCol) && !seen.has(`${nextRow}:${nextCol}`)) {
115 | seen.add(`${nextRow}:${nextCol}`);
116 | nextQueue.push([nextRow, nextCol, [...currPath, [nextCol, nextRow]]]);
117 | }
118 | }
119 | }
120 | queue = nextQueue;
121 | }
122 | };
123 | //# sourceMappingURL=methods.js.map
--------------------------------------------------------------------------------
/frontend/src/movement.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | advanceCoord,
3 | countTurns,
4 | getDirection,
5 | getNextCoordIndex,
6 | getRotation,
7 | getTurnDistance,
8 | isBetween,
9 | } from './movement';
10 |
11 | test('check if a number is between two numbers inclusive', () => {
12 | expect(isBetween(14.23, 15, 14)).toEqual(true);
13 | expect(isBetween(15.32, 16, 15)).toEqual(true);
14 | expect(isBetween(16, 16, 15)).toEqual(true);
15 | expect(isBetween(15, 16, 15)).toEqual(true);
16 | expect(isBetween(14, 16, 15)).toEqual(false);
17 | });
18 |
19 | test('get the index next set of coords on the path', () => {
20 | let path = [
21 | [0, 20],
22 | [1, 20],
23 | [2, 20],
24 | [3, 20],
25 | [4, 20],
26 | ];
27 | expect(getNextCoordIndex(1.23, 20, path)).toEqual(2);
28 | expect(getNextCoordIndex(2, 20, path)).toEqual(2);
29 | expect(getNextCoordIndex(0, 20, path)).toEqual(0);
30 | expect(getNextCoordIndex(4, 20, path)).toEqual(4);
31 |
32 | path = [
33 | [8, 17],
34 | [8, 16],
35 | [8, 15],
36 | [8, 14],
37 | [7, 14],
38 | [6, 14],
39 | [6, 13],
40 | [6, 12],
41 | [6, 11],
42 | [6, 10],
43 | [6, 9],
44 | [6, 8],
45 | [6, 7],
46 | [6, 6],
47 | [7, 6],
48 | [8, 6],
49 | [9, 6],
50 | [10, 6],
51 | [11, 6],
52 | [12, 6],
53 | [12, 7],
54 | [12, 8],
55 | [12, 9],
56 | [12, 10],
57 | [12, 11],
58 | [12, 12],
59 | [12, 13],
60 | [12, 14],
61 | [13, 14],
62 | [14, 14],
63 | [15, 14],
64 | [16, 14],
65 | [16, 13],
66 | ];
67 | expect(getNextCoordIndex(8, 16, path)).toEqual(1);
68 | });
69 |
70 | test('advance a coordinate', () => {
71 | expect(advanceCoord(1.48, 2, 0.22)).toEqual(1.7);
72 | expect(advanceCoord(1.99, 2, 0.22)).toEqual(2.0);
73 | });
74 |
75 | test('count turns', () => {
76 | let section = [
77 | [0, 0],
78 | [1, 0],
79 | [2, 0],
80 | [3, 0],
81 | [4, 0],
82 | [5, 0],
83 | ];
84 | expect(countTurns(section)).toEqual(0);
85 |
86 | section = [
87 | [0, 0],
88 | [1, 0],
89 | [1, 1],
90 | ];
91 | expect(countTurns(section)).toEqual(1);
92 |
93 | section = [
94 | [0, 0],
95 | [1, 0],
96 | [1, 1],
97 | [2, 1],
98 | [2, 2],
99 | ];
100 | expect(countTurns(section)).toEqual(3);
101 |
102 | section = [
103 | [0, 0],
104 | [1, 0],
105 | [1, 1],
106 | [2, 1],
107 | [2, 2],
108 | [3, 2],
109 | [4, 2],
110 | [5, 2],
111 | [6, 2],
112 | [6, 3],
113 | ];
114 | expect(countTurns(section)).toEqual(5);
115 | });
116 |
117 | test('get direction', () => {
118 | expect(
119 | getDirection(
120 | [
121 | [0, 0],
122 | [1, 0],
123 | ],
124 | 1
125 | )
126 | ).toEqual('x');
127 | expect(
128 | getDirection(
129 | [
130 | [0, 1],
131 | [0, 2],
132 | ],
133 | 1
134 | )
135 | ).toEqual('y');
136 | });
137 |
138 | test('get rotation', () => {
139 | expect(
140 | getRotation(
141 | [
142 | [0, 1],
143 | [0, 0],
144 | ],
145 | 1
146 | )
147 | ).toEqual(0);
148 | expect(
149 | getRotation(
150 | [
151 | [0, 0],
152 | [1, 0],
153 | ],
154 | 1
155 | )
156 | ).toEqual(90);
157 | expect(
158 | getRotation(
159 | [
160 | [0, 0],
161 | [0, 1],
162 | ],
163 | 1
164 | )
165 | ).toEqual(180);
166 | expect(
167 | getRotation(
168 | [
169 | [1, 0],
170 | [0, 0],
171 | ],
172 | 1
173 | )
174 | ).toEqual(270);
175 | });
176 |
177 | test('get turn distance', () => {
178 | expect(getTurnDistance(90, 0)).toEqual({
179 | distClockwise: 270,
180 | distCounterclockwise: 90,
181 | });
182 | expect(getTurnDistance(90, 180)).toEqual({
183 | distClockwise: 90,
184 | distCounterclockwise: 270,
185 | });
186 | expect(getTurnDistance(90, 180)).toEqual({
187 | distClockwise: 90,
188 | distCounterclockwise: 270,
189 | });
190 | expect(getTurnDistance(180, 0)).toEqual({
191 | distClockwise: 180,
192 | distCounterclockwise: 180,
193 | });
194 | expect(getTurnDistance(180, 360)).toEqual({
195 | distClockwise: 180,
196 | distCounterclockwise: 180,
197 | });
198 | expect(getTurnDistance(0, 90)).toEqual({
199 | distClockwise: 90,
200 | distCounterclockwise: 270,
201 | });
202 | expect(getTurnDistance(360, 90)).toEqual({
203 | distClockwise: 90,
204 | distCounterclockwise: 270,
205 | });
206 | expect(getTurnDistance(270, 90)).toEqual({
207 | distClockwise: 180,
208 | distCounterclockwise: 180,
209 | });
210 | expect(getTurnDistance(270, 0)).toEqual({
211 | distClockwise: 90,
212 | distCounterclockwise: 270,
213 | });
214 | expect(getTurnDistance(270, 360)).toEqual({
215 | distClockwise: 90,
216 | distCounterclockwise: 270,
217 | });
218 | });
219 |
--------------------------------------------------------------------------------
/frontend/src/Car.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CarIcon } from './Icons';
3 | import { wait } from '../../shared/utils';
4 | import {
5 | advanceCoord,
6 | countTurns,
7 | getNextCoordIndex,
8 | getRotation,
9 | getTurnDistance,
10 | } from './movement';
11 | import config from '../../shared/config';
12 |
13 | const {
14 | squareSize,
15 | fetchInterval,
16 | refreshInterval,
17 | turnDuration,
18 | animationOverhead,
19 | } = config;
20 |
21 | interface Props {
22 | driverId: string;
23 | path: [number, number][];
24 | actual: [number, number];
25 | }
26 |
27 | interface State {
28 | position: [number, number];
29 | rotation: number;
30 | }
31 |
32 | export default class Car extends React.Component {
33 | private latestUpdateAt = 0;
34 | private rotateBusy = false;
35 | private moveBusy = false;
36 |
37 | constructor(props: Props) {
38 | super(props);
39 | const { path, actual } = props;
40 |
41 | let rotation = 0;
42 | if (path.length > 1) {
43 | let pathIndex = path.findIndex(([x, y]) => {
44 | return x === actual[0] && y === actual[1];
45 | });
46 | if (pathIndex === 0) pathIndex = 1;
47 | rotation = getRotation(path, pathIndex);
48 | }
49 |
50 | this.state = {
51 | position: actual,
52 | rotation,
53 | };
54 | }
55 |
56 | async rotate(section: [number, number][], i: number) {
57 | this.rotateBusy = true;
58 |
59 | let rotation = this.state.rotation;
60 | const targetRotation = getRotation(section, i);
61 | if (this.state.rotation === targetRotation)
62 | return (this.rotateBusy = false);
63 |
64 | const { distClockwise, distCounterclockwise } = getTurnDistance(
65 | rotation,
66 | targetRotation
67 | );
68 | const isClockwise = distClockwise < distCounterclockwise;
69 |
70 | const diff = Math.min(distClockwise, distCounterclockwise);
71 | const steps = turnDuration / refreshInterval;
72 | const increment = diff / steps;
73 |
74 | while (this.state.rotation !== targetRotation) {
75 | if (isClockwise) rotation += increment;
76 | else rotation -= increment;
77 |
78 | if (rotation > 360) rotation = 0;
79 | else if (rotation < 0) rotation = 360 - Math.abs(rotation);
80 |
81 | this.setState({ rotation });
82 | await wait(refreshInterval);
83 | }
84 |
85 | this.rotateBusy = false;
86 | }
87 |
88 | async move(
89 | actual: [number, number],
90 | path: [number, number][],
91 | receivedAt: number
92 | ) {
93 | while (this.moveBusy) {
94 | await wait(100);
95 | if (receivedAt !== this.latestUpdateAt) return;
96 | }
97 |
98 | this.moveBusy = true;
99 |
100 | const { position } = this.state;
101 | let [currX, currY] = position;
102 |
103 | const startIndex = getNextCoordIndex(currX, currY, path);
104 | const endIndex = path.findIndex(([x, y]) => {
105 | return x === actual[0] && y === actual[1];
106 | });
107 |
108 | const section = path.slice(startIndex, endIndex + 1);
109 | if (section.length < 2) return (this.moveBusy = false);
110 | const turnCount = countTurns(section);
111 | const turnsDuration = turnCount * turnDuration;
112 |
113 | const distance = endIndex - startIndex + Math.max(currX % 1, currY % 1);
114 | const steps =
115 | (fetchInterval - turnsDuration - animationOverhead) / refreshInterval;
116 | const increment = distance / steps;
117 |
118 | for (let i = 0; i < section.length; i++) {
119 | if (i > 0) {
120 | while (this.rotateBusy) {
121 | await wait(refreshInterval);
122 | }
123 | await this.rotate(section, i);
124 | }
125 |
126 | const [nextX, nextY] = section[i];
127 | while (currX !== nextX) {
128 | currX = advanceCoord(currX, nextX, increment);
129 |
130 | this.setState({ position: [currX, this.state.position[1]] });
131 | await wait(refreshInterval);
132 | }
133 |
134 | while (currY !== nextY) {
135 | currY = advanceCoord(currY, nextY, increment);
136 |
137 | this.setState({ position: [this.state.position[0], currY] });
138 | await wait(refreshInterval);
139 | }
140 | }
141 |
142 | this.moveBusy = false;
143 | }
144 |
145 | componentDidUpdate(prevProps: Props) {
146 | if (prevProps.actual === this.props.actual) return;
147 |
148 | const receivedAt = Date.now();
149 | this.latestUpdateAt = receivedAt;
150 | this.move(this.props.actual, this.props.path, receivedAt);
151 | }
152 |
153 | render() {
154 | const { position, rotation } = this.state;
155 |
156 | const [x, y] = position;
157 |
158 | return (
159 |
164 | );
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/simulation/dist/methods.test.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"methods.test.js","sourceRoot":"","sources":["../src/methods.test.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/simulation/src/methods.ts:
--------------------------------------------------------------------------------
1 | import obstacles from '../../shared/obstacles.js';
2 | import { getRandomInt } from '../../shared/utils.js';
3 | import config from '../../shared/config.js';
4 | import { getObstaclesMap } from '../../shared/methods.js';
5 | import { Coord, CoordPair, Graph, Path } from './types.js';
6 |
7 | const { gridCount } = config;
8 |
9 | interface Cache {
10 | graph: Graph | null;
11 | }
12 | const cache: Cache = { graph: null };
13 |
14 | export const getRoadNodes = (): CoordPair[] => {
15 | const obstaclesMap = getObstaclesMap(obstacles);
16 |
17 | const roadNodes: CoordPair[] = [];
18 | for (let x = 0; x < gridCount; x++) {
19 | for (let y = 0; y < gridCount; y++) {
20 | if (!obstaclesMap.get(`${x}:${y}`)) {
21 | roadNodes.push([x, y]);
22 | }
23 | }
24 | }
25 |
26 | return roadNodes.filter(([x, y]: CoordPair) => {
27 | return x !== 0 && x !== gridCount - 1 && y !== 0 && y !== gridCount - 1;
28 | });
29 | };
30 |
31 | export const buildGraph = (
32 | obstaclesMap: Map,
33 | gridCount: number
34 | ): Graph => {
35 | const graph: Graph = [];
36 | for (let y = 0; y < gridCount; y++) {
37 | graph[y] = [];
38 |
39 | for (let x = 0; x < gridCount; x++) {
40 | if (obstaclesMap.get(`${x}:${y}`)) graph[y][x] = 0;
41 | else graph[y][x] = 1;
42 | }
43 | }
44 |
45 | return graph;
46 | };
47 |
48 | export const getGraph = (): Graph => {
49 | if (cache.graph) return cache.graph;
50 | cache.graph = buildGraph(getObstaclesMap(obstacles), gridCount);
51 | return cache.graph;
52 | };
53 |
54 | export const getDestinationRange = (coord: number): [number, number] =>
55 | coord < gridCount / 2
56 | ? [gridCount / 2 + Math.floor(coord / 2), gridCount]
57 | : [0, gridCount / 2 - Math.floor((gridCount - coord) / 2)];
58 |
59 | export const getClosestRoadNode = (
60 | x: number,
61 | y: number,
62 | graph: Graph = getGraph()
63 | ): CoordPair => {
64 | const isValid = (y, x) =>
65 | y >= 0 && y < graph.length && x >= 0 && x < graph[y].length;
66 |
67 | if (isValid(y, x) && graph[y][x] === 1) return [x, y];
68 |
69 | const directions = [
70 | [0, -1],
71 | [1, 0],
72 | [0, 1],
73 | [-1, 0],
74 | ];
75 |
76 | let queue = [[y, x]];
77 | const seen = new Set([`${y}:${x}`]);
78 |
79 | while (queue.length) {
80 | const nextQueue = [];
81 |
82 | for (let i = 0; i < queue.length; i++) {
83 | const [y, x] = queue[i];
84 |
85 | for (const [dx, dy] of directions) {
86 | const nextY = y + dy;
87 | const nextX = x + dx;
88 |
89 | if (isValid(nextY, nextX) && !seen.has(`${nextY}:${nextX}`)) {
90 | if (graph[nextY][nextX] === 1) {
91 | return [nextX, nextY];
92 | }
93 | seen.add(`${nextY}:${nextX}`);
94 | nextQueue.push([nextY, nextX]);
95 | }
96 | }
97 | }
98 | queue = nextQueue;
99 | }
100 | };
101 |
102 | export const generateDestination = (coordPair: CoordPair): CoordPair => {
103 | const [startX, startY] = coordPair;
104 | const rangeX = getDestinationRange(startX);
105 | const rangeY = getDestinationRange(startY);
106 |
107 | const destX = getRandomInt(rangeX[0], rangeX[1]);
108 | const destY = getRandomInt(rangeY[0], rangeY[1]);
109 |
110 | let destination = getClosestRoadNode(destX, destY);
111 |
112 | return destination;
113 | };
114 |
115 | export const getStraightLineDistance = (
116 | coordsA: CoordPair,
117 | coordsB: CoordPair
118 | ): number => {
119 | const [xA, yA] = coordsA;
120 | const [xB, yB] = coordsB;
121 | return Math.sqrt(Math.pow(xB - xA, 2) + Math.pow(yB - yA, 2));
122 | };
123 |
124 | export const getShortestPath = (
125 | startingPosition: CoordPair,
126 | destination: CoordPair,
127 | graph: Graph = getGraph()
128 | ): Path => {
129 | const isValid = (y, x) =>
130 | y >= 0 &&
131 | y < graph.length &&
132 | x >= 0 &&
133 | x < graph[y].length &&
134 | graph[y][x] === 1;
135 |
136 | const directions: [number, number][] = [
137 | [1, 0],
138 | [-1, 0],
139 | [0, 1],
140 | [0, -1],
141 | ];
142 |
143 | const [col, row]: CoordPair = startingPosition;
144 | let queue: [Coord, Coord, CoordPair[]][] = [[row, col, [startingPosition]]];
145 | const seen = new Set([`${row}:${col}`]);
146 |
147 | while (queue.length) {
148 | const nextQueue = [];
149 | for (let i = 0; i < queue.length; i++) {
150 | const [row, col, currPath] = queue[i];
151 |
152 | if (row === destination[1] && col === destination[0]) {
153 | return currPath;
154 | }
155 |
156 | for (let j = 0; j < directions.length; j++) {
157 | const [dx, dy]: [number, number] = directions[j];
158 |
159 | const nextRow = row + dy;
160 | const nextCol = col + dx;
161 |
162 | if (isValid(nextRow, nextCol) && !seen.has(`${nextRow}:${nextCol}`)) {
163 | seen.add(`${nextRow}:${nextCol}`);
164 | nextQueue.push([nextRow, nextCol, [...currPath, [nextCol, nextRow]]]);
165 | }
166 | }
167 | }
168 | queue = nextQueue;
169 | }
170 | };
171 |
--------------------------------------------------------------------------------
/simulation/dist/methods.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"methods.js","sourceRoot":"","sources":["../src/methods.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/frontend/src/Tour.tsx:
--------------------------------------------------------------------------------
1 | import Link from './Link';
2 | import { useTour } from '@reactour/tour';
3 | import { useNavigate } from 'react-router-dom';
4 |
5 | export const TourStart = ({ setStep }) => {
6 | const { setIsOpen } = useTour();
7 | // @ts-ignore
8 | window.reactour = { setIsOpen };
9 | const redirect = useNavigate();
10 |
11 | const startTour = () => {
12 | setStep(0);
13 | redirect('/map');
14 | setIsOpen(true);
15 | };
16 |
17 | return (
18 |
22 |
23 |
34 |
35 |
36 |
37 |
WALK THROUGH
38 |
39 |
40 | );
41 | };
42 |
43 | export const steps = [
44 | {
45 | selector: "[data-tour='map']",
46 | content: (
47 | <>
48 |
49 | The map visualizes the real-time state of the system. The server
50 | handles all calculations, including location updates, pick-ups,
51 | drop-offs, and customer requests. The client polls for the data in
52 | short intervals.
53 |
54 | {/*
55 | The road network is implemented as a graph using an adjacency matrix.
56 | Breadth-First Search is used to find the shortest paths.
57 |
*/}
58 |
59 | The grey paths lead from the driver to the customer. The{' '}
60 | black paths lead to the final destinations.
61 |
62 |
63 |
67 |
68 |
72 |
73 |
77 |
78 |
79 | >
80 | ),
81 | highlightedSelectors: ["[data-tour='map']"],
82 | },
83 | {
84 | selector: "[data-tour='list']",
85 | content: (
86 | <>
87 |
88 | Customers are matched to the nearest available drivers. To keep the
89 | simulation performant, expensive calculations such as this one are
90 | offloaded to parallel processes.
91 |
92 |
93 |
97 |
98 |
102 |
103 |
107 |
108 |
109 | >
110 | ),
111 | highlightedSelectors: ["[data-tour='list']"],
112 | },
113 | {
114 | selector: "[data-tour='monitor']",
115 | content: (
116 | <>
117 |
118 | The system is fully containerized with Docker and
119 | observable using Prometheus and{' '}
120 | Grafana . This setup proved useful when diagnosing
121 | various memory issues and concurrency bugs.
122 |
123 |
124 |
128 |
129 |
130 | >
131 | ),
132 | highlightedSelectors: ["[data-tour='monitor']"],
133 | },
134 | {
135 | selector: "[data-tour='docs']",
136 | content: (
137 | <>
138 |
139 | The documentation explains the architecture of the system and gives
140 | reasoning for the technical choices I've made.
141 |
142 |
143 | window.reactour.setIsOpen(0)}
147 | >
148 | Finish
149 |
150 |
151 | >
152 | ),
153 | highlightedSelectors: ["[data-tour='docs']"],
154 | },
155 | ];
156 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | db "app/db"
5 | "encoding/json"
6 | "log"
7 | "net/http"
8 | "net/http/httputil"
9 | "net/url"
10 | "os"
11 | "path/filepath"
12 | "strings"
13 | "time"
14 |
15 | "github.com/gorilla/mux"
16 | "github.com/joho/godotenv"
17 | )
18 |
19 | type Driver struct {
20 | Id string `json:"id"`
21 | DriverId string `json:"driverId"`
22 | Name string `json:"name"`
23 | Status string `json:"status"`
24 | Location string `json:"location"`
25 | Path string `json:"path"`
26 | PathIndex int `json:"pathIndex"`
27 | CustomerId string `json:"customerId"`
28 | CustomerName string `json:"customerName"`
29 | }
30 |
31 | type Customer struct {
32 | Id string `json:"id"`
33 | CustomerId string `json:"customerId"`
34 | Name string `json:"name"`
35 | Active bool `json:"active"`
36 | Location string `json:"location"`
37 | Destination string `json:"destination"`
38 | DriverId string `json:"driverId"`
39 | }
40 |
41 | func driversHandler(w http.ResponseWriter, req *http.Request) {
42 | rows, err := db.Connection.Query(`
43 | SELECT
44 | id,
45 | driver_id,
46 | name,
47 | status,
48 | location,
49 | path,
50 | path_index,
51 | customer_id,
52 | customer_name
53 | FROM drivers
54 | `,
55 | )
56 | if err != nil {
57 | http.Error(w, "Failed to get drivers: "+err.Error(), http.StatusInternalServerError)
58 | return
59 | }
60 | defer rows.Close()
61 |
62 | var drivers []Driver
63 |
64 | for rows.Next() {
65 | var driver Driver
66 | rows.Scan(
67 | &driver.Id,
68 | &driver.DriverId,
69 | &driver.Name,
70 | &driver.Status,
71 | &driver.Location,
72 | &driver.Path,
73 | &driver.PathIndex,
74 | &driver.CustomerId,
75 | &driver.CustomerName,
76 | )
77 | drivers = append(drivers, driver)
78 | }
79 |
80 | ridesBytes, _ := json.MarshalIndent(drivers, "", "\t")
81 |
82 | w.Header().Set("Content-Type", "application/json")
83 | w.Write(ridesBytes)
84 | }
85 |
86 | func customersHandler(w http.ResponseWriter, req *http.Request) {
87 | rows, err := db.Connection.Query(`
88 | SELECT
89 | id,
90 | customer_id,
91 | name,
92 | active,
93 | location,
94 | destination,
95 | driver_id
96 | FROM customers WHERE
97 | active = true AND
98 | driver_id IS NULL AND
99 | (location IS NOT NULL AND location != 'null')
100 | `,
101 | )
102 | if err != nil {
103 | http.Error(w, "Failed to get customers: "+err.Error(), http.StatusInternalServerError)
104 | return
105 | }
106 | defer rows.Close()
107 |
108 | var customers []Customer
109 |
110 | for rows.Next() {
111 | var customer Customer
112 | rows.Scan(
113 | &customer.Id,
114 | &customer.CustomerId,
115 | &customer.Name,
116 | &customer.Active,
117 | &customer.Location,
118 | &customer.Destination,
119 | &customer.DriverId,
120 | )
121 | customers = append(customers, customer)
122 | }
123 |
124 | ridesBytes, _ := json.MarshalIndent(customers, "", "\t")
125 |
126 | w.Header().Set("Content-Type", "application/json")
127 | w.Write(ridesBytes)
128 | }
129 |
130 | func getGrafanaHandler() func(w http.ResponseWriter, r *http.Request) {
131 | grafanaURL, _ := url.Parse("http://" + os.Getenv("SERVER_IP") + ":3000")
132 | grafanaProxy := httputil.NewSingleHostReverseProxy(grafanaURL)
133 |
134 | return func(w http.ResponseWriter, r *http.Request) {
135 | r.URL.Host = grafanaURL.Host
136 | r.URL.Scheme = grafanaURL.Scheme
137 | r.Header.Set("X-Forwarded-Host", r.Header.Get("Host"))
138 | r.Header.Set("Host", r.URL.Host)
139 | r.Header.Set("Origin", "http://"+os.Getenv("SERVER_IP"))
140 | r.Host = grafanaURL.Host
141 |
142 | // Modify the incoming request URL to remove the "/grafana" prefix
143 | r.URL.Path = strings.TrimPrefix(r.URL.Path, "/grafana")
144 |
145 | grafanaProxy.ServeHTTP(w, r)
146 | }
147 | }
148 |
149 | func spaHandler(w http.ResponseWriter, r *http.Request) {
150 | staticPath := "../frontend/build"
151 | indexPath := "index.html"
152 | path, err := filepath.Abs(r.URL.Path)
153 | if err != nil {
154 | http.Error(w, err.Error(), http.StatusBadRequest)
155 | return
156 | }
157 |
158 | path = filepath.Join(staticPath, path)
159 |
160 | _, err = os.Stat(path)
161 | if os.IsNotExist(err) {
162 | http.ServeFile(w, r, filepath.Join(staticPath, indexPath))
163 | return
164 | } else if err != nil {
165 | http.Error(w, err.Error(), http.StatusInternalServerError)
166 | return
167 | }
168 |
169 | http.FileServer(http.Dir(staticPath)).ServeHTTP(w, r)
170 | }
171 |
172 | func main() {
173 | err := godotenv.Load("../.env")
174 | if err != nil {
175 | log.Fatal(err)
176 | }
177 |
178 | db.InitDB()
179 | defer db.Connection.Close()
180 |
181 | router := mux.NewRouter()
182 |
183 | router.HandleFunc("/api/drivers", driversHandler)
184 | router.HandleFunc("/api/customers", customersHandler)
185 | router.HandleFunc("/grafana/{subpath:.*}", getGrafanaHandler())
186 | router.PathPrefix("/").Handler(http.HandlerFunc(spaHandler))
187 |
188 | serverEnv := os.Getenv("SERVER_ENV")
189 | if serverEnv == "DEV" {
190 | srv := &http.Server{
191 | Handler: router,
192 | Addr: ":8080",
193 | WriteTimeout: 15 * time.Second,
194 | ReadTimeout: 15 * time.Second,
195 | }
196 |
197 | log.Fatal(srv.ListenAndServe())
198 | } else if serverEnv == "PROD" {
199 | srv := &http.Server{
200 | Handler: router,
201 | Addr: ":443",
202 | WriteTimeout: 15 * time.Second,
203 | ReadTimeout: 15 * time.Second,
204 | }
205 |
206 | log.Fatal(srv.ListenAndServeTLS(
207 | "/etc/letsencrypt/live/rides.jurajmajerik.com/fullchain.pem",
208 | "/etc/letsencrypt/live/rides.jurajmajerik.com/privkey.pem",
209 | ))
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/simulation/dist/paths.js:
--------------------------------------------------------------------------------
1 | const paths = [
2 | {
3 | carId: 'car1',
4 | selected: 'first',
5 | i: 0,
6 | first: [
7 | [8, 17],
8 | [8, 16],
9 | [8, 15],
10 | [8, 14],
11 | [7, 14],
12 | [6, 14],
13 | [6, 13],
14 | [6, 12],
15 | [6, 11],
16 | [6, 10],
17 | [6, 9],
18 | [6, 8],
19 | [6, 7],
20 | [6, 6],
21 | [7, 6],
22 | [8, 6],
23 | [9, 6],
24 | [10, 6],
25 | [11, 6],
26 | [12, 6],
27 | [12, 7],
28 | [12, 8],
29 | [12, 9],
30 | [12, 10],
31 | [12, 11],
32 | [12, 12],
33 | [12, 13],
34 | [12, 14],
35 | [13, 14],
36 | [14, 14],
37 | [15, 14],
38 | [16, 14],
39 | [16, 13],
40 | [16, 12],
41 | ],
42 | second: [
43 | [16, 12],
44 | [16, 11],
45 | [17, 11],
46 | [18, 11],
47 | [19, 11],
48 | [20, 11],
49 | [21, 11],
50 | [22, 11],
51 | [23, 11],
52 | [24, 11],
53 | [25, 11],
54 | [26, 11],
55 | [26, 12],
56 | [26, 13],
57 | [26, 14],
58 | [26, 15],
59 | [26, 16],
60 | [26, 17],
61 | [26, 18],
62 | [26, 19],
63 | [26, 20],
64 | [26, 21],
65 | [26, 22],
66 | [25, 22],
67 | [24, 22],
68 | [23, 22],
69 | [22, 22],
70 | [21, 22],
71 | [20, 22],
72 | [20, 23],
73 | [20, 24],
74 | [19, 24],
75 | [18, 24],
76 | [17, 24],
77 | [16, 24],
78 | [15, 24],
79 | [14, 24],
80 | [13, 24],
81 | [12, 24],
82 | [11, 24],
83 | [10, 24],
84 | [9, 24],
85 | [8, 24],
86 | [8, 23],
87 | [8, 22],
88 | [8, 21],
89 | [8, 20],
90 | [8, 19],
91 | [8, 18],
92 | [8, 17],
93 | ],
94 | },
95 | {
96 | carId: 'car2',
97 | selected: 'first',
98 | i: 0,
99 | first: [
100 | [32, 31],
101 | [33, 31],
102 | [34, 31],
103 | [35, 31],
104 | [36, 31],
105 | [37, 31],
106 | [38, 31],
107 | [39, 31],
108 | [39, 32],
109 | [39, 33],
110 | [39, 34],
111 | [39, 35],
112 | [40, 35],
113 | [41, 35],
114 | [42, 35],
115 | [43, 35],
116 | [44, 35],
117 | [44, 36],
118 | [44, 37],
119 | [44, 38],
120 | [44, 39],
121 | [44, 40],
122 | [44, 41],
123 | [44, 42],
124 | [43, 42],
125 | [42, 42],
126 | [41, 42],
127 | [40, 42],
128 | [39, 42],
129 | [38, 42],
130 | [37, 42],
131 | [36, 42],
132 | [35, 42],
133 | [34, 42],
134 | [33, 42],
135 | [33, 41],
136 | [33, 40],
137 | [33, 39],
138 | [32, 39],
139 | [31, 39],
140 | [31, 38],
141 | [30, 38],
142 | [29, 38],
143 | [28, 38],
144 | ],
145 | second: [
146 | [28, 38],
147 | [27, 38],
148 | [26, 38],
149 | [25, 38],
150 | [25, 39],
151 | [24, 39],
152 | [23, 39],
153 | [22, 39],
154 | [21, 39],
155 | [20, 39],
156 | [20, 38],
157 | [20, 37],
158 | [20, 36],
159 | [20, 35],
160 | [20, 34],
161 | [20, 33],
162 | [20, 32],
163 | [20, 31],
164 | [21, 31],
165 | [22, 31],
166 | [23, 31],
167 | [24, 31],
168 | [25, 31],
169 | [26, 31],
170 | [27, 31],
171 | [28, 31],
172 | [29, 31],
173 | [30, 31],
174 | [31, 31],
175 | [32, 31],
176 | ],
177 | },
178 | {
179 | carId: 'car3',
180 | i: 0,
181 | selected: 'first',
182 | first: [
183 | [9, 38],
184 | [8, 38],
185 | [8, 37],
186 | [8, 36],
187 | [8, 35],
188 | [8, 34],
189 | [8, 33],
190 | [8, 32],
191 | [8, 31],
192 | [8, 30],
193 | [8, 29],
194 | [8, 28],
195 | [8, 27],
196 | [8, 26],
197 | [9, 26],
198 | [10, 26],
199 | [11, 26],
200 | [12, 26],
201 | [13, 26],
202 | [14, 26],
203 | [15, 26],
204 | [16, 26],
205 | [16, 27],
206 | [16, 28],
207 | [16, 29],
208 | [16, 30],
209 | [16, 31],
210 | ],
211 | second: [
212 | [16, 31],
213 | [16, 32],
214 | [16, 33],
215 | [16, 34],
216 | [16, 35],
217 | [16, 36],
218 | [16, 37],
219 | [16, 38],
220 | [16, 39],
221 | [16, 40],
222 | [15, 40],
223 | [14, 40],
224 | [13, 40],
225 | [12, 40],
226 | [11, 40],
227 | [10, 40],
228 | [9, 40],
229 | [9, 39],
230 | [9, 38],
231 | ],
232 | },
233 | ];
234 | export default paths;
235 | //# sourceMappingURL=paths.js.map
--------------------------------------------------------------------------------
/simulation/dist/paths.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,MAAM,KAAK,GAAG;IACZ;QACE,KAAK,EAAE,MAAM;QACb,QAAQ,EAAE,OAAO;QACjB,CAAC,EAAE,CAAC;QACJ,KAAK,EAAE;YACL,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,CAAC,EAAE,CAAC,CAAC;YACN,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;SACT;QACD,MAAM,EAAE;YACN,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;SACR;KACF;IACD;QACE,KAAK,EAAE,MAAM;QACb,QAAQ,EAAE,OAAO;QACjB,CAAC,EAAE,CAAC;QACJ,KAAK,EAAE;YACL,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;SACT;QACD,MAAM,EAAE;YACN,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;SACT;KACF;IACD;QACE,KAAK,EAAE,MAAM;QACb,CAAC,EAAE,CAAC;QACJ,QAAQ,EAAE,OAAO;QACjB,KAAK,EAAE;YACL,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;SACT;QACD,MAAM,EAAE;YACN,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,EAAE,EAAE,EAAE,CAAC;YACR,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;YACP,CAAC,CAAC,EAAE,EAAE,CAAC;SACR;KACF;CACF,CAAC;AACF,eAAe,KAAK,CAAC"}
--------------------------------------------------------------------------------
/simulation/src/Driver.ts:
--------------------------------------------------------------------------------
1 | import g from './global.js';
2 | import { wait, getRandomInt, decide } from '../../shared/utils.js';
3 | import { CoordPair, Path } from './types.js';
4 | import config from '../../shared/config.js';
5 | import firstNames from '../../shared/firstNames.js';
6 | import lastNames from '../../shared/lastNames.js';
7 |
8 | const { maxActiveDrivers } = config;
9 |
10 | export default class Driver {
11 | private busy = false;
12 |
13 | public driverId: string;
14 | public name: string;
15 | public active = false;
16 | private status: 'idle' | 'pickup' | 'enroute' = 'idle';
17 | public location: CoordPair | null = null;
18 | private customerId: string | null = null;
19 | private customerName: string | null = null;
20 | private customerLocation: CoordPair | null = null;
21 | private path: Path | null = null;
22 | private pathIndex: number | null = null;
23 |
24 | constructor({ driverId }: { driverId: string }) {
25 | this.driverId = driverId;
26 | this.name = `${firstNames[getRandomInt(0, firstNames.length - 1)]} ${
27 | lastNames[getRandomInt(0, lastNames.length - 1)]
28 | }`;
29 | this.location = g.roadNodes[getRandomInt(0, g.roadNodes.length - 1)];
30 |
31 | this.handleDispatcherResult = this.handleDispatcherResult.bind(this);
32 | this.handleRoutePlannerResult = this.handleRoutePlannerResult.bind(this);
33 |
34 | this.updateDB();
35 | this.simulate();
36 | }
37 |
38 | private async updateDB(): Promise {
39 | try {
40 | g.db.query(
41 | `
42 | INSERT INTO drivers (driver_id, name, status, location, path, path_index, customer_id, customer_name)
43 | VALUES (
44 | '${this.driverId}',
45 | '${this.name}',
46 | '${this.status}',
47 | '${this.location[0]}:${this.location[1]}',
48 | ${this.path ? `'${JSON.stringify(this.path)}'` : null},
49 | ${this.pathIndex ? `'${this.pathIndex}'` : null},
50 | ${this.customerId ? `'${this.customerId}'` : null},
51 | ${this.customerName ? `'${this.customerName}'` : null}
52 | )
53 | ON CONFLICT (driver_id)
54 | DO UPDATE SET
55 | name = EXCLUDED.name,
56 | status = EXCLUDED.status,
57 | location = EXCLUDED.location,
58 | path = EXCLUDED.path,
59 | path_index = EXCLUDED.path_index,
60 | customer_id = EXCLUDED.customer_id,
61 | customer_name = EXCLUDED.customer_name
62 | `
63 | );
64 | } catch (error) {
65 | console.error(error);
66 | }
67 |
68 | return;
69 | }
70 |
71 | isDestinationReached(): boolean {
72 | const { path, location } = this;
73 | return (
74 | location[0] === path[path.length - 1][0] &&
75 | location[1] === path[path.length - 1][1]
76 | );
77 | }
78 |
79 | requestMatch() {
80 | g.dispatcher.send({
81 | from: 'driver',
82 | data: {
83 | driverId: this.driverId,
84 | name: this.name,
85 | location: this.location,
86 | },
87 | });
88 | }
89 |
90 | requestRoute(destination) {
91 | g.routePlanner.send({
92 | driverId: this.driverId,
93 | startingPosition: this.location,
94 | destination,
95 | });
96 | }
97 |
98 | private simulate = async (): Promise => {
99 | while (true) {
100 | await wait(200);
101 |
102 | if (!this.busy) {
103 | if (!this.active) {
104 | if (g.activeDrivers.size < maxActiveDrivers) {
105 | let newActive = false;
106 | newActive = decide(5);
107 | if (newActive) {
108 | this.active = true;
109 | g.activeDrivers.add(this.driverId);
110 | this.name = `${
111 | firstNames[getRandomInt(0, firstNames.length - 1)]
112 | } ${lastNames[getRandomInt(0, lastNames.length - 1)]}`;
113 | this.updateDB();
114 | }
115 | }
116 | } else if (this.active && !this.customerId) {
117 | // Match with a customer
118 | this.busy = true;
119 | this.requestMatch();
120 | } else if (this.active && this.customerId && !this.path) {
121 | // Request path to the customer
122 | this.busy = true;
123 | this.requestRoute(this.customerLocation);
124 | } else if (this.active && this.path && !this.isDestinationReached()) {
125 | // Move to next location on the path
126 | this.pathIndex++;
127 | this.location = this.path[this.pathIndex];
128 |
129 | this.updateDB();
130 | } else if (this.active && this.path && this.isDestinationReached()) {
131 | if (this.status === 'pickup') {
132 | // Customer reached, request route towards customer's destination
133 | await wait(3000);
134 |
135 | this.busy = true;
136 | const customerDestination =
137 | g.customerInstances[this.customerId].destination;
138 | this.requestRoute(customerDestination);
139 | } else if (this.status === 'enroute') {
140 | // Customer's destination reached, reset state, deactivate customer
141 | await wait(3000);
142 |
143 | g.customerInstances[this.customerId].deactivate();
144 |
145 | this.status = 'idle';
146 | this.customerId = null;
147 | this.customerLocation = null;
148 | this.customerName = null;
149 | this.path = null;
150 | this.pathIndex = null;
151 | this.updateDB();
152 |
153 | // Decide whether to stay active
154 | const newActive = decide(50);
155 | this.active = newActive;
156 |
157 | if (!newActive) g.activeDrivers.delete(this.driverId);
158 |
159 | await wait(2000);
160 | }
161 | }
162 | }
163 | }
164 | };
165 |
166 | public handleDispatcherResult(
167 | customerId: string,
168 | customerName: string,
169 | customerLocation: CoordPair
170 | ): void {
171 | this.customerId = customerId;
172 | this.customerName = customerName;
173 | this.customerLocation = customerLocation;
174 | this.updateDB();
175 | this.busy = false;
176 | }
177 |
178 | public handleRoutePlannerResult(path: Path): void {
179 | let newStatus;
180 | if (this.status === 'idle') newStatus = 'pickup';
181 | else if (this.status === 'pickup') newStatus = 'enroute';
182 |
183 | this.status = newStatus;
184 |
185 | this.path = path;
186 | this.pathIndex = 0;
187 | this.updateDB();
188 | this.busy = false;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/simulation/dist/Driver.js:
--------------------------------------------------------------------------------
1 | import g from './global.js';
2 | import { wait, getRandomInt, decide } from '../../shared/utils.js';
3 | import config from '../../shared/config.js';
4 | import firstNames from '../../shared/firstNames.js';
5 | import lastNames from '../../shared/lastNames.js';
6 | const { maxActiveDrivers } = config;
7 | export default class Driver {
8 | constructor({ driverId }) {
9 | this.busy = false;
10 | this.active = false;
11 | this.status = 'idle';
12 | this.location = null;
13 | this.customerId = null;
14 | this.customerLocation = null;
15 | this.path = null;
16 | this.pathIndex = null;
17 | this.simulate = async () => {
18 | while (true) {
19 | await wait(200);
20 | if (!this.busy) {
21 | if (!this.active) {
22 | if (g.activeDrivers.size < maxActiveDrivers) {
23 | let newActive = false;
24 | newActive = decide(5);
25 | if (newActive) {
26 | this.active = true;
27 | g.activeDrivers.add(this.driverId);
28 | this.name = `${firstNames[getRandomInt(0, firstNames.length - 1)]} ${lastNames[getRandomInt(0, lastNames.length - 1)]}`;
29 | this.updateDB();
30 | }
31 | }
32 | }
33 | else if (this.active && !this.customerId) {
34 | // Match with a customer
35 | this.busy = true;
36 | this.requestMatch();
37 | }
38 | else if (this.active && this.customerId && !this.path) {
39 | // Request path to the customer
40 | this.busy = true;
41 | this.requestRoute(this.customerLocation);
42 | }
43 | else if (this.active && this.path && !this.isDestinationReached()) {
44 | // Move to next location on the path
45 | this.pathIndex++;
46 | this.location = this.path[this.pathIndex];
47 | this.updateDB();
48 | }
49 | else if (this.active && this.path && this.isDestinationReached()) {
50 | if (this.status === 'pickup') {
51 | // Customer reached, request route towards customer's destination
52 | await wait(3000);
53 | this.busy = true;
54 | const customerDestination = g.customerInstances[this.customerId].destination;
55 | this.requestRoute(customerDestination);
56 | }
57 | else if (this.status === 'enroute') {
58 | // Customer's destination reached, reset state, deactivate customer
59 | await wait(3000);
60 | g.customerInstances[this.customerId].deactivate();
61 | this.status = 'idle';
62 | this.customerId = null;
63 | this.path = null;
64 | this.pathIndex = null;
65 | this.updateDB();
66 | // Decide whether to stay active
67 | const newActive = decide(50);
68 | this.active = newActive;
69 | if (!newActive)
70 | g.activeDrivers.delete(this.driverId);
71 | await wait(2000);
72 | }
73 | }
74 | }
75 | }
76 | };
77 | this.driverId = driverId;
78 | this.location = g.roadNodes[getRandomInt(0, g.roadNodes.length - 1)];
79 | this.handleDispatcherResult = this.handleDispatcherResult.bind(this);
80 | this.handleRoutePlannerResult = this.handleRoutePlannerResult.bind(this);
81 | this.updateDB();
82 | this.simulate();
83 | }
84 | async updateDB() {
85 | try {
86 | g.db.query(`
87 | INSERT INTO drivers (driver_id, name, status, location, path, path_index, customer_id)
88 | VALUES (
89 | '${this.driverId}',
90 | '${this.name}',
91 | '${this.status}',
92 | '${this.location[0]}:${this.location[1]}',
93 | ${this.path ? `'${JSON.stringify(this.path)}'` : null},
94 | ${this.pathIndex ? `'${this.pathIndex}'` : null},
95 | ${this.customerId ? `'${this.customerId}'` : null}
96 | )
97 | ON CONFLICT (driver_id)
98 | DO UPDATE SET
99 | name = EXCLUDED.name,
100 | status = EXCLUDED.status,
101 | location = EXCLUDED.location,
102 | path = EXCLUDED.path,
103 | path_index = EXCLUDED.path_index,
104 | customer_id = EXCLUDED.customer_id
105 | `);
106 | }
107 | catch (error) {
108 | console.error(error);
109 | }
110 | return;
111 | }
112 | isDestinationReached() {
113 | const { path, location } = this;
114 | return (location[0] === path[path.length - 1][0] &&
115 | location[1] === path[path.length - 1][1]);
116 | }
117 | requestMatch() {
118 | g.dispatcher.send({
119 | from: 'driver',
120 | data: {
121 | driverId: this.driverId,
122 | name: this.name,
123 | location: this.location,
124 | },
125 | });
126 | }
127 | requestRoute(destination) {
128 | g.routePlanner.send({
129 | driverId: this.driverId,
130 | startingPosition: this.location,
131 | destination,
132 | });
133 | }
134 | handleDispatcherResult(customerId, customerLocation) {
135 | this.customerId = customerId;
136 | this.customerLocation = customerLocation;
137 | this.updateDB();
138 | this.busy = false;
139 | }
140 | handleRoutePlannerResult(path) {
141 | let newStatus;
142 | if (this.status === 'idle')
143 | newStatus = 'pickup';
144 | else if (this.status === 'pickup')
145 | newStatus = 'enroute';
146 | this.status = newStatus;
147 | this.path = path;
148 | this.pathIndex = 0;
149 | this.updateDB();
150 | this.busy = false;
151 | }
152 | }
153 | //# sourceMappingURL=Driver.js.map
--------------------------------------------------------------------------------
/frontend/src/GeoMap.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 | import Car from './Car';
3 | import ListItem from './ListItem';
4 | import { api } from './api';
5 | import { wait } from '../../shared/utils';
6 | import config from '../../shared/config';
7 | import { getObstaclesMap } from '../../shared/methods';
8 | import { MapCustomerIcon, MapDestIcon } from './Icons';
9 |
10 | const { gridSize, squareSize, fetchInterval } = config;
11 | const obstaclesMap = getObstaclesMap();
12 |
13 | const loadDrivers = async (previousUpdateAtRef, setCars, setRefreshing) => {
14 | while (true) {
15 | const drivers = await api.get('/drivers');
16 |
17 | const timeout = 2000;
18 | const now = Date.now();
19 | if (now - previousUpdateAtRef.current > timeout) {
20 | previousUpdateAtRef.current = now;
21 | setCars([]);
22 | setRefreshing(true);
23 | await wait(fetchInterval);
24 | continue;
25 | }
26 | previousUpdateAtRef.current = now;
27 |
28 | const cars = [];
29 | for (const driver of drivers) {
30 | const { location } = driver;
31 | let path = [];
32 | if (driver.path) path = JSON.parse(driver.path) as [number, number][];
33 | const [x, y] = location.split(':');
34 | cars.push({
35 | ...driver,
36 | actual: [parseInt(x), parseInt(y)],
37 | path,
38 | });
39 | }
40 |
41 | setCars(cars);
42 | setRefreshing(false);
43 | await wait(fetchInterval);
44 | }
45 | };
46 |
47 | const loadCustomers = async (setCustomers) => {
48 | while (true) {
49 | let customers = await api.get('/customers');
50 | customers = customers.filter((c) => {
51 | if (!c.location) console.log('no location', c);
52 | return c.location;
53 | });
54 | customers = customers.map((c) => {
55 | const { location } = c;
56 | const [x, y] = location.split(':');
57 | return { ...c, location: [parseInt(x), parseInt(y)] };
58 | });
59 | setCustomers(customers);
60 | await wait(fetchInterval);
61 | }
62 | };
63 |
64 | const GeoMap = () => {
65 | const previousUpdateAtRef = useRef(Date.now());
66 |
67 | const [cars, setCars] = useState([]);
68 | const [customers, setCustomers] = useState([]);
69 | const [refreshing, setRefreshing] = useState(false);
70 |
71 | useEffect(() => {
72 | previousUpdateAtRef.current = Date.now();
73 |
74 | loadDrivers(previousUpdateAtRef, setCars, setRefreshing);
75 | loadCustomers(setCustomers);
76 | }, []);
77 |
78 | const obstacleElems = [];
79 | for (let [key, color] of obstaclesMap) {
80 | const [x, y] = key.split(':');
81 | obstacleElems.push(
82 |
91 | );
92 | }
93 |
94 | const pathElems = cars.map(({ driverId, path, status }) => {
95 | let points = '';
96 |
97 | path.forEach(([x, y]) => {
98 | points += `${x * squareSize + squareSize / 4},${
99 | y * squareSize + squareSize / 4
100 | } `;
101 | });
102 |
103 | const first = path[0];
104 |
105 | return (
106 | <>
107 | {first && (
108 |
118 | )}
119 |
128 | >
129 | );
130 | });
131 |
132 | const carElems = cars.map(({ driverId, actual, path }) => {
133 | return (
134 |
140 | );
141 | });
142 |
143 | const seenCustomers = new Set();
144 | const customerElems = cars
145 | .filter(({ status }) => status === 'pickup')
146 | .map(({ path }) => {
147 | if (!path || path.length === 0) return null;
148 |
149 | const [x, y] = path[path.length - 1];
150 | seenCustomers.add(`${x}:${y}`);
151 | return (
152 |
157 | );
158 | });
159 |
160 | customers.forEach(({ location }) => {
161 | const [x, y] = location;
162 | if (seenCustomers.has(`${x}:${y}`)) return;
163 | customerElems.push(
164 |
169 | );
170 | });
171 |
172 | const destElems = cars
173 | .filter(({ status }) => status === 'enroute')
174 | .map(({ driverId, path }) => {
175 | const [x, y] = path[path.length - 1];
176 | return (
177 |
182 | );
183 | });
184 |
185 | const listElems = cars
186 | .filter(({ status }) => status === 'enroute' || status === 'pickup')
187 | .sort((a, b) => (a.name < b.name ? -1 : 0))
188 | .map(
189 | ({
190 | driverId,
191 | customerId,
192 | name,
193 | customerName,
194 | status,
195 | path,
196 | pathIndex,
197 | }) => {
198 | return (
199 |
208 | );
209 | }
210 | );
211 |
212 | return (
213 |
214 |
215 |
216 |
217 |
222 | {obstacleElems}
223 | {pathElems}
224 | {carElems}
225 | {customerElems}
226 | {destElems}
227 |
228 |
229 |
230 |
231 | {listElems}
232 |
233 |
234 | );
235 | };
236 | export default GeoMap;
237 |
--------------------------------------------------------------------------------