Added initial quazar implementation.

This commit is contained in:
Andrew Lalis 2022-05-20 18:40:44 +02:00
parent e45b942f34
commit ee165f6d8b
48 changed files with 10077 additions and 0 deletions

9
quasar-app/.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

6
quasar-app/.eslintignore Normal file
View File

@ -0,0 +1,6 @@
/dist
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.js

66
quasar-app/.eslintrc.js Normal file
View File

@ -0,0 +1,66 @@
module.exports = {
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
// This option interrupts the configuration hierarchy at this file
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
root: true,
parserOptions: {
ecmaVersion: '2021', // Allows for the parsing of modern ECMAScript features
},
env: {
node: true,
browser: true,
'vue/setup-compiler-macros': true
},
// Rules order is important, please avoid shuffling them
extends: [
// Base ESLint recommended rules
// 'eslint:recommended',
// Uncomment any of the lines below to choose desired strictness,
// but leave only one uncommented!
// See https://eslint.vuejs.org/rules/#available-rules
'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
// https://github.com/prettier/eslint-config-prettier#installation
// usage with Prettier, provided by 'eslint-config-prettier'.
'prettier'
],
plugins: [
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
// required to lint *.vue files
'vue',
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
// Prettier has not been included as plugin to avoid performance impact
// add it as an extension for your IDE
],
globals: {
ga: 'readonly', // Google Analytics
cordova: 'readonly',
__statics: 'readonly',
__QUASAR_SSR__: 'readonly',
__QUASAR_SSR_SERVER__: 'readonly',
__QUASAR_SSR_CLIENT__: 'readonly',
__QUASAR_SSR_PWA__: 'readonly',
process: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly'
},
// add your custom rules here
rules: {
'prefer-promise-reject-errors': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}

29
quasar-app/.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
.DS_Store
.thumbs.db
node_modules
# Quasar core related directories
.quasar
/dist
# Cordova related directories and files
/src-cordova/node_modules
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www
# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/node_modules
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln

41
quasar-app/README.md Normal file
View File

@ -0,0 +1,41 @@
# Rail Signal App (rail-signal)
App for the Rail Signal system.
## Install the dependencies
```bash
yarn
# or
npm install
```
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
quasar dev
```
### Lint the files
```bash
yarn lint
# or
npm run lint
```
### Format the files
```bash
yarn format
# or
npm run format
```
### Build the app for production
```bash
quasar build
```
### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).

16
quasar-app/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
<meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
<body>
<!-- quasar:entry-point -->
</body>
</html>

39
quasar-app/jsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": [
"src/*"
],
"app/*": [
"*"
],
"components/*": [
"src/components/*"
],
"layouts/*": [
"src/layouts/*"
],
"pages/*": [
"src/pages/*"
],
"assets/*": [
"src/assets/*"
],
"boot/*": [
"src/boot/*"
],
"stores/*": [
"src/stores/*"
],
"vue$": [
"node_modules/vue/dist/vue.runtime.esm-bundler.js"
]
}
},
"exclude": [
"dist",
".quasar",
"node_modules"
]
}

8019
quasar-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
quasar-app/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "rail-signal",
"version": "0.0.1",
"description": "App for the Rail Signal system.",
"productName": "Rail Signal App",
"author": "Andrew Lalis <andrewlalisofficial@gmail.com>",
"private": true,
"scripts": {
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test": "echo \"No test specified\" && exit 0"
},
"dependencies": {
"axios": "^0.21.1",
"pinia": "^2.0.11",
"@quasar/extras": "^1.0.0",
"quasar": "^2.6.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0"
},
"devDependencies": {
"eslint": "^8.10.0",
"eslint-plugin-vue": "^8.5.0",
"eslint-config-prettier": "^8.1.0",
"prettier": "^2.5.1",
"@quasar/app-vite": "^1.0.0",
"autoprefixer": "^10.4.2"
},
"engines": {
"node": "^18 || ^16 || ^14.19",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
}

View File

@ -0,0 +1,27 @@
/* eslint-disable */
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
plugins: [
// https://github.com/postcss/autoprefixer
require('autoprefixer')({
overrideBrowserslist: [
'last 4 Chrome versions',
'last 4 Firefox versions',
'last 4 Edge versions',
'last 4 Safari versions',
'last 4 Android versions',
'last 4 ChromeAndroid versions',
'last 4 FirefoxAndroid versions',
'last 4 iOS versions'
]
})
// https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then
// 1. yarn/npm install postcss-rtlcss
// 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line:
// require('postcss-rtlcss')
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

231
quasar-app/quasar.config.js Normal file
View File

@ -0,0 +1,231 @@
/* eslint-env node */
/*
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
* the ES6 features that are supported by your Node version. https://node.green/
*/
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { configure } = require('quasar/wrappers');
const { Notify } = require("quasar");
module.exports = configure(function (ctx) {
return {
eslint: {
// fix: true,
// include = [],
// exclude = [],
// rawOptions = {},
warnings: true,
errors: true
},
// https://v2.quasar.dev/quasar-cli/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: [
'axios',
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: [
'app.scss'
],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
],
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: [ 'es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1' ],
node: 'node16'
},
vueRouterMode: 'history', // available values: 'hash', 'history'
// vueRouterBase: '/rail-systems',
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
env: {
API_URL: ctx.dev ? "http://localhost:8080/api" : "http://localhost:8080/api",
WS_URL: ctx.dev ? "ws://localhost:8080/api/ws/app" : "ws://localhost:8080/api/ws/app"
},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
// vitePlugins: [
// [ 'package-name', { ..options.. } ]
// ]
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
open: true // opens browser window automatically
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: {
config: {
brand: {
primary: '#29733c',
secondary: '#7b9651',
accent: '#a38234',
dark: '#072e14',
positive: '#22ba64',
negative: '#a64a1c',
info: '#43a180',
warning: '#a88c40'
}
},
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: [
"Notify",
"Dialog"
]
},
// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
// sourceFiles: {
// rootComponent: 'src/App.vue',
// router: 'src/router/index',
// store: 'src/store/index',
// registerServiceWorker: 'src-pwa/register-service-worker',
// serviceWorker: 'src-pwa/custom-service-worker',
// pwaManifestFile: 'src-pwa/manifest.json',
// electronMain: 'src-electron/electron-main',
// electronPreload: 'src-electron/electron-preload'
// },
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
ssr: {
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
// will mess up SSR
// extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {},
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
middlewares: [
'render' // keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
pwa: {
workboxMode: 'generateSW', // or 'injectManifest'
injectPwaMetaTags: true,
swFilename: 'sw.js',
manifestFilename: 'manifest.json',
useCredentialsForManifestTag: false,
// extendGenerateSWOptions (cfg) {}
// extendInjectManifestOptions (cfg) {},
// extendManifestJson (json) {}
// extendPWACustomSWConf (esbuildConf) {}
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf)
// extendElectronPreloadConf (esbuildConf)
inspectPort: 5858,
bundler: 'packager', // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: 'rail-signal'
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
contentScripts: [
'my-content-script'
],
// extendBexScriptsConf (esbuildConf) {}
// extendBexManifestJson (json) {}
}
}
});

11
quasar-app/src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<router-view />
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App'
})
</script>

View File

@ -0,0 +1,101 @@
import axios from "axios";
import {API_URL} from "./constants";
export function refreshComponents(rs) {
return new Promise((resolve, reject) => {
axios.get(`${API_URL}/rs/${rs.id}/c`)
.then(response => {
const previousSelectedComponentId = rs.selectedComponent ? rs.selectedComponent.id : null;
rs.components = response.data;
if (previousSelectedComponentId !== null) {
const previousComponent = rs.components.find(c => c.id === previousSelectedComponentId);
if (previousComponent) {
rs.selectedComponent = previousComponent;
} else {
rs.selectedComponent = null;
}
} else {
rs.selectedComponent = null;
}
resolve();
})
.catch(reject);
});
}
export function refreshSomeComponents(rs, components) {
const promises = [];
for (let i = 0; i < components.length; i++) {
const promise = new Promise((resolve, reject) => {
axios.get(`${API_URL}/rs/${rs.id}/c/${components[i].id}`)
.then(resp => {
const idx = rs.components.findIndex(c => c.id === resp.data.id);
if (idx > -1) rs.components[idx] = resp.data;
resolve();
})
.catch(reject);
});
promises.push(promise);
}
return Promise.all(promises);
}
export function getComponent(rs, id) {
return new Promise((resolve, reject) => {
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${id}`)
.then(response => resolve(response.data))
.catch(reject);
});
}
/**
* Searches through the rail system's components.
* @param {RailSystem} rs
* @param {string|null} searchQuery
* @return {Promise<Object>}
*/
export function searchComponents(rs, searchQuery) {
return new Promise((resolve, reject) => {
const params = {
page: 0,
size: 25
};
if (searchQuery) params.q = searchQuery;
axios.get(`${API_URL}/rs/${rs.id}/c/search`, {params: params})
.then(response => {
resolve(response.data);
})
.catch(reject);
});
}
export function createComponent(rs, data) {
return new Promise((resolve, reject) => {
axios.post(`${API_URL}/rs/${rs.id}/c`, data)
.then(response => {
const newComponentId = response.data.id;
refreshComponents(rs)
.then(() => {
const newComponent = rs.components.find(c => c.id === newComponentId);
if (newComponent) {
rs.selectedComponent = newComponent;
}
resolve();
})
.catch(reject);
})
.catch(reject);
});
}
export function removeComponent(rs, id) {
return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${rs.id}/c/${id}`)
.then(() => {
refreshComponents(rs)
.then(resolve)
.catch(reject);
})
.catch(reject);
});
}

View File

@ -0,0 +1,2 @@
export const API_URL = process.env.API_URL;
export const WS_URL = process.env.WS_URL;

View File

@ -0,0 +1,57 @@
import {API_URL} from "./constants";
import axios from "axios";
/**
* A token that's used by components to provide real-time up and down links.
*/
export class LinkToken {
constructor(data) {
this.id = data.id;
this.label = data.label;
this.components = data.components;
}
}
/**
* Gets the list of link tokens in a rail system.
* @param {RailSystem} rs
* @return {Promise<LinkToken[]>}
*/
export function getTokens(rs) {
return new Promise((resolve, reject) => {
axios.get(`${API_URL}/rs/${rs.id}/lt`)
.then(response => {
resolve(response.data.map(obj => new LinkToken(obj)));
})
.catch(reject);
});
}
/**
* Creates a new link token.
* @param {RailSystem} rs
* @param {LinkToken} data
* @return {Promise<string>} A promise that resolves to the token that was created.
*/
export function createLinkToken(rs, data) {
return new Promise((resolve, reject) => {
axios.post(`${API_URL}/rs/${rs.id}/lt`, data)
.then(response => {
resolve(response.data.token);
})
.catch(reject);
});
}
/**
* Deletes a link token.
* @param {RailSystem} rs
* @param {Number} tokenId
*/
export function deleteToken(rs, tokenId) {
return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${rs.id}/lt/${tokenId}`)
.then(resolve)
.catch(reject);
});
}

