Added initial quazar implementation.
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
/dist
|
||||||
|
/src-capacitor
|
||||||
|
/src-cordova
|
||||||
|
/.quasar
|
||||||
|
/node_modules
|
||||||
|
.eslintrc.js
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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).
|
|
@ -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>
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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')
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 422 B |
After Width: | Height: | Size: 840 B |
After Width: | Height: | Size: 15 KiB |
|
@ -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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'App'
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const API_URL = process.env.API_URL;
|
||||||
|
export const WS_URL = process.env.WS_URL;
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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));
|
||||||
|
});
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 |
|
@ -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 }
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "SelectedComponentView"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
||||||
|
// app global css in SCSS form
|
|
@ -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;
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
})
|
|
@ -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
|
|
@ -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
|
||||||
|
})
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|