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;
|
||||
}
|
||||
}
|