View File

@ -0,0 +1,64 @@
import axios from "axios";
import {API_URL} from "./constants";
import {refreshSomeComponents} from "./components";
/**
* Updates the connections to a path node.
* @param {RailSystem} rs The rail system to which the node belongs.
* @param {Object} node The node to update.
* @returns {Promise}
*/
export function updateConnections(rs, node) {
return new Promise((resolve, reject) => {
axios.patch(
`${API_URL}/rs/${rs.id}/c/${node.id}/connectedNodes`,
node
)
.then(response => {
node.connectedNodes = response.data.connectedNodes;
resolve();
})
.catch(reject);
});
}
/**
* Adds a connection to a path node.
* @param {RailSystem} rs
* @param {Object} node
* @param {Object} other
* @returns {Promise}
*/
export function addConnection(rs, node, other) {
node.connectedNodes.push(other);
return updateConnections(rs, node);
}
/**
* Removes a connection from a path node.
* @param {RailSystem} rs
* @param {Object} node
* @param {Object} other
* @returns {Promise}
*/
export function removeConnection(rs, node, other) {
const idx = node.connectedNodes.findIndex(n => n.id === other.id);
return new Promise((resolve, reject) => {
if (idx > -1) {
node.connectedNodes.splice(idx, 1);
updateConnections(rs, node)
.then(() => {
const nodes = [];
nodes.push(...node.connectedNodes);
nodes.push(other);
refreshSomeComponents(rs, nodes)
.then(resolve)
.catch(reject);
})
.catch(reject);
} else {
resolve();
}
});
}

View File

@ -0,0 +1,62 @@
import axios from "axios";
import {API_URL} from "./constants";
import { refreshSegments } from "src/api/segments";
import { refreshComponents } from "src/api/components";
export class RailSystem {
constructor(data) {
this.id = data.id;
this.name = data.name;
this.segments = [];
this.components = [];
this.websocket = null;
this.selectedComponent = null;
}
}
export function refreshRailSystems(rsStore) {
return new Promise(resolve => {
axios.get(`${API_URL}/rs`)
.then(response => {
const rsItems = response.data;
rsStore.railSystems.length = 0;
for (let i = 0; i < rsItems.length; i++) {
rsStore.railSystems.push(new RailSystem(rsItems[i]));
}
resolve();
})
.catch(error => console.error(error));
})
}
export function refreshRailSystem(rs) {
const promises = [];
promises.push(refreshSegments(rs));
promises.push(refreshComponents(rs));
return Promise.all(promises);
}
export function createRailSystem(rsStore, name) {
return new Promise((resolve, reject) => {
axios.post(`${API_URL}/rs`, {name: name})
.then(response => {
const newId = response.data.id;
refreshRailSystems(rsStore)
.then(() => resolve(rsStore.railSystems.find(rs => rs.id === newId)))
.catch(error => reject(error));
})
.catch(error => reject(error));
});
}
export function removeRailSystem(rsStore, id) {
return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${id}`)
.then(() => {
rsStore.selectedRailSystem = null;
refreshRailSystems(rsStore)
.then(() => resolve)
.catch(error => reject(error));
});
});
}

View File

@ -0,0 +1,50 @@
import axios from "axios";
import {API_URL} from "./constants";
/**
* Fetches the set of segments for a rail system.
* @param {Number} rsId
* @returns {Promise<[Object]>}
*/
export function getSegments(rsId) {
return new Promise((resolve, reject) => {
axios.get(`${API_URL}/rs/${rsId}/s`)
.then(response => resolve(response.data))
.catch(error => reject(error));
});
}
export function refreshSegments(rs) {
return new Promise(resolve => {
getSegments(rs.id)
.then(segments => {
rs.segments = segments;
resolve();
})
.catch(error => console.error(error));
});
}
export function createSegment(rs, name) {
return new Promise((resolve, reject) => {
axios.post(`${API_URL}/rs/${rs.id}/s`, {name: name})
.then(() => {
refreshSegments(rs)
.then(() => resolve())
.catch(error => reject(error));
})
.catch(error => reject(error));
});
}
export function removeSegment(rs, segmentId) {
return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${rs.id}/s/${segmentId}`)
.then(() => {
refreshSegments(rs)
.then(() => resolve())
.catch(error => reject(error));
})
.catch(error => reject(error));
});
}

View File

@ -0,0 +1,39 @@
import {WS_URL} from "./constants";
/**
* Establishes a websocket connection to the given rail system.
* @param {RailSystem} rs
*/
export function establishWebsocketConnection(rs) {
closeWebsocketConnection(rs);
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
rs.websocket.onopen = () => {
console.log("Opened websocket connection to rail system " + rs.id);
};
rs.websocket.onclose = event => {
if (event.code !== 1000) {
console.warn("Lost websocket connection. Attempting to reestablish.");
setTimeout(() => {
establishWebsocketConnection(rs);
}, 3000);
}
console.log("Closed websocket connection to rail system " + rs.id);
};
rs.websocket.onmessage = msg => {
console.log(msg);
};
rs.websocket.onerror = error => {
console.log(error);
};
}
/**
* Closes the websocket connection to a rail system, if possible.
* @param {RailSystem} rs
*/
export function closeWebsocketConnection(rs) {
if (rs.websocket) {
rs.websocket.close();
rs.websocket = null;
}
}

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="256"
height="256"
viewBox="0 0 67.733332 67.733335"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="icon.svg"
inkscape:export-filename="/home/andrew/Programming/github-andrewlalis/RailSignalAPI/railsignal-app/src/assets/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="164.88542"
inkscape:cy="100.52253"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-229.26665)">
<rect
id="rect3713"
width="52.916668"
height="67.73333"
x="7.4083328"
y="229.26665"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
rx="13.229167" />
<rect
rx="12.30785"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.24762905"
y="231.24725"
x="9.250967"
height="63.772137"
width="49.2314"
id="rect4520" />
<circle
style="fill:#00d900;fill-opacity:1;stroke:none;stroke-width:0.65214598"
id="path4522"
cx="33.866665"
cy="248.70078"
r="10.364463" />
<circle
r="10.364463"
cy="277.56586"
cx="33.866665"
id="circle4524"
style="fill:#e71e00;fill-opacity:1;stroke:none;stroke-width:0.65214598" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

View File

@ -0,0 +1,24 @@
import { boot } from 'quasar/wrappers'
import axios from 'axios'
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({ baseURL: 'https://api.example.com' })
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
})
export { api }

View File

@ -0,0 +1,29 @@
<template>
<q-item
clickable
:to="'/rail-systems/' + railSystem.id"
>
<q-item-section>
<q-item-label>{{railSystem.name}}</q-item-label>
<q-item-label caption>Id: {{railSystem.id}}</q-item-label>
</q-item-section>
</q-item>
</template>
<script>
import { RailSystem } from "src/api/railSystems";
export default {
name: "RailSystemLink",
props: {
railSystem: {
type: RailSystem,
required: true
}
}
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="flex">
<div class="row full-width">
<div class="col-md-6">
<q-list>
<q-item
v-for="component in railSystem.components"
:key="component.id"
clickable
v-ripple
>
<q-item-section>
<q-item-label>{{component.name}}</q-item-label>
<q-item-label caption>Id: {{component.id}}</q-item-label>
</q-item-section>
<q-item-section side top>
<q-item-label caption>x: {{component.position.x}}, y: {{component.position.y}}, z: {{component.position.z}}</q-item-label>
<q-item-label caption>{{component.type}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</div>
<q-page-sticky position="bottom-right" :offset="[50, 18]">
<q-fab
icon="add"
direction="up"
color="accent"
>
<q-fab-action color="primary" label="Signal"/>
<q-fab-action color="primary" label="Switch"/>
<q-fab-action color="primary" label="Signal Boundary"/>
</q-fab>
</q-page-sticky>
</template>
<script>
import { RailSystem } from "src/api/railSystems";
export default {
name: "ComponentsView",
props: {
railSystem: {
type: RailSystem,
required: true
}
}
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="row bg-green-4">
<div class="col-md-8 bg-blue-2" id="railSystemMapCanvasContainer">
<canvas id="railSystemMapCanvas" height="800">
Your browser doesn't support canvas.
</canvas>
</div>
<div class="col-md-4 bg-amber">
<p>
Info panel
</p>
</div>
</div>
</template>
<script>
import { RailSystem } from "src/api/railSystems";
import { draw, initMap } from "src/render/mapRenderer";
export default {
name: "MapView",
props: {
railSystem: {
type: RailSystem,
required: true
}
},
mounted() {
initMap(this.railSystem);
},
updated() {
initMap(this.railSystem);
},
watch: {
railSystem: {
handler() {
draw();
},
deep: true
}
}
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="flex">
<div class="row full-width">
<div class="col-md-6">
<q-list>
<q-item
v-for="segment in railSystem.segments"
:key="segment.id"
clickable
v-ripple
>
<q-item-section>
<q-item-label>{{segment.name}}</q-item-label>
<q-item-label caption>Id: {{segment.id}}</q-item-label>
</q-item-section>
<q-menu touch-position context-menu>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="remove(segment)">
<q-item-section>Delete</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
</q-list>
</div>
<div class="col-md-6">
</div>
</div>
</div>
<q-page-sticky position="bottom-right" :offset="[18, 18]">
<q-btn fab icon="add" color="accent" @click="openAddSegmentDialog"></q-btn>
</q-page-sticky>
<q-dialog v-model="showAddSegmentDialog" persistent>
<q-card style="min-width: 400px">
<q-form @submit="onSubmit" @reset="onReset">
<q-card-section>
<div class="text-h6">Add Segment</div>
<p>
Add a new segment to the rail system. A segment can be thought of as
the basic building block of any rail system, and segments define a
section of rails that trains can travel in and out of, usually one at
a time.
</p>
</q-card-section>
<q-card-section>
<q-input
v-model="segmentName"
autofocus
label="Segment Name"
:rules="[value => value && value.length > 0]"
lazy-rules
type="text"
/>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" type="reset" @click="showAddSegmentDialog = false"/>
<q-btn flat label="Add Segment" type="submit"/>
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import { useRailSystemsStore } from "stores/railSystemsStore";
import { RailSystem } from "src/api/railSystems";
import { useQuasar } from "quasar";
import { createSegment, removeSegment } from "src/api/segments";
import { ref } from "vue";
export default {
name: "SegmentsView",
setup() {
const rsStore = useRailSystemsStore();
const quasar = useQuasar();
return {rsStore, quasar};
},
props: {
railSystem: {
type: RailSystem,
required: true
}
},
data() {
return {
showAddSegmentDialog: false,
segmentName: ""
}
},
methods: {
openAddSegmentDialog() {
this.showAddSegmentDialog = true;
},
onSubmit() {
createSegment(this.railSystem, this.segmentName)
.then(() => {
this.quasar.notify({
color: "positive",
message: "Segment created."
});
this.onReset();
this.showAddSegmentDialog = false;
})
.catch(error => {
console.log(error);
this.quasar.notify({
color: "negative",
message: "An error occurred: " + error.response.data.message
});
});
},
onReset() {
this.segmentName = "";
},
remove(segment) {
this.quasar.dialog({
title: "Confirm",
message: "Are you sure you want to remove this segment? This will remove any connected components, and it cannot be undone.",
cancel: true,
persistent: true
}).onOk(() => {
removeSegment(this.railSystem, segment.id)
.then(() => {
this.quasar.notify({
color: "positive",
message: "Segment " + segment.name + " has been removed."
});
})
.catch(error => {
this.quasar.notify({
color: "negative",
message: "An error occurred: " + error.response.data.message
});
});
});
}
}
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="q-pa-md">
</div>
</template>
<script>
export default {
name: "SelectedComponentView"
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,25 @@
<template>
<div>
<p>
Settings view. Nothing here yet.
</p>
</div>
</template>
<script>
import { RailSystem } from "src/api/railSystems";
export default {
name: "SettingsView",
props: {
railSystem: {
type: RailSystem,
required: true
}
}
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1 @@
// app global css in SCSS form

View File

@ -0,0 +1,24 @@
// Quasar SCSS (& Sass) Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
// Check documentation for full list of Quasar variables
// Your own variables (that are declared here) and Quasar's own
// ones will be available out of the box in your .vue/.scss/.sass files
// It's highly recommended to change the default colors
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary : #29733c;
$secondary : #7b9651;
$accent : #a38234;
$dark : #072e14;
$positive : #22ba64;
$negative : #a64a1c;
$info : #43a180;
$warning : #a88c40;

View File

@ -0,0 +1,74 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title>
Rail Signal
<span v-if="rsStore.selectedRailSystem">
- {{rsStore.selectedRailSystem.name}}
</span>
</q-toolbar-title>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen"
show-if-above
bordered
>
<q-list>
<q-item-label header>Rail Systems</q-item-label>
<rail-system-link
v-for="rs in rsStore.railSystems"
:key="rs.id"
:rail-system="rs"
/>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
import { defineComponent, ref } from "vue";
import RailSystemLink from "components/RailSystemLink.vue";
import { useRailSystemsStore } from "stores/railSystemsStore";
import { refreshRailSystems } from "src/api/railSystems";
export default defineComponent({
name: 'MainLayout',
components: {
RailSystemLink
},
setup () {
const rsStore = useRailSystemsStore()
const leftDrawerOpen = ref(false)
return {
rsStore,
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
},
created() {
refreshRailSystems(this.rsStore);
}
})
</script>

View File

@ -0,0 +1,31 @@
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ErrorNotFound'
})
</script>

View File

@ -0,0 +1,17 @@
<template>
<q-page>
<p>
This is the index page.
</p>
</q-page>
</template>
<script>
export default {
name: "IndexPage"
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,68 @@
<template>
<q-page>
<div v-if="railSystem">
<q-tabs
v-model="panel"
align="left"
active-bg-color="positive"
class="bg-secondary"
>
<q-tab name="map" label="Map"/>
<q-tab name="segments" label="Segments"/>
<q-tab name="components" label="Components"/>
<q-tab name="settings" label="Settings"/>
</q-tabs>
<q-tab-panels v-model="panel">
<q-tab-panel name="map">
<map-view :rail-system="railSystem"/>
</q-tab-panel>
<q-tab-panel name="segments">
<segments-view :rail-system="railSystem"/>
</q-tab-panel>
<q-tab-panel name="components">
<components-view :rail-system="railSystem"/>
</q-tab-panel>
<q-tab-panel name="settings">
<settings-view :rail-system="railSystem"/>
</q-tab-panel>
</q-tab-panels>
<router-view />
</div>
</q-page>
</template>
<script>
import { useRailSystemsStore } from "stores/railSystemsStore";
import MapView from "components/rs/MapView.vue";
import SegmentsView from "components/rs/SegmentsView.vue";
import ComponentsView from "components/rs/ComponentsView.vue";
import SettingsView from "components/rs/SettingsView.vue";
export default {
name: "RailSystemPage",
components: { SettingsView, ComponentsView, SegmentsView, MapView },
data() {
return {
panel: "map",
railSystem: null
}
},
async beforeRouteEnter(to, from, next) {
const id = parseInt(to.params.id);
const rsStore = useRailSystemsStore();
await rsStore.selectRailSystem(id);
next(vm => vm.railSystem = rsStore.selectedRailSystem);
},
async beforeRouteUpdate(to, from) {
const id = parseInt(to.params.id);
const rsStore = useRailSystemsStore();
await rsStore.selectRailSystem(id);
this.railSystem = rsStore.selectedRailSystem;
}
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,16 @@
export function roundedRect(ctx, x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
ctx.beginPath();
ctx.moveTo(x+r, y);
ctx.arcTo(x+w, y, x+w, y+h, r);
ctx.arcTo(x+w, y+h, x, y+h, r);
ctx.arcTo(x, y+h, x, y, r);
ctx.arcTo(x, y, x+w, y, r);
ctx.closePath();
}
export function circle(ctx, x, y, r) {
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
}

View File

@ -0,0 +1,132 @@
/*
Helper functions to actually perform rendering of different components.
*/
import {getScaleFactor, isComponentHovered} from "./mapRenderer";
import {roundedRect, circle} from "./canvasUtils";
export function drawComponent(ctx, worldTx, component) {
const tx = DOMMatrix.fromMatrix(worldTx);
tx.translateSelf(component.position.x, component.position.z, 0);
const s = getScaleFactor();
tx.scaleSelf(1/s, 1/s, 1/s);
tx.scaleSelf(20, 20, 20);
ctx.setTransform(tx);
if (component.type === "SIGNAL") {
drawSignal(ctx, component);
} else if (component.type === "SEGMENT_BOUNDARY") {
drawSegmentBoundary(ctx, component);
} else if (component.type === "SWITCH") {
drawSwitch(ctx, component);
}
ctx.setTransform(tx.translate(0.75, -0.75));
if (component.online !== undefined && component.online !== null) {
drawOnlineIndicator(ctx, component);
}
ctx.setTransform(tx);
// Draw hovered status.
if (isComponentHovered(component)) {
ctx.fillStyle = `rgba(255, 255, 0, 0.5)`;
circle(ctx, 0, 0, 0.75);
ctx.fill();
}
}
function drawSignal(ctx) {
roundedRect(ctx, -0.3, -0.5, 0.6, 1, 0.25);
ctx.fillStyle = "black";
ctx.fill();
ctx.fillStyle = "rgb(0, 255, 0)";
circle(ctx, 0, -0.2, 0.15);
ctx.fill();
}
function drawSegmentBoundary(ctx) {
ctx.fillStyle = `rgb(150, 58, 224)`;
ctx.beginPath();
ctx.moveTo(0, -0.5);
ctx.lineTo(-0.5, 0);
ctx.lineTo(0, 0.5);
ctx.lineTo(0.5, 0);
ctx.lineTo(0, -0.5);
ctx.fill();
}
function drawSwitch(ctx, sw) {
const colors = [
`rgba(61, 148, 66, 0.25)`,
`rgba(59, 22, 135, 0.25)`,
`rgba(145, 17, 90, 0.25)`,
`rgba(191, 49, 10, 0.25)`
];
ctx.lineWidth = 1;
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
const config = sw.possibleConfigurations[i];
ctx.strokeStyle = colors[i];
for (let j = 0; j < config.nodes.length; j++) {
const node = config.nodes[j];
const diff = {
x: sw.position.x - node.position.x,
y: sw.position.z - node.position.z,
};
const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2));
diff.x = 2 * -diff.x / mag;
diff.y = 2 * -diff.y / mag;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(diff.x, diff.y);
ctx.stroke();
}
}
ctx.fillStyle = `rgb(245, 188, 66)`;
ctx.strokeStyle = `rgb(245, 188, 66)`;
ctx.lineWidth = 0.2;
circle(ctx, 0, 0.3, 0.2);
ctx.fill();
circle(ctx, -0.3, -0.3, 0.2);
ctx.fill();
circle(ctx, 0.3, -0.3, 0.2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, 0.3);
ctx.lineTo(0, 0);
ctx.lineTo(0.3, -0.3);
ctx.moveTo(0, 0);
ctx.lineTo(-0.3, -0.3);
ctx.stroke();
}
function drawOnlineIndicator(ctx, component) {
ctx.lineWidth = 0.1;
if (component.online) {
ctx.fillStyle = `rgba(52, 174, 235, 128)`;
ctx.strokeStyle = `rgba(52, 174, 235, 128)`;
} else {
ctx.fillStyle = `rgba(153, 153, 153, 128)`;
ctx.strokeStyle = `rgba(153, 153, 153, 128)`;
}
ctx.beginPath();
ctx.arc(0, 0.2, 0.125, 0, Math.PI * 2);
ctx.fill();
for (let r = 0; r < 3; r++) {
ctx.beginPath();
ctx.arc(0, 0, 0.1 + 0.2 * r, 7 * Math.PI / 6, 11 * Math.PI / 6);
ctx.stroke();
}
}
export function drawConnectedNodes(ctx, worldTx, component) {
const s = getScaleFactor();
ctx.lineWidth = 5 / s;
ctx.strokeStyle = "black";
for (let i = 0; i < component.connectedNodes.length; i++) {
const node = component.connectedNodes[i];
ctx.beginPath();
ctx.moveTo(component.position.x, component.position.z);
ctx.lineTo(node.position.x, node.position.z);
ctx.stroke();
}
}

View File

@ -0,0 +1,248 @@
/*
This component is responsible for the rendering of a RailSystem in a 2d map
view.
*/
import {drawComponent, drawConnectedNodes} from "./drawing";
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
const SCALE_INDEX_NORMAL = 7;
const HOVER_RADIUS = 10;
let mapContainerDiv = null;
let mapCanvas = null;
let railSystem = null;
let mapScaleIndex = SCALE_INDEX_NORMAL;
let mapTranslation = {x: 0, y: 0};
let mapDragOrigin = null;
let mapDragTranslation = null;
let lastMousePoint = new DOMPoint(0, 0, 0, 0);
const hoveredElements = [];
export function initMap(rs) {
railSystem = rs;
console.log("Initializing map for rail system: " + rs.name);
hoveredElements.length = 0;
mapCanvas = document.getElementById("railSystemMapCanvas");
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
mapCanvas.removeEventListener("wheel", onMouseWheel);
mapCanvas.addEventListener("wheel", onMouseWheel);
mapCanvas.removeEventListener("mousedown", onMouseDown);
mapCanvas.addEventListener("mousedown", onMouseDown);
mapCanvas.removeEventListener("mouseup", onMouseUp);
mapCanvas.addEventListener("mouseup", onMouseUp);
mapCanvas.removeEventListener("mousemove", onMouseMove);
mapCanvas.addEventListener("mousemove", onMouseMove);
// Do an initial draw.
draw();
}
export function draw() {
if (!(mapCanvas && railSystem && railSystem.components)) {
console.warn("Attempted to draw map without canvas or railSystem.");
return;
}
const ctx = mapCanvas.getContext("2d");
if (mapCanvas.width !== mapContainerDiv.clientWidth) {
mapCanvas.width = mapContainerDiv.clientWidth;
}
if (mapCanvas.height !== mapContainerDiv.clientHeight) {
mapCanvas.height = mapContainerDiv.clientHeight - 6;
}
const width = mapCanvas.width;
const height = mapCanvas.height;
ctx.resetTransform();
ctx.fillStyle = `rgb(240, 240, 240)`;
ctx.fillRect(0, 0, width, height);
const worldTx = getWorldTransform();
ctx.setTransform(worldTx);
// Draw segments!
const segmentPoints = new Map();
railSystem.segments.forEach(segment => segmentPoints.set(segment.id, []));
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
if (c.type === "SEGMENT_BOUNDARY") {
for (let j = 0; j < c.segments.length; j++) {
segmentPoints.get(c.segments[j].id).push({x: c.position.x, y: c.position.z});
}
}
}
railSystem.segments.forEach(segment => {
const points = segmentPoints.get(segment.id);
const avgPoint = {x: 0, y: 0};
points.forEach(point => {
avgPoint.x += point.x;
avgPoint.y += point.y;
});
avgPoint.x /= points.length;
avgPoint.y /= points.length;
let r = 5;
points.forEach(point => {
const dist2 = Math.pow(avgPoint.x - point.x, 2) + Math.pow(avgPoint.y - point.y, 2);
if (dist2 > r * r) {
r = Math.sqrt(dist2);
}
});
ctx.fillStyle = `rgba(200, 200, 200, 0.25)`;
const p = worldPointToMap(new DOMPoint(avgPoint.x, avgPoint.y, 0, 0));
const s = getScaleFactor();
ctx.beginPath();
ctx.arc(p.x / s, p.y / s, r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.font = "3px Sans-Serif";
ctx.fillText(`${segment.name}`, p.x / s, p.y / s);
});
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
drawConnectedNodes(ctx, worldTx, c);
}
}
for (let i = 0; i < railSystem.components.length; i++) {
drawComponent(ctx, worldTx, railSystem.components[i]);
}
// Draw debug info.
ctx.resetTransform();
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
ctx.font = "10px Sans-Serif";
const lastWorldPoint = mapPointToWorld(lastMousePoint);
const lines = [
"Scale factor: " + getScaleFactor(),
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
`Components: ${railSystem.components.length}`,
`Hovered elements: ${hoveredElements.length}`
]
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 10, 20 + (i * 15));
}
}
export function getScaleFactor() {
return SCALE_VALUES[mapScaleIndex];
}
function getWorldTransform() {
const canvasRect = mapCanvas.getBoundingClientRect();
const scale = getScaleFactor();
const tx = new DOMMatrix();
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
tx.scaleSelf(scale, scale, scale);
tx.translateSelf(mapTranslation.x, mapTranslation.y, 0);
if (mapDragOrigin !== null && mapDragTranslation !== null) {
tx.translateSelf(mapDragTranslation.x, mapDragTranslation.y, 0);
}
return tx;
}
export function isComponentHovered(component) {
for (let i = 0; i < hoveredElements.length; i++) {
if (hoveredElements[i].id === component.id) return true;
}
return false;
}
/**
* Maps a point on the map coordinates to world coordinates.
* @param {DOMPoint} p
* @returns {DOMPoint}
*/
function mapPointToWorld(p) {
return getWorldTransform().invertSelf().transformPoint(p);
}
/**
* Maps a point in the world to map coordinates.
* @param {DOMPoint} p
* @returns {DOMPoint}
*/
function worldPointToMap(p) {
return getWorldTransform().transformPoint(p);
}
/*
EVENT HANDLING
*/
/**
* @param {WheelEvent} event
*/
function onMouseWheel(event) {
const s = event.deltaY;
if (s > 0) {
mapScaleIndex = Math.max(0, mapScaleIndex - 1);
} else if (s < 0) {
mapScaleIndex = Math.min(SCALE_VALUES.length - 1, mapScaleIndex + 1);
}
draw();
event.stopPropagation();
return false;
}
/**
* @param {MouseEvent} event
*/
function onMouseDown(event) {
const p = getMousePoint(event);
mapDragOrigin = {x: p.x, y: p.y};
}
function onMouseUp() {
if (mapDragTranslation !== null) {
mapTranslation.x += mapDragTranslation.x;
mapTranslation.y += mapDragTranslation.y;
}
if (hoveredElements.length === 1) {
railSystem.selectedComponent = hoveredElements[0];
} else {
railSystem.selectedComponent = null;
}
mapDragOrigin = null;
mapDragTranslation = null;
}
/**
* @param {MouseEvent} event
*/
function onMouseMove(event) {
const p = getMousePoint(event);
lastMousePoint = p;
if (mapDragOrigin !== null) {
const scale = getScaleFactor();
const dx = p.x - mapDragOrigin.x;
const dy = p.y - mapDragOrigin.y;
mapDragTranslation = {x: dx / scale, y: dy / scale};
} else {
hoveredElements.length = 0;
// Populate with list of hovered elements.
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
const componentPoint = new DOMPoint(c.position.x, c.position.z, 0, 1);
const mapComponentPoint = worldPointToMap(componentPoint);
const dist2 = (p.x - mapComponentPoint.x) * (p.x - mapComponentPoint.x) + (p.y - mapComponentPoint.y) * (p.y - mapComponentPoint.y);
if (dist2 < HOVER_RADIUS * HOVER_RADIUS) {
hoveredElements.push(c);
}
}
}
draw();
}
/**
* Gets the point at which the user clicked on the map.
* @param {MouseEvent} event
* @returns {DOMPoint}
*/
function getMousePoint(event) {
const rect = mapCanvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return new DOMPoint(x, y, 0, 1);
}

View File

@ -0,0 +1,30 @@
import { route } from 'quasar/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE)
})
return Router
})

View File

@ -0,0 +1,28 @@
import { useRailSystemsStore } from "stores/railSystemsStore";
const routes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{
path: '',
component: () => import('pages/IndexPage.vue')
},
{// Rail Systems page
path: 'rail-systems/:id',
component: () => import('pages/RailSystem.vue'),
props: true
}
]
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
]
export default routes

View File

@ -0,0 +1,20 @@
import { store } from 'quasar/wrappers'
import { createPinia } from 'pinia'
/*
* If not building with SSR mode, you can
* directly export the Store instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Store instance.
*/
export default store((/* { ssrContext } */) => {
const pinia = createPinia()
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})

View File

@ -0,0 +1,44 @@
import {defineStore} from "pinia";
import {refreshSegments} from "../api/segments"
import {refreshComponents} from "../api/components";
import {closeWebsocketConnection, establishWebsocketConnection} from "../api/websocket";
import { refreshRailSystems } from "src/api/railSystems";
export const useRailSystemsStore = defineStore('RailSystemsStore', {
state: () => ({
/**
* @type {RailSystem[]}
*/
railSystems: [],
/**
* @type {RailSystem | null}
*/
selectedRailSystem: null,
loaded: false
}),
actions: {
async selectRailSystem(rsId) {
if (!this.loaded) {
await refreshRailSystems(this);
}
this.railSystems.forEach(r => {
r.components.length = 0;
r.segments.length = 0;
closeWebsocketConnection(r);
});
if (!rsId) return;
const rs = this.railSystems.find(r => r.id === rsId);
await refreshSegments(rs);
await refreshComponents(rs);
establishWebsocketConnection(rs);
this.selectedRailSystem = rs;
}
},
getters: {
rsId() {
if (this.selectedRailSystem === null) return null;
return this.selectedRailSystem.id;
}
}
});

10
quasar-app/src/stores/store-flag.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
store: true;
}
}