Compare commits
59 Commits
0.0.1-Snap
...
main
Author | SHA1 | Date |
---|---|---|
|
1cf5ed50fc | |
|
9b6eb7b667 | |
|
9c0d588543 | |
|
ad18c1b3d4 | |
|
ac7d040b5e | |
|
e90c267ec2 | |
|
feaff75121 | |
|
ebad42cf99 | |
|
99f438161f | |
|
cb8eed835a | |
|
3c9554ebba | |
|
72b4d0dda9 | |
|
bd6c87149a | |
|
c16627e7d3 | |
|
71ca7b4467 | |
|
2561c8f3d4 | |
|
9cb074b2ec | |
|
f67be6e3ee | |
|
f090e105dd | |
|
2f7e3ec20c | |
|
3c51204261 | |
|
b84f271a40 | |
|
637dee747d | |
|
d9a4ec31c4 | |
|
3b2797f259 | |
|
2771e934c9 | |
|
dc9ca7c2af | |
|
2a05e26d6d | |
|
e1a756ffc9 | |
|
f1bc3c7b0a | |
|
c0c036d223 | |
|
2cc3c2259a | |
|
6cfc630310 | |
|
b5bcdc12e1 | |
|
ee165f6d8b | |
|
e45b942f34 | |
|
ed3f6bd6b9 | |
|
4abd39cbe1 | |
|
08fb892cc5 | |
|
35c13d83bd | |
|
ecd9549e77 | |
|
1906111ab8 | |
|
e5165330d9 | |
|
74cf5736f0 | |
|
ba409985e5 | |
|
a02758ecd4 | |
|
3ac886feeb | |
|
e608e2ba8c | |
|
2fbc22af0d | |
|
e8cb22276a | |
|
d623812959 | |
|
91988c17b0 | |
|
fe92f2903c | |
|
e7274cb6d0 | |
|
fd2cf357dd | |
|
25e59cd92c | |
|
4546993f0f | |
|
6edb2e4912 | |
|
cbbf74ee4a |
|
@ -34,3 +34,8 @@ build/
|
|||
|
||||
*.mv.db
|
||||
*.trace.db
|
||||
|
||||
src/main/resources/app
|
||||
/build_system
|
||||
/log
|
||||
/github_token.properties
|
||||
|
|
26
README.md
|
@ -1,2 +1,24 @@
|
|||
# RailSignalAPI
|
||||
A simple API for tracking rail traffic in signalled blocks.
|
||||
# Rail Signal
|
||||
A comprehensive solution to tracking and managing your rail system, in real time.
|
||||
|
||||
## Setup
|
||||
To set up your own Rail Signal, system, you will need to follow the following steps:
|
||||
1. Download and run the Rail Signal API and web app. Go to the [releases](https://github.com/andrewlalis/RailSignalAPI/releases) page to download the latest release. Then, place the JAR file in your desired location, and run it with `java -jar rail-signal.jar`.
|
||||
2. Open the web app (http://localhost:8080 by default) and create a new rail system, and add components and segments to build your network. More information about this will be given later.
|
||||
3. Add components to your actual rail system, and install a driver script onto your device. For Minecraft rail systems using Immersive railroading and some computer mod, you can run `pastebin run jKyAiE8k` to run an installation script. *Please make an issue if you have a system for which there is not yet any available driver.*
|
||||
|
||||
## Development
|
||||
To work on and develop Rail Signal, you will need to run both the Java/Spring-Boot backend API, and the Vue/Quasar frontend app.
|
||||
|
||||
To start up the API, the project directory in IntelliJ (or the IDE of your choice), and run the `RailSignalApiApplication` main method.
|
||||
|
||||
To start up the app, open a terminal in the `quasar-app` directory, and run `quasar dev`.
|
||||
|
||||
### Building
|
||||
To build a complete API/app distributable JAR file, simply run the following:
|
||||
```
|
||||
./build_system.d
|
||||
```
|
||||
> Note: The build script requires the D language toolchain to be installed on your system. Also, you can compile `build_system.d` to a native executable to run the build script more efficiently.
|
||||
|
||||
This will produce a `rail-signal-api-XXX.jar` file in the `target` directory, which contains both the API, and the frontend app, packaged together so that the entire JAR can simply be run via `java -jar`.
|
|
@ -0,0 +1,166 @@
|
|||
<?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="1280"
|
||||
height="640"
|
||||
viewBox="0 0 338.66666 169.33334"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="banner.svg"
|
||||
inkscape:export-filename="/home/andrew/Pictures/railsignalbanner.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="-19.654761 : -18.142858 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="338.66666 : 84.66667 : 1"
|
||||
inkscape:persp3d-origin="169.33333 : 56.444447 : 1"
|
||||
id="perspective815" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.7"
|
||||
inkscape:cx="845.17179"
|
||||
inkscape:cy="205.3419"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
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,-127.66666)">
|
||||
<rect
|
||||
style="fill:#659a54;fill-opacity:1;stroke:none;stroke-width:12.70000076;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect870"
|
||||
width="338.66666"
|
||||
height="169.33333"
|
||||
x="0"
|
||||
y="127.66666" />
|
||||
<rect
|
||||
style="fill:#454e44;fill-opacity:1;stroke:none;stroke-width:12.70000076;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect911"
|
||||
width="338.66666"
|
||||
height="111.125"
|
||||
x="-1.110223e-16"
|
||||
y="185.875" />
|
||||
<g
|
||||
id="g852"
|
||||
transform="rotate(90,151.19047,228.96429)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path835"
|
||||
d="m 122.84226,146.37648 h 92.98215"
|
||||
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 122.84226,212.9003 h 92.98215"
|
||||
id="path837"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path839"
|
||||
d="m 122.84226,246.1622 h 92.98215"
|
||||
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 122.84226,179.63839 h 92.98215"
|
||||
id="path841"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path843"
|
||||
d="m 122.84226,279.42411 h 92.98215"
|
||||
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path831"
|
||||
d="M 148.35565,291.70833 V 129.93452"
|
||||
style="fill:none;stroke:#989898;stroke-width:8.46666718;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#989898;stroke-width:8.46666718;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 190.31102,291.70833 V 129.93452"
|
||||
id="path833"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<flowRoot
|
||||
xml:space="preserve"
|
||||
id="flowRoot913"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="matrix(0.57599614,0,0,0.57599614,-10.203222,101.60397)"><flowRegion
|
||||
id="flowRegion915"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"><rect
|
||||
id="rect917"
|
||||
width="1102.8572"
|
||||
height="162.85715"
|
||||
x="51.42857"
|
||||
y="34.285713"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></flowRegion><flowPara
|
||||
id="flowPara919"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:96px;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono Bold';stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">Rail Signal</flowPara></flowRoot> <g
|
||||
id="g933"
|
||||
transform="translate(123.59822)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path921"
|
||||
d="m 169.33333,245.97321 v 46.49107"
|
||||
style="fill:none;stroke:#000000;stroke-width:16.93333435;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
ry="2.6458333"
|
||||
rx="2.6458333"
|
||||
y="196.08035"
|
||||
x="152.13544"
|
||||
height="51.026794"
|
||||
width="34.395802"
|
||||
id="rect923"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#bababa;stroke-width:4.23333359;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="6.0476193"
|
||||
cy="212.33333"
|
||||
cx="169.33333"
|
||||
id="path925"
|
||||
style="fill:#1cd500;fill-opacity:1;stroke:none;stroke-width:8.46666718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#d50000;fill-opacity:1;stroke:none;stroke-width:8.46666718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle927"
|
||||
cx="169.33333"
|
||||
cy="228.5863"
|
||||
r="6.0476193" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.2 KiB |
|
@ -0,0 +1,173 @@
|
|||
#!/usr/bin/env dub
|
||||
/+ dub.sdl:
|
||||
dependency "dsh" version="~>1.6.1"
|
||||
dependency "dxml" version="~>0.4.3"
|
||||
dependency "requests" version="~>2.0.8"
|
||||
dependency "d-properties" version="~>1.0.4"
|
||||
+/
|
||||
|
||||
/**
|
||||
* This script will build the Rail Signal Vue app, then bundle it into this
|
||||
* Spring project's files under src/main/resources/app/, and will then build
|
||||
* this project into a jar file.
|
||||
*/
|
||||
module build_system;
|
||||
|
||||
import dsh;
|
||||
import dxml.dom;
|
||||
import dxml.util;
|
||||
|
||||
import std.stdio;
|
||||
import std.string;
|
||||
import std.algorithm;
|
||||
import std.uni;
|
||||
|
||||
const DIST = "./src/main/resources/app";
|
||||
const DIST_ORIGIN = "./quasar-app/dist/spa";
|
||||
const APP_DIR = "./quasar-app";
|
||||
const APP_BUILD = "quasar build -m spa";
|
||||
const API_BUILD = "mvn clean package spring-boot:repackage -DskipTests=true";
|
||||
|
||||
const LOG_DIR = "./log";
|
||||
const API_LOG = LOG_DIR ~ "/api_build.txt";
|
||||
const APP_LOG = LOG_DIR ~ "/app_build.txt";
|
||||
|
||||
const GITHUB_PROPS_FILE = "github_token.properties";
|
||||
|
||||
int main(string[] args) {
|
||||
string ver = getVersion();
|
||||
if (ver is null) {
|
||||
error("Could not determine version.");
|
||||
return 1;
|
||||
}
|
||||
removeIfExists(LOG_DIR);
|
||||
mkdir(LOG_DIR);
|
||||
print("Building Rail Signal v%s", ver);
|
||||
|
||||
if (args.length >= 2) {
|
||||
string command = args[1].strip.toLower;
|
||||
if (command == "app") {
|
||||
buildApp();
|
||||
} else if (command == "api") {
|
||||
buildApi(ver);
|
||||
} else if (command == "all") {
|
||||
buildApp();
|
||||
buildApi(ver);
|
||||
if (args.length >= 3 && args[2].strip.toLower == "release") {
|
||||
print("Are you sure you want to create a GitHub release for version %s?", ver);
|
||||
string response = readln().strip.toLower;
|
||||
if (response == "yes" || response == "y") {
|
||||
print("Please enter a short description for this release.");
|
||||
string description = readln().strip;
|
||||
createRelease(ver, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buildApp();
|
||||
buildApi(ver);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the production version of the frontend app and injects it into the
|
||||
* API's resources to serve statically.
|
||||
*/
|
||||
void buildApp() {
|
||||
chdir(APP_DIR);
|
||||
print("Building app...");
|
||||
runOrQuit(APP_BUILD, "." ~ APP_LOG); // Use an extra dot because we moved into app dir.
|
||||
print("Copying dist from %s to %s", DIST_ORIGIN, DIST);
|
||||
chdir("..");
|
||||
removeIfExists(DIST);
|
||||
mkdir(DIST);
|
||||
copyDir(DIST_ORIGIN, DIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the production version of the backend API.
|
||||
*/
|
||||
void buildApi(string ver) {
|
||||
print("Building API...");
|
||||
runOrQuit(API_BUILD, API_LOG);
|
||||
string[] jars = findFilesByExtension("target", ".jar", false);
|
||||
string jarFile = jars[0];
|
||||
string finalJarFile = "./target/rail-signal-" ~ ver ~ ".jar";
|
||||
// Clean up the jar file name.
|
||||
copy(jarFile, finalJarFile);
|
||||
print("Build complete. Created %s", finalJarFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the version of the system from the pom file.
|
||||
* Returns: The version string, or null if it couldn't be found.
|
||||
*/
|
||||
string getVersion() {
|
||||
auto data = parseDOM!simpleXML(readText("pom.xml"));
|
||||
auto root = data.children[0];
|
||||
foreach (child; root.children) {
|
||||
if (child.name == "version") {
|
||||
return child.children[0].text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new GitHub release using the specified version, and uploads the
|
||||
* JAR file to the release.
|
||||
* Params:
|
||||
* ver = The version.
|
||||
*/
|
||||
void createRelease(string ver, string description) {
|
||||
import d_properties;
|
||||
import requests;
|
||||
import std.json;
|
||||
|
||||
print("Creating release...");
|
||||
|
||||
JSONValue data = [
|
||||
"tag_name": "v" ~ ver,
|
||||
"name": "Rail Signal v" ~ ver,
|
||||
"body": description
|
||||
];
|
||||
data.object["prerelease"] = JSONValue(false);
|
||||
data.object["generate_release_notes"] = JSONValue(false);
|
||||
|
||||
auto rq = Request();
|
||||
auto props = Properties(GITHUB_PROPS_FILE);
|
||||
string username = props["username"];
|
||||
string token = props["token"];
|
||||
rq.authenticator = new BasicAuthentication(username, token);
|
||||
auto response = rq.post(
|
||||
"https://api.github.com/repos/andrewlalis/RailSignalAPI/releases",
|
||||
data.toString,
|
||||
"application/json"
|
||||
);
|
||||
if (response.code == 201) {
|
||||
string responseBody = cast(string) response.responseBody;
|
||||
JSONValue responseData = parseJSON(responseBody);
|
||||
print("Created release %s", responseData["url"].str);
|
||||
long releaseId = responseData["id"].integer;
|
||||
string uploadUrl = format!"https://uploads.github.com/repos/andrewlalis/RailSignalAPI/releases/%d/assets?name=%s"(
|
||||
releaseId,
|
||||
"rail-signal-" ~ ver ~ ".jar"
|
||||
);
|
||||
print("Uploading JAR file to %s", uploadUrl);
|
||||
auto f = File("./target/rail-signal-" ~ ver ~ ".jar", "rb");
|
||||
ulong assetSize = f.size();
|
||||
rq.addHeaders(["Content-Length": format!"%d"(assetSize)]);
|
||||
auto assetResponse = rq.post(uploadUrl, f.byChunk(4096));
|
||||
if (assetResponse.code == 201) {
|
||||
print("JAR file uploaded successfully.");
|
||||
} else {
|
||||
error("An error occurred while uploading the JAR file.");
|
||||
}
|
||||
} else {
|
||||
error("An error occurred while creating the release.");
|
||||
writeln(response.responseBody);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# Component Drivers
|
||||
This document describes the general methodology for writing component drivers that operate within your rail system to connect it to the online system's up- and down-link services.
|
||||
|
||||
The following types of components are supported by Rail Signal:
|
||||
- `SIGNAL`
|
||||
- `SEGMENT_BOUNDARY`
|
||||
- `SWITCH`
|
||||
|
||||
The following information is generally required for any driver to be able to connect to the system and operate nominally:
|
||||
- A valid link token
|
||||
- The base URL of the system's API. Usually `http://localhost:8080` or whatever you've configured it to be.
|
||||
- Live connection information. This differs depending on what type of communication your device supports.
|
||||
- For devices with **websocket** support, you will need the base URL of the websocket. Usually `ws://localhost:8080` or whatever you've configured your server to use.
|
||||
- For devices with **TCP socket** support, you will need the hostname and port of the system's server socket. By default, this is `localhost:8081`.
|
||||
|
||||
A device is not limited to a single component, but will act as a relay for all components linked to the device's link token. Generally, there is no limit to the number of components that a single device can manage, but more components will lead to more load on the device.
|
||||
|
||||
## Live Communication
|
||||
The main purpose of component drivers is to relay real-time messages between actual component devices, and their representations in the online rail system. While multiple types of communication are available, in general, all messages are sent as JSON UTF-8 encoded strings. Devices must present their link token when they initiate the connection.
|
||||
|
||||
If the link token is valid, the connection will be initiated, and the device will immediately receive a `COMPONENT_DATA` message for each component that the token is linked to.
|
||||
|
||||
### Websocket
|
||||
Websocket connections should be made to `{BASE_WS_URL}/api/ws/component?token={token}`, where `{BASE_WS_URL}` is the base websocket URL, such as `ws://localhost:8080`, and `{token}` is the device's link token.
|
||||
|
||||
- If the link token is missing or improperly formatted, a 400 Bad Request response is given.
|
||||
- If the link token is not correct or not active or otherwise set to reject connections, a 401 Unauthorized response is given.
|
||||
|
||||
### TCP Socket
|
||||
TCP socket connections should be made to the server's TCP socket address, which by default is `localhost:8081`. The device should immediately send 2 bytes indicating the length of its token string, followed by the token string's bytes. The client should expect to receive in response a *connect message*:
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"message": "Connection established."
|
||||
}
|
||||
```
|
||||
|
||||
- If the token is invalid, a `"valid": false` is returned and the server closes the socket.
|
||||
|
||||
Note: All messages sent via TCP are sent as JSON messages with a 2-byte length header.
|
||||
|
||||
## Components
|
||||
Each device should be designed to handle multiple independent components concurrently. The device may receive messages at any time pertaining to any of the components, and the device may send messages at any time, pertaining to any of the components. Messages are only sent regarding a single component.
|
||||
|
||||
Every component message should contain at least the following two properties:
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "COMPONENT_DATA",
|
||||
...
|
||||
}
|
||||
```
|
||||
`cId` is the id of the component that this message is about. `type` is the type of message. This defines what additional structure to expect.
|
||||
|
||||
All components may receive `COMPONENT_DATA` messages. For example, the following could be a message regarding a signal:
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "COMPONENT_DATA",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"position": {"x": 0, "y": 0, "z": 0},
|
||||
"name": "my-component",
|
||||
"type": "SIGNAL",
|
||||
"online": true,
|
||||
"segment": {
|
||||
"id": 4,
|
||||
"name": "my-segment",
|
||||
"occupied": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The following sections will provide more detail about the other types of messages that can be sent and received by the different components.
|
||||
|
||||
### Signal
|
||||
Signals display the status of a connected segment, and as such can only receive data. They will receive `SEGMENT_STATUS` messages:
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "SEGMENT_STATUS",
|
||||
"segmentId": 4,
|
||||
"occupied": true
|
||||
}
|
||||
```
|
||||
`sId` is the id of the segment that was updated. `occupied` contains the current status of the segment.
|
||||
|
||||
### Segment Boundary
|
||||
Segment boundaries send updates as trains pass them, in order to provide information to the system about the state of connected segments.
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "SEGMENT_BOUNDARY_UPDATE",
|
||||
"toSegmentId": 3,
|
||||
"eventType": "ENTERING"
|
||||
}
|
||||
```
|
||||
`toSegmentId` is the id of the segment a train is moving towards. `eventType` is the type of boundary event. This can either be `ENTERING` if a train has just begun entering the segment, or `ENTERED` if a train has just left the boundary and completely entered the segment.
|
||||
|
||||
### Switch
|
||||
Switches can send information about their status, if it's been updated, and they can also receive messages that direct them to change their status.
|
||||
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "SWITCH_UPDATE",
|
||||
"activeConfigId": 497238
|
||||
}
|
||||
```
|
||||
`activeConfigId` is the id of the switch configuration that's active. This message can be sent by either the system or the switch.
|
21
pom.xml
|
@ -5,12 +5,12 @@
|
|||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.6.0</version>
|
||||
<version>2.6.6</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>nl.andrewl</groupId>
|
||||
<artifactId>rail-signal-api</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<version>2.3.0</version>
|
||||
<name>rail-signal-api</name>
|
||||
<description>A simple API for tracking rail traffic in signalled blocks.</description>
|
||||
<properties>
|
||||
|
@ -25,6 +25,22 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
|
@ -32,6 +48,7 @@
|
|||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.1.212</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
@ -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,280 @@
|
|||
<?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="512"
|
||||
height="200"
|
||||
viewBox="0 0 135.46666 52.916668"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="layout.svg"
|
||||
inkscape:export-filename="/home/andrew/Programming/github-andrewlalis/RailSignalAPI/quasar-app/src/assets/img/guide/layout.png"
|
||||
inkscape:export-xdpi="192"
|
||||
inkscape:export-ydpi="192">
|
||||
<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="261.33576"
|
||||
inkscape:cy="131.69301"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:pagecheckerboard="false"
|
||||
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,-244.08333)">
|
||||
<path
|
||||
style="fill:#fb6700;fill-opacity:0.23137255;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 43.67314,257.80597 55.767023,12.57864 0.06207,18.71578 H 30.710567 v -18.33185 z"
|
||||
id="path1012"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
y="151.32266"
|
||||
x="173.9552"
|
||||
height="18.148809"
|
||||
width="39.182587"
|
||||
id="rect1006"
|
||||
style="fill:#00fcb4;fill-opacity:0.23157893;stroke:none;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:2.11666664, 1.05833332;stroke-dashoffset:0;stroke-opacity:1"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#0afc00;fill-opacity:0.23157893;stroke:none;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:2.11666664, 1.05833332;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1004"
|
||||
width="31.245104"
|
||||
height="18.715776"
|
||||
x="-0.53453904"
|
||||
y="270.38474" />
|
||||
<rect
|
||||
y="270.38474"
|
||||
x="99.440163"
|
||||
height="18.715775"
|
||||
width="36.914753"
|
||||
id="rect1008"
|
||||
style="fill:#2200fc;fill-opacity:0.23157893;stroke:none;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:2.11666664, 1.05833332;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M -2.2717908,280.13995 H 137.51017"
|
||||
id="path817"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 75.50364,280.14805 c -34.076865,0 -57.596582,-37.15857 -57.596582,-37.15857"
|
||||
id="path819"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<rect
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect945"
|
||||
width="4.5178719"
|
||||
height="4.5178719"
|
||||
x="266.22791"
|
||||
y="125.43176"
|
||||
transform="rotate(45)" />
|
||||
<path
|
||||
style="fill:none;stroke:#ff0000;stroke-width:1.05833328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.11666664, 1.05833332;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 99.502231,270.76854 v 18.33185"
|
||||
id="path821"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
transform="rotate(45)"
|
||||
y="174.00168"
|
||||
x="217.65794"
|
||||
height="4.5178719"
|
||||
width="4.5178719"
|
||||
id="rect943"
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path823"
|
||||
d="m 30.710565,270.76854 v 18.33185"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:1.05833328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.11666664, 1.05833332;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect941"
|
||||
width="4.5178719"
|
||||
height="4.5178719"
|
||||
x="210.94887"
|
||||
y="158.59915"
|
||||
transform="rotate(45)" />
|
||||
<path
|
||||
style="fill:none;stroke:#ff0000;stroke-width:1.05833328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.11666664, 1.05833332;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 43.673141,257.80597 30.710565,270.76854"
|
||||
id="path825"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="63.122021"
|
||||
y="251.64285"
|
||||
id="text829"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan827"
|
||||
x="63.122021"
|
||||
y="261.00662"
|
||||
style="stroke-width:0.26458332"></tspan></text>
|
||||
<g
|
||||
transform="matrix(0.08953353,0,0,0.08953353,102.00472,251.52712)"
|
||||
id="layer1-3"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path815"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
rx="6.6145835"
|
||||
y="233.04643"
|
||||
x="16.933334"
|
||||
height="43.845234"
|
||||
width="33.866665"
|
||||
id="rect817"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="245.50073"
|
||||
cx="33.866665"
|
||||
id="path819-5"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle821"
|
||||
cx="33.866665"
|
||||
cy="262.60416"
|
||||
r="5.8586307" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
id="g929"
|
||||
transform="matrix(0.08953353,0,0,0.08953353,23.962019,262.35154)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
id="path921"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect923"
|
||||
width="33.866665"
|
||||
height="43.845234"
|
||||
x="16.933334"
|
||||
y="233.04643"
|
||||
rx="6.6145835" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle925"
|
||||
cx="33.866665"
|
||||
cy="245.50073"
|
||||
r="5.8586307" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="262.60416"
|
||||
cx="33.866665"
|
||||
id="circle927"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.08953353,0,0,0.08953353,25.832906,239.90089)"
|
||||
id="g939"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path931"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
rx="6.6145835"
|
||||
y="233.04643"
|
||||
x="16.933334"
|
||||
height="43.845234"
|
||||
width="33.866665"
|
||||
id="rect933"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="245.50073"
|
||||
cx="33.866665"
|
||||
id="circle935"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle937"
|
||||
cx="33.866665"
|
||||
cy="262.60416"
|
||||
r="5.8586307" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.10431371,0,0,0.10431371,60.72992,252.67196)"
|
||||
id="layer1-2"
|
||||
inkscape:label="Layer 1"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-opacity:1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path838"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
id="path840"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path842"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="13.554182"
|
||||
id="path844"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle846"
|
||||
cx="54.713692"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="287.79529"
|
||||
cx="33.866665"
|
||||
id="circle848"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,632 @@
|
|||
<?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="512"
|
||||
height="256"
|
||||
viewBox="0 0 135.46666 67.733335"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="layout2.svg"
|
||||
inkscape:export-filename="/home/andrew/Programming/github-andrewlalis/RailSignalAPI/quasar-app/src/assets/img/guide/layout2.png"
|
||||
inkscape:export-xdpi="192"
|
||||
inkscape:export-ydpi="192">
|
||||
<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="191.26796"
|
||||
inkscape:cy="128.73452"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:pagecheckerboard="false"
|
||||
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.26666)">
|
||||
<rect
|
||||
style="fill:#fc8700;fill-opacity:0.23157893;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1179"
|
||||
width="58.515881"
|
||||
height="29.386139"
|
||||
x="35.644123"
|
||||
y="253.14487" />
|
||||
<rect
|
||||
transform="rotate(90)"
|
||||
y="-136.71936"
|
||||
x="264.76611"
|
||||
height="42.559361"
|
||||
width="8.8824463"
|
||||
id="rect1173"
|
||||
style="fill:#fcdf00;fill-opacity:0.23157893;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
x="63.122021"
|
||||
y="251.64285"
|
||||
id="text829"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan827"
|
||||
x="63.122021"
|
||||
y="261.00662"
|
||||
style="stroke-width:0.26458332"></tspan></text>
|
||||
<g
|
||||
transform="matrix(0.08953353,0,0,0.08953353,50.295819,223.70883)"
|
||||
id="g939"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path931"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
rx="6.6145835"
|
||||
y="233.04643"
|
||||
x="16.933334"
|
||||
height="43.845234"
|
||||
width="33.866665"
|
||||
id="rect933"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="245.50073"
|
||||
cx="33.866665"
|
||||
id="circle935"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle937"
|
||||
cx="33.866665"
|
||||
cy="262.60416"
|
||||
r="5.8586307" />
|
||||
</g>
|
||||
<rect
|
||||
transform="rotate(90)"
|
||||
y="-35.825115"
|
||||
x="273.64856"
|
||||
height="42.559361"
|
||||
width="8.8824463"
|
||||
id="rect1177"
|
||||
style="fill:#00fcac;fill-opacity:0.23157893;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#7afc00;fill-opacity:0.23157893;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1175"
|
||||
width="8.8824463"
|
||||
height="42.559361"
|
||||
x="264.76611"
|
||||
y="-35.825115"
|
||||
transform="rotate(90)" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666664;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M -2.0045214,269.47123 H 137.37654"
|
||||
id="path1071"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:#fc0058;fill-opacity:0.23157893;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1171"
|
||||
width="8.8824463"
|
||||
height="42.559361"
|
||||
x="273.64856"
|
||||
y="-136.71936"
|
||||
transform="rotate(90)" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1073"
|
||||
d="M -2.0045214,278.29112 H 137.37654"
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#2200fc;fill-opacity:0.23157893;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1167"
|
||||
width="8.8824406"
|
||||
height="27.59226"
|
||||
x="56.412945"
|
||||
y="225.94048" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 61.071086,227.50992 v 21.11429"
|
||||
id="path1075"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
y="225.94048"
|
||||
x="65.295387"
|
||||
height="27.59226"
|
||||
width="8.8824406"
|
||||
id="rect1169"
|
||||
style="fill:#b200fc;fill-opacity:0.23157893;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1077"
|
||||
d="m 69.490076,227.50992 v 21.11429"
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 69.490076,248.62421 c 0,29.66692 29.600098,29.66692 29.600098,29.66692"
|
||||
id="path1079"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1081"
|
||||
d="m 61.071086,248.62421 c 0,29.66692 29.600098,29.66692 29.600098,29.66692"
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1083"
|
||||
d="m 69.490076,248.62421 c 0,29.66692 -29.600098,29.66692 -29.600098,29.66692"
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect945"
|
||||
width="4.5178719"
|
||||
height="4.5178719"
|
||||
x="224.94077"
|
||||
y="126.66696"
|
||||
transform="rotate(45)" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 61.071086,248.62421 c 0,29.66692 -29.600098,29.66692 -29.600098,29.66692"
|
||||
id="path1085"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
<rect
|
||||
transform="rotate(45)"
|
||||
y="132.63582"
|
||||
x="218.97191"
|
||||
height="4.5178719"
|
||||
width="4.5178719"
|
||||
id="rect1087"
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
transform="rotate(45)"
|
||||
y="126.71421"
|
||||
x="262.33102"
|
||||
height="4.5178719"
|
||||
width="4.5178719"
|
||||
id="rect1089"
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<g
|
||||
transform="matrix(0.10431371,0,0,0.10431371,65.957315,227.96333)"
|
||||
id="layer1-2"
|
||||
inkscape:label="Layer 1"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-opacity:1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path838"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
id="path840"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path842"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="13.554182"
|
||||
id="path844"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle846"
|
||||
cx="54.713692"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="287.79529"
|
||||
cx="33.866665"
|
||||
id="circle848"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<rect
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1091"
|
||||
width="4.5178719"
|
||||
height="4.5178719"
|
||||
x="256.04718"
|
||||
y="120.43036"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
transform="rotate(45)"
|
||||
y="170.01033"
|
||||
x="219.0349"
|
||||
height="4.5178719"
|
||||
width="4.5178719"
|
||||
id="rect1093"
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#963ae0;fill-opacity:1;stroke:none;stroke-width:0.8320865;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.6641731, 0.83208655;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1095"
|
||||
width="4.5178719"
|
||||
height="4.5178719"
|
||||
x="212.75104"
|
||||
y="163.72647"
|
||||
transform="rotate(45)" />
|
||||
<g
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-opacity:1"
|
||||
inkscape:label="Layer 1"
|
||||
id="g1109"
|
||||
transform="matrix(0.10431371,0,0,0.10431371,57.516077,227.96333)">
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
id="path1097"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1099"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
id="path1101"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1103"
|
||||
cx="13.554182"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="54.713692"
|
||||
id="circle1105"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1107"
|
||||
cx="33.866665"
|
||||
cy="287.79529"
|
||||
r="7.7508163" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-opacity:1"
|
||||
inkscape:label="Layer 1"
|
||||
id="g1123"
|
||||
transform="matrix(0,0.10431371,-0.10431371,0,119.75106,274.75839)">
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
id="path1111"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1113"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
id="path1115"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1117"
|
||||
cx="13.554182"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="54.713692"
|
||||
id="circle1119"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1121"
|
||||
cx="33.866665"
|
||||
cy="287.79529"
|
||||
r="7.7508163" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0,0.10431371,-0.10431371,0,112.85299,274.75839)"
|
||||
id="g1137"
|
||||
inkscape:label="Layer 1"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-opacity:1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1125"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
id="path1127"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1129"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="13.554182"
|
||||
id="circle1131"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1133"
|
||||
cx="54.713692"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="287.79529"
|
||||
cx="33.866665"
|
||||
id="circle1135"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-opacity:1"
|
||||
inkscape:label="Layer 1"
|
||||
id="g1151"
|
||||
transform="matrix(0,-0.10431371,0.10431371,0,10.810111,281.82389)">
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
id="path1139"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1141"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
id="path1143"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1145"
|
||||
cx="13.554182"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="54.713692"
|
||||
id="circle1147"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1149"
|
||||
cx="33.866665"
|
||||
cy="287.79529"
|
||||
r="7.7508163" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0,-0.10431371,0.10431371,0,18.180647,281.82389)"
|
||||
id="g1165"
|
||||
inkscape:label="Layer 1"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-opacity:1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1153"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
id="path1155"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1157"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="13.554182"
|
||||
id="circle1159"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1161"
|
||||
cx="54.713692"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="287.79529"
|
||||
cx="33.866665"
|
||||
id="circle1163"
|
||||
style="fill:#f5bc42;fill-opacity:1;stroke:#f5bc42;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
id="g1189"
|
||||
transform="matrix(0.08953353,0,0,0.08953353,75.151885,223.70883)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
id="path1181"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1183"
|
||||
width="33.866665"
|
||||
height="43.845234"
|
||||
x="16.933334"
|
||||
y="233.04643"
|
||||
rx="6.6145835" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1185"
|
||||
cx="33.866665"
|
||||
cy="245.50073"
|
||||
r="5.8586307" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="262.60416"
|
||||
cx="33.866665"
|
||||
id="circle1187"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.08953353,0,0,0.08953353,95.063465,236.80504)"
|
||||
id="g1199"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1191"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
rx="6.6145835"
|
||||
y="233.04643"
|
||||
x="16.933334"
|
||||
height="43.845234"
|
||||
width="33.866665"
|
||||
id="rect1193"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="245.50073"
|
||||
cx="33.866665"
|
||||
id="circle1195"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1197"
|
||||
cx="33.866665"
|
||||
cy="262.60416"
|
||||
r="5.8586307" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
id="g1209"
|
||||
transform="matrix(0.08953353,0,0,0.08953353,95.063465,263.39836)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
id="path1201"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1203"
|
||||
width="33.866665"
|
||||
height="43.845234"
|
||||
x="16.933334"
|
||||
y="233.04643"
|
||||
rx="6.6145835" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1205"
|
||||
cx="33.866665"
|
||||
cy="245.50073"
|
||||
r="5.8586307" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="262.60416"
|
||||
cx="33.866665"
|
||||
id="circle1207"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.08953353,0,0,0.08953353,27.978814,263.39836)"
|
||||
id="g1219"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1211"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
rx="6.6145835"
|
||||
y="233.04643"
|
||||
x="16.933334"
|
||||
height="43.845234"
|
||||
width="33.866665"
|
||||
id="rect1213"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="245.50073"
|
||||
cx="33.866665"
|
||||
id="circle1215"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1217"
|
||||
cx="33.866665"
|
||||
cy="262.60416"
|
||||
r="5.8586307" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
id="g1229"
|
||||
transform="matrix(0.08953353,0,0,0.08953353,27.978814,236.93868)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
id="path1221"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1223"
|
||||
width="33.866665"
|
||||
height="43.845234"
|
||||
x="16.933334"
|
||||
y="233.04643"
|
||||
rx="6.6145835" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle1225"
|
||||
cx="33.866665"
|
||||
cy="245.50073"
|
||||
r="5.8586307" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="262.60416"
|
||||
cx="33.866665"
|
||||
id="circle1227"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,82 @@
|
|||
<?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="label_icon.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="-7.53551"
|
||||
inkscape:cy="167.89676"
|
||||
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 />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-229.26665)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 23.812499,294.90224 V 240.28468"
|
||||
id="path841"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect843"
|
||||
width="38.364582"
|
||||
height="24.095985"
|
||||
x="23.812498"
|
||||
y="233.40552"
|
||||
rx="6.614583" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:4.23333334;stroke-linecap:round;stroke-linejoin:bevel;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 31.372024,241.57922 H 54.475819"
|
||||
id="path845"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path847"
|
||||
d="M 31.479768,247.91032 H 54.583563"
|
||||
style="fill:none;stroke:#000000;stroke-width:4.23333311;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,79 @@
|
|||
<?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="segment-boundary_icon.svg">
|
||||
<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="56.595604"
|
||||
inkscape:cy="134.48081"
|
||||
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)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 0.70544035,242.15266 H 28.234201 v 41.96132 H 1.1063446"
|
||||
id="path869"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path871"
|
||||
d="M 67.027893,284.11398 H 39.499132 v -41.96132 h 27.127856"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 6.9392617,256.58521 H 60.794072"
|
||||
id="path873"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path875"
|
||||
d="M 6.9392617,269.68142 H 60.794072"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,84 @@
|
|||
<?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="signal_icon.svg">
|
||||
<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="97.668938"
|
||||
inkscape:cy="136.97831"
|
||||
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)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.35000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
id="path815"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect817"
|
||||
width="33.866665"
|
||||
height="43.845234"
|
||||
x="16.933334"
|
||||
y="233.04643"
|
||||
rx="6.6145835" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path819"
|
||||
cx="33.866665"
|
||||
cy="245.50073"
|
||||
r="5.8586307" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="262.60416"
|
||||
cx="33.866665"
|
||||
id="circle821"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,92 @@
|
|||
<?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="switch_icon.svg">
|
||||
<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="34.945838"
|
||||
inkscape:cy="171.1685"
|
||||
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)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
id="path838"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path840"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
id="path842"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path844"
|
||||
cx="13.554182"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="54.713692"
|
||||
id="circle846"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle848"
|
||||
cx="33.866665"
|
||||
cy="287.79529"
|
||||
r="7.7508163" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
|
@ -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,35 @@
|
|||
{
|
||||
"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": {
|
||||
"@quasar/extras": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"pinia": "^2.0.11",
|
||||
"quasar": "^2.6.0",
|
||||
"randomcolor": "^0.6.2",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.0.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"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,230 @@
|
|||
/* 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: "/app/",
|
||||
// analyze: true,
|
||||
// env: {
|
||||
// API_URL: ctx.dev ? "http://localhost:8080/api" : process.env.RAIL_SIGNAL_API_URL,
|
||||
// WS_URL: ctx.dev ? "ws://localhost:8080/api/ws/app" : process.env.RAIL_SIGNAL_WS_URL
|
||||
// },
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
// minify: "hidden",
|
||||
// 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,124 @@
|
|||
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 previousSelectedComponentIds = rs.selectedComponents.map(c => c.id);
|
||||
rs.selectedComponents.length = 0;
|
||||
rs.components = response.data;
|
||||
for (let i = 0; i < previousSelectedComponentIds.length; i++) {
|
||||
const component = rs.components.find(c => c.id === previousSelectedComponentIds[i]);
|
||||
if (component) {
|
||||
rs.selectedComponents.push(component);
|
||||
}
|
||||
}
|
||||
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(`${API_URL}/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.selectedComponents.length = 0;
|
||||
rs.selectedComponents.push(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);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateComponent(rs, component) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.patch(`${API_URL}/rs/${rs.id}/c/${component.id}`, component)
|
||||
.then(() => {
|
||||
refreshComponents(rs)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateSwitchConfiguration(rs, sw, configId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(
|
||||
`${API_URL}/rs/${rs.id}/c/${sw.id}/activeConfiguration`,
|
||||
{
|
||||
activeConfigurationId: configId
|
||||
}
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
In development mode, we should use localhost:8080 as that's where the API is set
|
||||
to run. In production mode, this app is deployed under the same host as the API,
|
||||
so we can simply use `location.origin` and `location.host` to define our API and
|
||||
WS urls.
|
||||
*/
|
||||
export const API_URL = process.env.DEV
|
||||
? "http://localhost:8080/api"
|
||||
: location.origin + "/api";
|
||||
export const WS_URL = process.env.DEV
|
||||
? "ws://localhost:8080/api/ws/app"
|
||||
: (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/api/ws/app";
|
|
@ -0,0 +1,62 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list of link tokens in a rail system.
|
||||
* @param {RailSystem} rs
|
||||
* @return {Promise}
|
||||
*/
|
||||
export function refreshLinkTokens(rs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/rs/${rs.id}/lt`)
|
||||
.then(response => {
|
||||
rs.linkTokens = response.data;
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new link token.
|
||||
* @param {RailSystem} rs
|
||||
* @param {{label: String, componentIds: Number[]}} 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 => {
|
||||
const token = response.data.token;
|
||||
refreshLinkTokens(rs).then(() => resolve(token)).catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a link token.
|
||||
* @param {RailSystem} rs
|
||||
* @param {Number} tokenId
|
||||
*/
|
||||
export function deleteLinkToken(rs, tokenId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(`${API_URL}/rs/${rs.id}/lt/${tokenId}`)
|
||||
.then(() => {
|
||||
refreshLinkTokens(rs).then(resolve).catch(reject);
|
||||
})
|
||||
.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,111 @@
|
|||
import axios from "axios";
|
||||
import {API_URL} from "./constants";
|
||||
import { refreshSegments } from "src/api/segments";
|
||||
import { refreshComponents } from "src/api/components";
|
||||
import { closeWebsocketConnection, establishWebsocketConnection } from "src/api/websocket";
|
||||
import { refreshLinkTokens } from "src/api/linkTokens";
|
||||
import { refreshSettings } from "src/api/settings";
|
||||
|
||||
export class RailSystem {
|
||||
constructor(data) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.settings = null;
|
||||
|
||||
this.segments = [];
|
||||
this.components = [];
|
||||
this.linkTokens = [];
|
||||
|
||||
this.websocket = null;
|
||||
this.selectedComponents = [];
|
||||
this.loaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (rsStore.selectedRailSystem !== null && rsStore.selectedRailSystem.id === id) {
|
||||
rsStore.selectedRailSystem = null;
|
||||
}
|
||||
refreshRailSystems(rsStore)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all data for a rail system. This is generally done when a rail system
|
||||
* is selected.
|
||||
* @param {RailSystem} rs
|
||||
*/
|
||||
export async function loadData(rs) {
|
||||
console.log("Loading rail system " + rs.id);
|
||||
await closeWebsocketConnection(rs);
|
||||
console.log("Closed websocket connection to " + rs.id);
|
||||
const updatePromises = [];
|
||||
updatePromises.push(refreshSegments(rs));
|
||||
updatePromises.push(refreshComponents(rs));
|
||||
updatePromises.push(refreshLinkTokens(rs));
|
||||
updatePromises.push(refreshSettings(rs));
|
||||
await Promise.all(updatePromises);
|
||||
await establishWebsocketConnection(rs);
|
||||
console.log("Finished loading rail system " + rs.id);
|
||||
rs.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads all data for a rail system. This is generally done when the user
|
||||
* navigates away from a rail system's page.
|
||||
* @param {RailSystem} rs
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function unloadData(rs) {
|
||||
console.log("Unloading data for rail system " + rs.id);
|
||||
await closeWebsocketConnection(rs);
|
||||
rs.segments = [];
|
||||
rs.components = [];
|
||||
rs.linkTokens = [];
|
||||
rs.selectedComponents = [];
|
||||
rs.settings = null;
|
||||
rs.loaded = false;
|
||||
console.log("Finished unloading data for rail system " + rs.id);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
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));
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleOccupied(rs, segmentId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.patch(`${API_URL}/rs/${rs.id}/s/${segmentId}/occupied`)
|
||||
.then(response => {
|
||||
const updatedSegment = response.data;
|
||||
const segment = rs.segments.find(s => s.id === updatedSegment.id);
|
||||
segment.occupied = updatedSegment.occupied;
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
})
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import {API_URL} from "src/api/constants";
|
||||
import axios from "axios";
|
||||
|
||||
export function refreshSettings(rs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/rs/${rs.id}/settings`)
|
||||
.then(response => {
|
||||
rs.settings = response.data;
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateSettings(rs, newSettings) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${API_URL}/rs/${rs.id}/settings`, newSettings)
|
||||
.then(() => {
|
||||
refreshSettings(rs).then(resolve).catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { WS_URL } from "./constants";
|
||||
|
||||
/**
|
||||
* The time to wait before attempting to reconnect if a websocket connection is
|
||||
* abruptly closed.
|
||||
* @type {number}
|
||||
*/
|
||||
const WS_RECONNECT_TIMEOUT = 3000;
|
||||
|
||||
/**
|
||||
* Establishes a websocket connection to the given rail system.
|
||||
* @param {RailSystem} rs
|
||||
* @return {Promise} A promise that resolves when a connection is established.
|
||||
*/
|
||||
export function establishWebsocketConnection(rs) {
|
||||
if (rs.websocket) {
|
||||
console.log('rail system ' + rs.id + ' already has websocket')
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
|
||||
rs.websocket.onopen = resolve;
|
||||
rs.websocket.onclose = event => {
|
||||
if (event.code === 1000) {
|
||||
console.log(`Closed websocket connection to rail system "${rs.name}" (${rs.id})`);
|
||||
} else {
|
||||
console.warn(`Unexpectedly lost websocket connection to rail system "${rs.name}" (${rs.id}). Attempting to reestablish in ${WS_RECONNECT_TIMEOUT} ms.`);
|
||||
setTimeout(() => {
|
||||
establishWebsocketConnection(rs)
|
||||
.then(() => console.log("Successfully reestablished connection."));
|
||||
}, WS_RECONNECT_TIMEOUT);
|
||||
}
|
||||
};
|
||||
rs.websocket.onmessage = msg => {
|
||||
const data = JSON.parse(msg.data);
|
||||
console.log(data);
|
||||
if (data.type === "COMPONENT_DATA") {
|
||||
const id = data.cId;
|
||||
const idx = rs.components.findIndex(c => c.id === id);
|
||||
if (idx > -1) {
|
||||
Object.assign(rs.components[idx], data.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
rs.websocket.onerror = error => {
|
||||
console.log(error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the websocket connection to a rail system, if possible.
|
||||
* @param {RailSystem} rs
|
||||
* @return {Promise} A promise that resolves when the connection is closed.
|
||||
*/
|
||||
export function closeWebsocketConnection(rs) {
|
||||
return new Promise(resolve => {
|
||||
if (rs.websocket && rs.websocket.readyState !== WebSocket.CLOSED) {
|
||||
rs.websocket.onclose = () => {
|
||||
rs.websocket = null;
|
||||
resolve();
|
||||
};
|
||||
rs.websocket.close();
|
||||
} else {
|
||||
rs.websocket = null;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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,82 @@
|
|||
<?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="label_icon.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="-7.53551"
|
||||
inkscape:cy="167.89676"
|
||||
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 />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-229.26665)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 23.812499,294.90224 V 240.28468"
|
||||
id="path841"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect843"
|
||||
width="38.364582"
|
||||
height="24.095985"
|
||||
x="23.812498"
|
||||
y="233.40552"
|
||||
rx="6.614583" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:4.23333334;stroke-linecap:round;stroke-linejoin:bevel;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 31.372024,241.57922 H 54.475819"
|
||||
id="path845"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path847"
|
||||
d="M 31.479768,247.91032 H 54.583563"
|
||||
style="fill:none;stroke:#000000;stroke-width:4.23333311;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,79 @@
|
|||
<?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="segment-boundary_icon.svg">
|
||||
<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="56.595604"
|
||||
inkscape:cy="134.48081"
|
||||
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)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 0.70544035,242.15266 H 28.234201 v 41.96132 H 1.1063446"
|
||||
id="path869"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path871"
|
||||
d="M 67.027893,284.11398 H 39.499132 v -41.96132 h 27.127856"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 6.9392617,256.58521 H 60.794072"
|
||||
id="path873"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path875"
|
||||
d="M 6.9392617,269.68142 H 60.794072"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,84 @@
|
|||
<?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="signal_icon.svg">
|
||||
<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="97.668938"
|
||||
inkscape:cy="136.97831"
|
||||
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)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.35000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,296.90549 V 276.77826"
|
||||
id="path815"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect817"
|
||||
width="33.866665"
|
||||
height="43.845234"
|
||||
x="16.933334"
|
||||
y="233.04643"
|
||||
rx="6.6145835" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path819"
|
||||
cx="33.866665"
|
||||
cy="245.50073"
|
||||
r="5.8586307" />
|
||||
<circle
|
||||
r="5.8586307"
|
||||
cy="262.60416"
|
||||
cx="33.866665"
|
||||
id="circle821"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.23333311;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,92 @@
|
|||
<?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="switch_icon.svg">
|
||||
<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="34.945838"
|
||||
inkscape:cy="171.1685"
|
||||
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)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,291.12006 V 259.31498"
|
||||
id="path838"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path840"
|
||||
d="M 33.866667,259.31498 56.356255,236.8254"
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:6.3499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 33.866667,259.31498 11.377087,236.8254"
|
||||
id="path842"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path844"
|
||||
cx="13.554182"
|
||||
cy="238.06705"
|
||||
r="7.7508163" />
|
||||
<circle
|
||||
r="7.7508163"
|
||||
cy="238.06705"
|
||||
cx="54.713692"
|
||||
id="circle846"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.3499999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle848"
|
||||
cx="33.866665"
|
||||
cy="287.79529"
|
||||
r="7.7508163" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 33 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,24 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-6 q-pa-md">
|
||||
<div class="text-h4">{{title}}</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "IndexPageSection",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
:to="'/rail-systems/' + railSystem.id"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>{{railSystem.name}}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section top side>
|
||||
<q-btn size="12px" flat dense round icon="delete" @click.prevent="deleteRs"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RailSystem, removeRailSystem } from "src/api/railSystems";
|
||||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
import { useQuasar } from "quasar";
|
||||
|
||||
export default {
|
||||
name: "RailSystemLink",
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {rsStore, quasar};
|
||||
},
|
||||
methods: {
|
||||
deleteRs() {
|
||||
this.quasar.dialog({
|
||||
title: "Confirm Removal",
|
||||
message: "Are you sure you want to remove this rail system? All associated data will be deleted, permanently.",
|
||||
cancel: true
|
||||
}).onOk(() => {
|
||||
removeRailSystem(this.rsStore, this.railSystem.id)
|
||||
.then(() => {
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Rail system removed."
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<q-list>
|
||||
<q-item-label header>Rail Systems</q-item-label>
|
||||
|
||||
<rail-system-link
|
||||
v-for="rs in railSystems"
|
||||
:key="rs.id"
|
||||
:rail-system="rs"
|
||||
/>
|
||||
|
||||
<q-item>
|
||||
<q-input
|
||||
dense
|
||||
type="text"
|
||||
label="Name"
|
||||
v-model="addRailSystemName"
|
||||
/>
|
||||
<q-btn label="Add" color="primary" @click="create"></q-btn>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RailSystemLink from "components/RailSystemLink.vue";
|
||||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
import { createRailSystem } from "src/api/railSystems";
|
||||
import { useQuasar } from "quasar";
|
||||
|
||||
export default {
|
||||
name: "RailSystemsList",
|
||||
components: { RailSystemLink },
|
||||
props: {
|
||||
railSystems: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {rsStore, quasar};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addRailSystemName: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
create() {
|
||||
createRailSystem(this.rsStore, this.addRailSystemName)
|
||||
.then(() => {
|
||||
this.addRailSystemName = "";
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Rail system created."
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<q-card>
|
||||
<q-form>
|
||||
<q-input
|
||||
label="Name"
|
||||
type="text"
|
||||
v-model="component.name"
|
||||
/>
|
||||
<q-input
|
||||
label="X"
|
||||
type="number"
|
||||
v-model="component.position.x"
|
||||
/>
|
||||
<q-input
|
||||
label="Y"
|
||||
type="number"
|
||||
v-model="component.position.y"
|
||||
/>
|
||||
<q-input
|
||||
label="Z"
|
||||
type="number"
|
||||
v-model="component.position.z"
|
||||
/>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AddComponentDialog",
|
||||
data() {
|
||||
return {
|
||||
component: {
|
||||
name: "",
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-8" id="railSystemMapCanvasContainer">
|
||||
<canvas id="railSystemMapCanvas" height="700">
|
||||
Your browser doesn't support canvas.
|
||||
</canvas>
|
||||
</div>
|
||||
<q-scroll-area class="col-md-4">
|
||||
<div class="row" v-for="component in railSystem.selectedComponents" :key="component.id">
|
||||
<div class="col full-width">
|
||||
<selected-component-view :component="component"/>
|
||||
</div>
|
||||
</div>
|
||||
<add-component-form v-if="addComponent.visible" :rail-system="railSystem" @created="addComponent.visible = false"/>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
<q-page-sticky position="bottom-right" :offset="[25, 25]">
|
||||
<q-fab icon="add" color="accent" v-model="addComponent.visible"/>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {RailSystem} from "src/api/railSystems";
|
||||
import {draw, initMap} from "src/map/mapRenderer";
|
||||
import SelectedComponentView from "components/rs/SelectedComponentView.vue";
|
||||
import {useQuasar} from "quasar";
|
||||
import AddComponentForm from "components/rs/add_component/AddComponentForm.vue";
|
||||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
import { registerComponentSelectionListener } from "src/map/mapEventListener";
|
||||
|
||||
export default {
|
||||
name: "MapView",
|
||||
components: { AddComponentForm, SelectedComponentView },
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {quasar, rsStore};
|
||||
},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addComponent: {
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
initMap(this.railSystem);
|
||||
registerComponentSelectionListener("addComponentFormHide", () => this.addComponent.visible = false);
|
||||
},
|
||||
updated() {
|
||||
initMap(this.railSystem);
|
||||
registerComponentSelectionListener("addComponentFormHide", () => this.addComponent.visible = false);
|
||||
},
|
||||
watch: {
|
||||
railSystem: {
|
||||
handler() {
|
||||
draw();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'addComponent.visible'(newValue) { // Deselect all components when the user opens the "Add Component" form.
|
||||
if (newValue === true) {
|
||||
this.rsStore.selectedRailSystem.selectedComponents.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<q-item 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-item-section side v-if="segment.occupied">
|
||||
<q-chip>Occupied</q-chip>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SegmentListItem",
|
||||
props: {
|
||||
segment: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,155 @@
|
|||
<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-item-section v-if="segment.occupied" side>
|
||||
<q-chip label="Occupied"/>
|
||||
</q-item-section>
|
||||
|
||||
<q-menu touch-position context-menu>
|
||||
<q-list dense style="min-width: 100px">
|
||||
<q-item clickable v-close-popup @click="toggleOccupiedInline(segment)">
|
||||
<q-item-section>Toggle Occupied</q-item-section>
|
||||
</q-item>
|
||||
<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, toggleOccupied } 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 = "";
|
||||
},
|
||||
toggleOccupiedInline(segment) {
|
||||
toggleOccupied(this.rsStore.selectedRailSystem, segment.id)
|
||||
},
|
||||
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,42 @@
|
|||
<template>
|
||||
<label-component-view
|
||||
:label="component"
|
||||
v-if="component.type === 'LABEL'"
|
||||
/>
|
||||
<signal-component-view
|
||||
:signal="component"
|
||||
v-if="component.type === 'SIGNAL'"
|
||||
/>
|
||||
<segment-boundary-component-view
|
||||
:segment-boundary="component"
|
||||
v-if="component.type === 'SEGMENT_BOUNDARY'"
|
||||
/>
|
||||
<switch-component-view
|
||||
:sw="component"
|
||||
v-if="component.type === 'SWITCH'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SignalComponentView from "components/rs/component_views/SignalComponentView.vue";
|
||||
import SegmentBoundaryComponentView from "components/rs/component_views/SegmentBoundaryComponentView.vue";
|
||||
import LabelComponentView from "components/rs/component_views/LabelComponentView.vue";
|
||||
import SwitchComponentView from "components/rs/component_views/SwitchComponentView.vue";
|
||||
|
||||
export default {
|
||||
name: "SelectedComponentView",
|
||||
components: {
|
||||
SwitchComponentView,
|
||||
LabelComponentView, SegmentBoundaryComponentView, SignalComponentView },
|
||||
props: {
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<q-list>
|
||||
<q-item-label header>General Settings</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Read Only</q-item-label>
|
||||
<q-item-label caption>Freeze this rail system and prevent all updates.</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-toggle v-model="settings.readOnly"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>Authentication Required</q-item-label>
|
||||
<q-item-label caption>Require users to login to view and manage the system.</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-toggle v-model="settings.authenticationRequired"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator/>
|
||||
|
||||
<link-tokens-view :rail-system="railSystem"/>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RailSystem } from "src/api/railSystems";
|
||||
import LinkTokensView from "components/rs/settings/LinkTokensView.vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import { updateSettings } from "src/api/settings";
|
||||
|
||||
export default {
|
||||
name: "SettingsView",
|
||||
components: { LinkTokensView },
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const quasar = useQuasar();
|
||||
return {quasar};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
settings: {
|
||||
readOnly: this.railSystem.settings.readOnly,
|
||||
authenticationRequired: this.railSystem.settings.authenticationRequired
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
settings: {
|
||||
handler(newValue, oldValue) {
|
||||
this.update();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
updateSettings(this.railSystem, this.settings)
|
||||
.then(() => {
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Settings have been updated.",
|
||||
closeBtn: true
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "Settings could not be updated: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<q-dialog v-model="componentData.toggle" style="min-width: 400px" @hide="onReset">
|
||||
<q-card>
|
||||
<q-form @submit="onSubmit" @reset="onReset" @change="onChange">
|
||||
<q-card-section>
|
||||
<div class="text-h6">{{title}}</div>
|
||||
<slot name="subtitle"></slot>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
label="Name"
|
||||
type="text"
|
||||
v-model="componentData.name"
|
||||
autofocus
|
||||
/>
|
||||
<div class="row">
|
||||
<q-input
|
||||
label="X"
|
||||
type="number"
|
||||
class="col-sm-4"
|
||||
v-model="componentData.position.x"
|
||||
/>
|
||||
<q-input
|
||||
label="Y"
|
||||
type="number"
|
||||
class="col-sm-4"
|
||||
v-model="componentData.position.y"
|
||||
/>
|
||||
<q-input
|
||||
label="Z"
|
||||
type="number"
|
||||
class="col-sm-4"
|
||||
v-model="componentData.position.z"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<slot :component-data="componentData"></slot>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" type="reset" @click="componentData.toggle = false"/>
|
||||
<q-btn flat label="Add" type="submit"/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createComponent } from "src/api/components";
|
||||
import { useQuasar } from "quasar";
|
||||
import { RailSystem } from "src/api/railSystems";
|
||||
|
||||
export default {
|
||||
name: "AddComponentDialog",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
successMessage: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const quasar = useQuasar();
|
||||
return {quasar};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
componentData: this.modelValue
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
const postData = {type: this.type, ...this.componentData};
|
||||
createComponent(this.railSystem, postData)
|
||||
.then(() => {
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: this.successMessage
|
||||
});
|
||||
this.componentData.toggle = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
},
|
||||
onReset() {
|
||||
this.componentData.name = "";
|
||||
this.componentData.position.x = 0;
|
||||
this.componentData.position.y = 0;
|
||||
this.componentData.position.z = 0;
|
||||
this.$emit("reset");
|
||||
},
|
||||
onChange() {
|
||||
this.$emit("update:modelValue", this.componentData);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,293 @@
|
|||
<template>
|
||||
<div class="row q-pa-md">
|
||||
<div class="col full-width">
|
||||
<q-form>
|
||||
<div class="text-h4">Add Component</div>
|
||||
<p>
|
||||
Add a new component to the rail system.
|
||||
</p>
|
||||
|
||||
<!-- Basic Attributes -->
|
||||
<div class="row">
|
||||
<div class="col full-width">
|
||||
<q-input label="Name" type="text" v-model="component.name" autofocus/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
label="X"
|
||||
type="number"
|
||||
class="col-sm-4"
|
||||
v-model="component.position.x"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
label="Y"
|
||||
type="number"
|
||||
class="col-sm-4"
|
||||
v-model="component.position.y"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
label="Z"
|
||||
type="number"
|
||||
class="col-sm-4"
|
||||
v-model="component.position.z"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="component.type"
|
||||
:options="typeOptions"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
label="Type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signal Attributes -->
|
||||
<div v-if="component.type === 'SIGNAL'" class="q-mt-md">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="signal.segment"
|
||||
:options="railSystem.segments"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
label="Segment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Label Attributes -->
|
||||
<div v-if="component.type === 'LABEL'" class="q-mt-md">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="label.text"
|
||||
type="text"
|
||||
label="Text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Segment Boundary Attributes -->
|
||||
<div v-if="component.type === 'SEGMENT_BOUNDARY'" class="q-mt-md">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="segmentBoundary.segments"
|
||||
:options="railSystem.segments"
|
||||
:option-value="segment => segment"
|
||||
:option-label="segment => segment.name"
|
||||
use-chips
|
||||
stack-label
|
||||
label="Segments"
|
||||
multiple
|
||||
:max-values="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch Attributes -->
|
||||
<div v-if="component.type === 'SWITCH'" class="q-mt-md">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="config in switchData.possibleConfigurations"
|
||||
:key="config.key"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="q-gutter-sm">
|
||||
<q-chip
|
||||
v-for="node in config.nodes"
|
||||
:key="node.id"
|
||||
:label="node.name"
|
||||
dense
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn size="12px" flat dense round icon="delete" @click="removeSwitchConfig(config)"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<q-select
|
||||
v-model="switchData.configNode1"
|
||||
:options="getEligibleSwitchConfigNodes(switchData.configNode2)"
|
||||
:option-value="node => node"
|
||||
:option-label="node => node.name"
|
||||
label="First Node"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<q-select
|
||||
v-model="switchData.configNode2"
|
||||
:options="getEligibleSwitchConfigNodes(switchData.configNode1)"
|
||||
:option-value="node => node"
|
||||
:option-label="node => node.name"
|
||||
label="Second Node"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-btn label="Add Configuration" @click="addSwitchConfig" v-if="canAddSwitchConfig"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col">
|
||||
<q-btn color="primary" label="Add" @click="submit" :disable="!canAdd()"/>
|
||||
</div>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RailSystem } from "src/api/railSystems";
|
||||
import { useQuasar } from "quasar";
|
||||
import { createComponent } from "src/api/components";
|
||||
|
||||
export default {
|
||||
name: "AddComponentForm",
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const typeOptions = [
|
||||
{label: "Signal", value: "SIGNAL"},
|
||||
{label: "Segment Boundary", value: "SEGMENT_BOUNDARY"},
|
||||
{label: "Switch", value: "SWITCH"},
|
||||
{label: "Label", value: "LABEL"}
|
||||
];
|
||||
const quasar = useQuasar();
|
||||
return {
|
||||
typeOptions,
|
||||
quasar
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
component: {
|
||||
name: "",
|
||||
position: {x: 0, y: 0, z: 0},
|
||||
type: null
|
||||
},
|
||||
signal: {
|
||||
segment: null
|
||||
},
|
||||
label: {
|
||||
text: ""
|
||||
},
|
||||
segmentBoundary: {
|
||||
segments: []
|
||||
},
|
||||
switchData: {
|
||||
possibleConfigurations: [],
|
||||
configNode1: null,
|
||||
configNode2: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const data = this.component;
|
||||
if (this.component.type === 'SIGNAL') {
|
||||
Object.assign(data, this.signal);
|
||||
}
|
||||
if (this.component.type === 'LABEL') {
|
||||
Object.assign(data, this.label);
|
||||
}
|
||||
if (this.component.type === 'SEGMENT_BOUNDARY') {
|
||||
Object.assign(data, this.segmentBoundary);
|
||||
}
|
||||
if (this.component.type === 'SWITCH') {
|
||||
Object.assign(data, this.switchData);
|
||||
}
|
||||
createComponent(this.railSystem, data)
|
||||
.then(() => {
|
||||
this.$emit('created');
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Added component: " + data.name
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
},
|
||||
canAdd() {
|
||||
if (this.component.type === null || this.component.name.length < 1) return false;
|
||||
if (this.component.type === 'SIGNAL') {
|
||||
return this.signal.segment !== null;
|
||||
}
|
||||
if (this.component.type === 'LABEL') {
|
||||
return this.label.text.length > 0;
|
||||
}
|
||||
if (this.component.type === 'SEGMENT_BOUNDARY') {
|
||||
return this.segmentBoundary.segments.length > 0;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getEligibleSwitchConfigNodes(excludedNode) {
|
||||
return this.railSystem.components.filter(c => {
|
||||
return (c.connectedNodes !== undefined && c.connectedNodes !== null) &&
|
||||
(excludedNode === null || c.id !== excludedNode.id);
|
||||
})
|
||||
},
|
||||
removeSwitchConfig(config) {
|
||||
const idx = this.switchData.possibleConfigurations.findIndex(cfg => cfg.key === config.key);
|
||||
if (idx > -1) {
|
||||
this.switchData.possibleConfigurations.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
canAddSwitchConfig() {
|
||||
const n1 = this.switchData.configNode1;
|
||||
const n2 = this.switchData.configNode2;
|
||||
return n1 !== null && n2 !== null && n1.id !== n2.id &&
|
||||
!this.switchData.possibleConfigurations.some(config => {
|
||||
return config.nodes.every(node => {
|
||||
return node.id === n1.id || node.id === n2.id;
|
||||
});
|
||||
});
|
||||
},
|
||||
addSwitchConfig() {
|
||||
this.switchData.possibleConfigurations.push({
|
||||
nodes: [this.switchData.configNode1, this.switchData.configNode2],
|
||||
key: this.switchData.configNode1.id + '_' + this.switchData.configNode2.id
|
||||
});
|
||||
this.switchData.configNode1 = null;
|
||||
this.switchData.configNode2 = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<add-component-dialog
|
||||
v-model="addSignalData"
|
||||
type="SIGNAL"
|
||||
:rail-system="railSystem"
|
||||
title="Add Signal"
|
||||
success-message="Signal added."
|
||||
@reset="onReset"
|
||||
>
|
||||
<template #subtitle>
|
||||
<p>
|
||||
Add a signal to the rail system.
|
||||
</p>
|
||||
</template>
|
||||
<template #default>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
v-model="addSignalData.segment"
|
||||
:options="railSystem.segments"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
label="Segment"
|
||||
/>
|
||||
</q-card-section>
|
||||
</template>
|
||||
</add-component-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RailSystem } from "src/api/railSystems";
|
||||
import AddComponentDialog from "components/rs/add_component/AddComponentDialog.vue";
|
||||
|
||||
export default {
|
||||
name: "AddSignalDialog",
|
||||
components: {AddComponentDialog},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addSignalData: {
|
||||
name: "",
|
||||
position: {
|
||||
x: 0, y: 0, z: 0
|
||||
},
|
||||
segment: null,
|
||||
toggle: this.modelValue
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onReset() {
|
||||
this.addSignalData.segment = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<div class="q-pa-md">
|
||||
<div class="text-h6">{{component.name}}</div>
|
||||
<q-list bordered>
|
||||
<q-item clickable>
|
||||
<q-item-section>
|
||||
<q-item-label>{{component.type}}</q-item-label>
|
||||
<q-item-label caption>Id: {{component.id}}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section v-if="component.online === true" top side>
|
||||
<q-chip color="positive" text-color="white">Online</q-chip>
|
||||
</q-item-section>
|
||||
<q-item-section v-if="component.online === false" top side>
|
||||
<q-chip color="negative" text-color="white">Offline</q-chip>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable>
|
||||
<q-item-section>
|
||||
<q-item-label>Position</q-item-label>
|
||||
<q-item-label caption>
|
||||
X: {{component.position.x}},
|
||||
Y: {{component.position.y}},
|
||||
Z: {{component.position.z}}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator/>
|
||||
<slot></slot>
|
||||
|
||||
<q-item>
|
||||
<q-item-section side>
|
||||
<q-btn color="negative" label="Remove" size="sm" @click="remove(component)"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {removeComponent} from "src/api/components";
|
||||
import {useQuasar} from "quasar";
|
||||
import {useRailSystemsStore} from "stores/railSystemsStore";
|
||||
|
||||
export default {
|
||||
name: "BaseComponentView",
|
||||
props: {
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {rsStore, quasar};
|
||||
},
|
||||
methods: {
|
||||
select(component) {
|
||||
const c = this.rsStore.selectedRailSystem.components.find(cp => cp.id === component.id);
|
||||
if (c) {
|
||||
this.rsStore.selectedRailSystem.selectedComponents.length = 0;
|
||||
this.rsStore.selectedRailSystem.selectedComponents.push(c);
|
||||
}
|
||||
},
|
||||
remove(component) {
|
||||
this.quasar.dialog({
|
||||
title: "Confirm Removal",
|
||||
message: "Are you sure you want to remove this component? This cannot be undone.",
|
||||
cancel: true
|
||||
}).onOk(() => {
|
||||
removeComponent(this.rsStore.selectedRailSystem, component.id)
|
||||
.then(() => {
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Component 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,28 @@
|
|||
<template>
|
||||
<base-component-view :component="label">
|
||||
<q-item-label header>Label</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label>{{label.text}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</base-component-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
|
||||
export default {
|
||||
name: "LabelComponentView",
|
||||
components: {BaseComponentView},
|
||||
props: {
|
||||
label: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<q-item-label header>Connected Path Nodes</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label class="q-gutter-sm">
|
||||
<q-chip
|
||||
v-for="node in pathNode.connectedNodes"
|
||||
:key="node.id"
|
||||
dense
|
||||
size="sm"
|
||||
:label="node.name"
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dense v-if="editable">
|
||||
<q-btn size="sm" color="accent" label="Edit Connected Nodes" @click="showDialog"/>
|
||||
<q-dialog v-model="dialog.visible" style="min-width: 400px" @hide="reset">
|
||||
<q-card>
|
||||
<q-form @submit="onSubmit" @reset="reset">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Edit Connected Nodes</div>
|
||||
<p>
|
||||
Update the nodes that this path node is connected to. These
|
||||
connections define how the system understands your rail network,
|
||||
and are used for routing and analytics.
|
||||
</p>
|
||||
<p v-if="pathNode.type ==='SEGMENT_BOUNDARY'">
|
||||
You're editing a <strong>segment boundary</strong> node. This
|
||||
means that you can only have at most <strong>2</strong> nodes
|
||||
connected to it.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
v-model="dialog.selectedNodes"
|
||||
:options="getEligibleNodes()"
|
||||
multiple
|
||||
:option-value="node => node"
|
||||
:option-label="node => node.name"
|
||||
use-chips
|
||||
stack-label
|
||||
label="Nodes"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" type="reset" @click="dialog.visible = false"/>
|
||||
<q-btn flat label="Edit" type="submit"/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "stores/railSystemsStore";
|
||||
import {useQuasar} from "quasar";
|
||||
import {updateComponent} from "src/api/components";
|
||||
|
||||
export default {
|
||||
name: "PathNodeItem",
|
||||
props: {
|
||||
pathNode: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {rsStore, quasar};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: {
|
||||
visible: false,
|
||||
selectedNodes: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDialog() {
|
||||
this.dialog.selectedNodes = this.pathNode.connectedNodes.slice();
|
||||
this.dialog.visible = true;
|
||||
},
|
||||
reset() {
|
||||
this.dialog.selectedNodes.length = 0;
|
||||
},
|
||||
onSubmit() {
|
||||
const data = {...this.pathNode};
|
||||
data.connectedNodes = this.dialog.selectedNodes;
|
||||
updateComponent(this.rsStore.selectedRailSystem, data)
|
||||
.then(() => {
|
||||
this.dialog.visible = false;
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Path node updated."
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
},
|
||||
getEligibleNodes() {
|
||||
return this.rsStore.selectedRailSystem.components.filter(c => {
|
||||
return (c.connectedNodes !== null && c.connectedNodes !== undefined) &&
|
||||
this.dialog.selectedNodes.findIndex(node => node.id === c.id) === -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<base-component-view :component="segmentBoundary">
|
||||
<q-item-label header>Connected Segments</q-item-label>
|
||||
<q-item>
|
||||
<q-item-section
|
||||
v-for="segment in segmentBoundary.segments"
|
||||
:key="segment.id"
|
||||
>
|
||||
<q-item-label>{{segment.name}}</q-item-label>
|
||||
<q-item-label caption>ID: {{segment.id}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-btn size="sm" color="accent" label="Edit Segments" @click="showDialog"/>
|
||||
<q-dialog v-model="dialog.visible" style="max-width: 400px" @hide="reset">
|
||||
<q-card>
|
||||
<q-form @submit="onSubmit" @reset="reset">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Edit Segments</div>
|
||||
<p>
|
||||
Update the segments that this boundary joins.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
v-model="dialog.segments"
|
||||
:options="rsStore.selectedRailSystem.segments"
|
||||
multiple
|
||||
:option-value="s => s"
|
||||
:option-label="s => s.name"
|
||||
use-chips
|
||||
stack-label
|
||||
label="Segments"
|
||||
max-values="2"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" type="reset" @click="dialog.visible = false"/>
|
||||
<q-btn flat label="Edit" type="submit"/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-item>
|
||||
<q-separator/>
|
||||
<path-node-item :path-node="segmentBoundary" :editable="true"/>
|
||||
</base-component-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
|
||||
import PathNodeItem from "components/rs/component_views/PathNodeItem.vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
import { updateComponent } from "src/api/components";
|
||||
export default {
|
||||
name: "SegmentBoundaryComponentView",
|
||||
components: {PathNodeItem, BaseComponentView},
|
||||
props: {
|
||||
segmentBoundary: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {rsStore, quasar};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: {
|
||||
visible: false,
|
||||
segments: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDialog() {
|
||||
this.dialog.segments = this.segmentBoundary.segments.slice();
|
||||
this.dialog.visible = true;
|
||||
},
|
||||
reset() {
|
||||
this.dialog.segments.length = 0;
|
||||
},
|
||||
onSubmit() {
|
||||
if (this.dialog.segments.length > 2) {
|
||||
this.quasar.notify({
|
||||
color: "warning",
|
||||
message: "Segment boundaries can only join 2 adjacent segments."
|
||||
});
|
||||
return;
|
||||
}
|
||||
const data = {...this.segmentBoundary};
|
||||
data.segments = this.dialog.segments;
|
||||
updateComponent(this.rsStore.selectedRailSystem, data)
|
||||
.then(() => {
|
||||
this.dialog.visible = false;
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Segments updated."
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<base-component-view :component="signal">
|
||||
<q-item-label header>Connected to Segment</q-item-label>
|
||||
<segment-list-item :segment="signal.segment"/>
|
||||
</base-component-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
|
||||
import SegmentListItem from "components/rs/SegmentListItem.vue";
|
||||
export default {
|
||||
name: "SignalComponentView",
|
||||
components: {SegmentListItem, BaseComponentView},
|
||||
props: {
|
||||
signal: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,210 @@
|
|||
<template>
|
||||
<base-component-view :component="sw">
|
||||
<q-item-label header>Switch Configurations</q-item-label>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="config in sw.possibleConfigurations"
|
||||
:key="config.id"
|
||||
dense
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="q-gutter-sm">
|
||||
<q-chip
|
||||
v-for="node in config.nodes"
|
||||
:key="node.id"
|
||||
dense
|
||||
size="sm"
|
||||
:label="node.name"
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section v-if="!isConfigActive(config)" side top>
|
||||
<q-btn dense size="sm" color="positive" @click="setActiveSwitchConfig(config.id)">Set Active</q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-btn size="sm" color="accent" label="Edit Configurations" @click="showDialog"/>
|
||||
<q-dialog v-model="dialog.visible" style="min-width: 400px" @hide="reset">
|
||||
<q-card>
|
||||
<q-form @submit="onSubmit" @reset="reset">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Edit Switch Configurations</div>
|
||||
<p>
|
||||
Edit the possible configurations for this switch.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="config in dialog.configurations"
|
||||
:key="config.key"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<q-chip
|
||||
v-for="node in config.nodes"
|
||||
:key="node.id"
|
||||
:label="node.name"
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn size="12px" flat dense round icon="delete" @click="removeConfig(config)"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<q-select
|
||||
v-model="dialog.pathNode1"
|
||||
:options="getEligibleSwitchNodes(dialog.pathNode2)"
|
||||
:option-value="node => node"
|
||||
:option-label="node => node.name"
|
||||
label="First Path Node"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<q-select
|
||||
v-model="dialog.pathNode2"
|
||||
:options="getEligibleSwitchNodes(dialog.pathNode1)"
|
||||
:option-value="node => node"
|
||||
:option-label="node => node.name"
|
||||
label="Second Path Node"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-btn label="Add Configuration" @click="addConfiguration" v-if="canAddConfig(dialog.pathNode1, dialog.pathNode2)"/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" type="reset" @click="dialog.visible = false"/>
|
||||
<q-btn flat label="Edit" type="submit"/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-separator/>
|
||||
<path-node-item :path-node="sw"/>
|
||||
</base-component-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
|
||||
import {updateComponent, updateSwitchConfiguration} from "src/api/components";
|
||||
import PathNodeItem from "components/rs/component_views/PathNodeItem.vue";
|
||||
import {useRailSystemsStore} from "stores/railSystemsStore";
|
||||
import {useQuasar} from "quasar";
|
||||
export default {
|
||||
name: "SwitchComponentView",
|
||||
components: {PathNodeItem, BaseComponentView},
|
||||
props: {
|
||||
sw: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {rsStore, quasar};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: {
|
||||
visible: false,
|
||||
configurations: [],
|
||||
pathNode1: null,
|
||||
pathNode2: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setActiveSwitchConfig(configId) {
|
||||
updateSwitchConfiguration(this.rsStore.selectedRailSystem, this.sw, configId)
|
||||
.then(() => {
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Sent switch configuration update request."
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
},
|
||||
isConfigActive(config) {
|
||||
return this.sw.activeConfiguration !== null && this.sw.activeConfiguration.id === config.id;
|
||||
},
|
||||
showDialog() {
|
||||
this.dialog.configurations = this.sw.possibleConfigurations.slice();
|
||||
this.dialog.configurations.forEach(cfg => {
|
||||
cfg.key = cfg.nodes[0].id + '_' + cfg.nodes[1].id
|
||||
});
|
||||
this.dialog.visible = true;
|
||||
},
|
||||
reset() {
|
||||
this.dialog.pathNode1 = null;
|
||||
this.dialog.pathNode2 = null;
|
||||
this.dialog.configurations.length = 0;
|
||||
},
|
||||
onSubmit() {
|
||||
const data = {...this.sw};
|
||||
data.possibleConfigurations = this.dialog.configurations;
|
||||
data.activeConfiguration = null;
|
||||
updateComponent(this.rsStore.selectedRailSystem, data)
|
||||
.then(() => {
|
||||
this.dialog.visible = false;
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Switch configurations updated."
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
},
|
||||
getEligibleSwitchNodes(exclude) {
|
||||
return this.rsStore.selectedRailSystem.components.filter(c => {
|
||||
return (c.connectedNodes !== undefined && c.connectedNodes !== null) &&
|
||||
(exclude === null || c.id !== exclude.id);
|
||||
});
|
||||
},
|
||||
canAddConfig(n1, n2) {
|
||||
return this.dialog.configurations.length < 2 &&
|
||||
n1 !== null && n2 !== null &&
|
||||
n1.id !== n2.id &&
|
||||
!this.dialog.configurations.some(config => {
|
||||
return config.nodes.every(node => {
|
||||
return node.id === n1.id || node.id === n2.id;
|
||||
});
|
||||
});
|
||||
},
|
||||
addConfiguration() {
|
||||
this.dialog.configurations.push({
|
||||
nodes: [this.dialog.pathNode1, this.dialog.pathNode2],
|
||||
key: this.dialog.pathNode1.id + '_' + this.dialog.pathNode2.id
|
||||
});
|
||||
},
|
||||
removeConfig(config) {
|
||||
const idx = this.dialog.configurations.findIndex(cfg => cfg.key === config.key);
|
||||
if (idx === -1) return;
|
||||
this.dialog.configurations.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,186 @@
|
|||
<template>
|
||||
<q-expansion-item
|
||||
expand-separator
|
||||
label="Component Links"
|
||||
caption="Link components to your system."
|
||||
switch-toggle-side
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="token in railSystem.linkTokens"
|
||||
:key="token.id"
|
||||
>
|
||||
<q-item-section top>
|
||||
<q-item-label>{{token.label}}</q-item-label>
|
||||
<q-item-label caption>{{token.id}}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section top>
|
||||
<q-item-label caption>Components</q-item-label>
|
||||
<q-item-label>
|
||||
<q-chip
|
||||
v-for="component in token.components"
|
||||
:key="component.id"
|
||||
:label="component.name"
|
||||
dense
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section top side>
|
||||
<q-btn size="12px" flat dense round icon="delete" @click="deleteToken(token)"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="railSystem.linkTokens.length === 0">
|
||||
<q-item-section>
|
||||
<q-item-label caption>There are no link tokens. Add one via the <em>Add Link</em> button below.</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-separator/>
|
||||
|
||||
<q-item>
|
||||
<q-item-section side>
|
||||
<q-btn label="Add Link" color="primary" @click="addTokenDialog = true" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-dialog v-model="addTokenDialog" style="min-width: 400px;" @hide="onReset">
|
||||
<q-card>
|
||||
<q-form @submit="onSubmit" @reset="onReset">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Add Link Token</div>
|
||||
<p>
|
||||
Create a new link token that gives external components access to
|
||||
this rail system's live data streams. Use a link token to let signals
|
||||
in your system get real-time updates, and to let segment boundaries
|
||||
send real-time status updates as trains move.
|
||||
</p>
|
||||
<p>
|
||||
You will be shown the link token <strong>only once</strong> when the
|
||||
token is first created. If you lose it, you will need to delete the
|
||||
token and create a new one.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
label="Label"
|
||||
type="text"
|
||||
v-model="addTokenData.label"
|
||||
autofocus
|
||||
/>
|
||||
<q-select
|
||||
label="Components"
|
||||
multiple
|
||||
v-model="addTokenData.selectedComponents"
|
||||
:options="getEligibleComponents()"
|
||||
:option-value="component => component.id"
|
||||
:option-label="component => component.name"
|
||||
use-chips
|
||||
stack-label
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="token">
|
||||
<q-banner class="bg-primary text-white">
|
||||
<p>
|
||||
Link token created: <code class="text-bold">{{token}}</code>
|
||||
</p>
|
||||
<p>
|
||||
Copy this token now; it won't be shown again.
|
||||
</p>
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Close" type="reset" @click="addTokenDialog = false"/>
|
||||
<q-btn flat label="Add" type="submit" v-if="!token"/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RailSystem } from "src/api/railSystems";
|
||||
import { createLinkToken, deleteLinkToken } from "src/api/linkTokens";
|
||||
import { useQuasar } from "quasar";
|
||||
|
||||
export default {
|
||||
name: "LinkTokensView",
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const quasar = useQuasar();
|
||||
return {quasar};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addTokenDialog: false,
|
||||
addTokenData: {
|
||||
label: "",
|
||||
selectedComponents: [],
|
||||
componentIds: []
|
||||
},
|
||||
token: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getEligibleComponents() {
|
||||
return this.railSystem.components.filter(component => {
|
||||
return component.type === "SIGNAL" || component.type === "SEGMENT_BOUNDARY" || component.type === "SWITCH";
|
||||
});
|
||||
},
|
||||
deleteToken(token) {
|
||||
this.quasar.dialog({
|
||||
title: "Confirm Removal",
|
||||
message: "Are you sure you want to remove this token? All devices using it will need to be given a new token.",
|
||||
cancel: true
|
||||
}).onOk(() => {
|
||||
deleteLinkToken(this.railSystem, token.id)
|
||||
.then(() => {
|
||||
this.quasar.notify({
|
||||
color: "positive",
|
||||
message: "Token removed."
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
onSubmit() {
|
||||
this.addTokenData.componentIds = this.addTokenData.selectedComponents.map(c => c.id);
|
||||
createLinkToken(this.railSystem, this.addTokenData)
|
||||
.then(token => {
|
||||
this.token = token;
|
||||
})
|
||||
.catch(error => {
|
||||
this.quasar.notify({
|
||||
color: "negative",
|
||||
message: "An error occurred: " + error.response.data.message
|
||||
});
|
||||
});
|
||||
},
|
||||
onReset() {
|
||||
this.addTokenData.label = "";
|
||||
this.addTokenData.selectedComponents.length = 0;
|
||||
this.addTokenData.componentIds.length = 0;
|
||||
this.token = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1 @@
|
|||
// app global css in SCSS form
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
/*# sourceMappingURL=quasar.variables.css.map */
|
|
@ -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,73 @@
|
|||
<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
|
||||
>
|
||||
<rail-systems-list :rail-systems="rsStore.railSystems" />
|
||||
<q-item clickable v-ripple :to="'/'">
|
||||
<q-item-section>
|
||||
<q-item-label>Home</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-ripple :to="'/about'">
|
||||
<q-item-section>
|
||||
<q-item-label>About</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from "vue";
|
||||
import RailSystemsList from "components/RailSystemsList.vue";
|
||||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
import { refreshRailSystems } from "src/api/railSystems";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MainLayout',
|
||||
components: {
|
||||
RailSystemsList
|
||||
},
|
||||
setup () {
|
||||
const rsStore = useRailSystemsStore()
|
||||
const leftDrawerOpen = ref(false)
|
||||
return {
|
||||
rsStore,
|
||||
leftDrawerOpen,
|
||||
toggleLeftDrawer () {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
refreshRailSystems(this.rsStore);
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,64 @@
|
|||
import { MAP_CANVAS } from "src/map/mapRenderer";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function mulberry32(a) {
|
||||
return function() {
|
||||
let t = a += 0x6D2B79F5;
|
||||
t = Math.imul(t ^ t >>> 15, t | 1);
|
||||
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
||||
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
||||
}
|
||||
}
|
||||
|
||||
export function sortPoints(points) {
|
||||
points = points.splice(0);
|
||||
const p0 = {};
|
||||
p0.y = Math.min.apply(null, points.map(p=>p.y));
|
||||
p0.x = Math.max.apply(null, points.filter(p=>p.y === p0.y).map(p=>p.x));
|
||||
points.sort((a,b)=>angleCompare(p0, a, b));
|
||||
return points;
|
||||
}
|
||||
|
||||
function angleCompare(p0, a, b) {
|
||||
const left = isLeft(p0, a, b);
|
||||
if (left === 0) return distCompare(p0, a, b);
|
||||
return left;
|
||||
}
|
||||
|
||||
function isLeft(p0, a, b) {
|
||||
return (a.x-p0.x)*(b.y-p0.y) - (b.x-p0.x)*(a.y-p0.y);
|
||||
}
|
||||
|
||||
function distCompare(p0, a, b) {
|
||||
const distA = (p0.x-a.x)*(p0.x-a.x) + (p0.y-a.y)*(p0.y-a.y);
|
||||
const distB = (p0.x-b.x)*(p0.x-b.x) + (p0.y-b.y)*(p0.y-b.y);
|
||||
return distA - distB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the point at which the user clicked on the map.
|
||||
* @param {MouseEvent} event
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function getMousePoint(event) {
|
||||
const rect = MAP_CANVAS.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
return new DOMPoint(x, y, 0, 1);
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
Helper functions to actually perform rendering of different components.
|
||||
*/
|
||||
|
||||
import { isComponentHovered, isComponentSelected, MAP_CTX, MAP_RAIL_SYSTEM } from "./mapRenderer";
|
||||
import { circle, roundedRect, sortPoints } from "./canvasUtils";
|
||||
import randomColor from "randomcolor";
|
||||
import { camGetTransform, camScale } from "src/map/mapCamera";
|
||||
|
||||
export function drawMap() {
|
||||
const worldTx = camGetTransform();
|
||||
MAP_CTX.setTransform(worldTx);
|
||||
drawSegments();
|
||||
drawNodeConnections(worldTx);
|
||||
drawComponents(worldTx);
|
||||
}
|
||||
|
||||
function drawSegments() {
|
||||
const segmentPoints = new Map();
|
||||
// Gather for each segment a set of points representing its bounds.
|
||||
MAP_RAIL_SYSTEM.segments.forEach(segment => segmentPoints.set(segment.id, []));
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort the points to make regular convex polygons.
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.segments.length; i++) {
|
||||
const unsortedPoints = segmentPoints.get(MAP_RAIL_SYSTEM.segments[i].id);
|
||||
segmentPoints.set(MAP_RAIL_SYSTEM.segments[i].id, sortPoints(unsortedPoints));
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.segments.length; i++) {
|
||||
const segment = MAP_RAIL_SYSTEM.segments[i];
|
||||
const color = randomColor({ luminosity: "light", format: "rgb", seed: segment.id });
|
||||
MAP_CTX.fillStyle = color;
|
||||
MAP_CTX.strokeStyle = color;
|
||||
MAP_CTX.lineWidth = 5;
|
||||
MAP_CTX.lineCap = "round";
|
||||
MAP_CTX.lineJoin = "round";
|
||||
MAP_CTX.font = "3px Sans-Serif";
|
||||
|
||||
const points = segmentPoints.get(segment.id);
|
||||
if (points.length === 0) continue;
|
||||
const avgPoint = { x: points[0].x, y: points[0].y };
|
||||
if (points.length === 1) {
|
||||
circle(MAP_CTX, points[0].x, points[0].y, 5);
|
||||
MAP_CTX.fill();
|
||||
} else {
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(points[0].x, points[0].y);
|
||||
for (let j = 1; j < points.length; j++) {
|
||||
MAP_CTX.lineTo(points[j].x, points[j].y);
|
||||
avgPoint.x += points[j].x;
|
||||
avgPoint.y += points[j].y;
|
||||
}
|
||||
avgPoint.x /= points.length;
|
||||
avgPoint.y /= points.length;
|
||||
MAP_CTX.lineTo(points[0].x, points[0].y);
|
||||
MAP_CTX.fill();
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
|
||||
// Draw the segment name.
|
||||
MAP_CTX.fillStyle = randomColor({ luminosity: "dark", format: "rgb", seed: segment.id });
|
||||
MAP_CTX.fillText(segment.name, avgPoint.x, avgPoint.y);
|
||||
}
|
||||
}
|
||||
|
||||
function drawNodeConnections(worldTx) {
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.components[i];
|
||||
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
|
||||
drawConnectedNodes(worldTx, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawComponents(worldTx) {
|
||||
// Draw switch configurations first
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.components[i];
|
||||
if (c.type === "SWITCH") {
|
||||
drawSwitchConfigurations(worldTx, c);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
drawComponent(worldTx, MAP_RAIL_SYSTEM.components[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function drawComponent(worldTx, component) {
|
||||
const componentTransform = DOMMatrix.fromMatrix(worldTx);
|
||||
componentTransform.translateSelf(component.position.x, component.position.z, 0);
|
||||
const s = camScale();
|
||||
componentTransform.scaleSelf(1 / s, 1 / s, 1 / s);
|
||||
componentTransform.scaleSelf(20, 20, 20);
|
||||
MAP_CTX.setTransform(componentTransform);
|
||||
|
||||
if (component.type === "SIGNAL") {
|
||||
drawSignal(component);
|
||||
} else if (component.type === "SEGMENT_BOUNDARY") {
|
||||
drawSegmentBoundary();
|
||||
} else if (component.type === "SWITCH") {
|
||||
drawSwitch();
|
||||
} else if (component.type === "LABEL") {
|
||||
drawLabel(component);
|
||||
}
|
||||
|
||||
MAP_CTX.setTransform(componentTransform.translate(0.75, -0.75));
|
||||
if (component.online !== undefined && component.online !== null) {
|
||||
drawOnlineIndicator(component);
|
||||
}
|
||||
|
||||
MAP_CTX.setTransform(componentTransform);
|
||||
// Draw hovered status.
|
||||
if (isComponentHovered(component) || isComponentSelected(component)) {
|
||||
MAP_CTX.fillStyle = `rgba(255, 255, 0, 0.5)`;
|
||||
circle(MAP_CTX, 0, 0, 0.75);
|
||||
MAP_CTX.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawSignal(signal) {
|
||||
roundedRect(MAP_CTX, -0.3, -0.5, 0.6, 1, 0.25);
|
||||
MAP_CTX.fillStyle = "black";
|
||||
MAP_CTX.fill();
|
||||
if (signal.segment) {
|
||||
if (signal.segment.occupied === true) {
|
||||
MAP_CTX.fillStyle = `rgb(255, 0, 0)`;
|
||||
} else if (signal.segment.occupied === false) {
|
||||
MAP_CTX.fillStyle = `rgb(0, 255, 0)`;
|
||||
} else {
|
||||
MAP_CTX.fillStyle = `rgb(255, 255, 0)`;
|
||||
}
|
||||
} else {
|
||||
MAP_CTX.fillStyle = `rgb(0, 0, 255)`;
|
||||
}
|
||||
circle(MAP_CTX, 0, -0.2, 0.15);
|
||||
MAP_CTX.fill();
|
||||
}
|
||||
|
||||
function drawSegmentBoundary() {
|
||||
MAP_CTX.fillStyle = `rgb(150, 58, 224)`;
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(0, -0.5);
|
||||
MAP_CTX.lineTo(-0.5, 0);
|
||||
MAP_CTX.lineTo(0, 0.5);
|
||||
MAP_CTX.lineTo(0.5, 0);
|
||||
MAP_CTX.lineTo(0, -0.5);
|
||||
MAP_CTX.fill();
|
||||
}
|
||||
|
||||
function drawSwitchConfigurations(worldTx, sw) {
|
||||
const tx = DOMMatrix.fromMatrix(worldTx);
|
||||
tx.translateSelf(sw.position.x, sw.position.z, 0);
|
||||
const s = camScale();
|
||||
tx.scaleSelf(1 / s, 1 / s, 1 / s);
|
||||
tx.scaleSelf(20, 20, 20);
|
||||
MAP_CTX.setTransform(tx);
|
||||
|
||||
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
|
||||
const config = sw.possibleConfigurations[i];
|
||||
MAP_CTX.strokeStyle = randomColor({
|
||||
seed: config.id,
|
||||
format: "rgb",
|
||||
luminosity: "bright"
|
||||
});
|
||||
if (sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id) {
|
||||
MAP_CTX.lineWidth = 0.6;
|
||||
} else {
|
||||
MAP_CTX.lineWidth = 0.3;
|
||||
}
|
||||
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 = 3 * -diff.x / mag;
|
||||
diff.y = 3 * -diff.y / mag;
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(0, 0);
|
||||
MAP_CTX.lineTo(diff.x, diff.y);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawSwitch() {
|
||||
MAP_CTX.fillStyle = `rgb(245, 188, 66)`;
|
||||
MAP_CTX.strokeStyle = `rgb(245, 188, 66)`;
|
||||
MAP_CTX.lineWidth = 0.2;
|
||||
circle(MAP_CTX, 0, 0.3, 0.2);
|
||||
MAP_CTX.fill();
|
||||
circle(MAP_CTX, -0.3, -0.3, 0.2);
|
||||
MAP_CTX.fill();
|
||||
circle(MAP_CTX, 0.3, -0.3, 0.2);
|
||||
MAP_CTX.fill();
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(0, 0.3);
|
||||
MAP_CTX.lineTo(0, 0);
|
||||
MAP_CTX.lineTo(0.3, -0.3);
|
||||
MAP_CTX.moveTo(0, 0);
|
||||
MAP_CTX.lineTo(-0.3, -0.3);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
|
||||
function drawLabel(lbl) {
|
||||
MAP_CTX.fillStyle = "black";
|
||||
circle(MAP_CTX, 0, 0, 0.1);
|
||||
MAP_CTX.fill();
|
||||
MAP_CTX.strokeStyle = "black";
|
||||
MAP_CTX.font = "0.5px Sans-Serif";
|
||||
MAP_CTX.fillText(lbl.text, 0.1, -0.2);
|
||||
}
|
||||
|
||||
function drawOnlineIndicator(component) {
|
||||
MAP_CTX.lineWidth = 0.1;
|
||||
if (component.online) {
|
||||
MAP_CTX.fillStyle = `rgba(52, 174, 235, 128)`;
|
||||
MAP_CTX.strokeStyle = `rgba(52, 174, 235, 128)`;
|
||||
} else {
|
||||
MAP_CTX.fillStyle = `rgba(153, 153, 153, 128)`;
|
||||
MAP_CTX.strokeStyle = `rgba(153, 153, 153, 128)`;
|
||||
}
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.arc(0, 0.2, 0.125, 0, Math.PI * 2);
|
||||
MAP_CTX.fill();
|
||||
for (let r = 0; r < 3; r++) {
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.arc(0, 0, 0.1 + 0.2 * r, 7 * Math.PI / 6, 11 * Math.PI / 6);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawConnectedNodes(worldTx, component) {
|
||||
const s = camScale();
|
||||
MAP_CTX.lineWidth = 5 / s;
|
||||
MAP_CTX.strokeStyle = "black";
|
||||
for (let i = 0; i < component.connectedNodes.length; i++) {
|
||||
const node = component.connectedNodes[i];
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(component.position.x, component.position.z);
|
||||
MAP_CTX.lineTo(node.position.x, node.position.z);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import { MAP_CANVAS } from "src/map/mapRenderer";
|
||||
|
||||
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;
|
||||
|
||||
let scaleIndex = SCALE_INDEX_NORMAL;
|
||||
let translation = {x: 0, y: 0};
|
||||
let panOrigin = null;
|
||||
let panTranslation = null;
|
||||
let lastPanPoint = null;
|
||||
|
||||
/**
|
||||
* Resets the camera view to the default values.
|
||||
*/
|
||||
export function camResetView() {
|
||||
scaleIndex = SCALE_INDEX_NORMAL;
|
||||
translation.x = 0;
|
||||
translation.y = 0;
|
||||
panOrigin = null;
|
||||
panTranslation = null;
|
||||
lastPanPoint = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms in the camera, if possible.
|
||||
*/
|
||||
export function camZoomIn() {
|
||||
if (scaleIndex < SCALE_VALUES.length - 1) {
|
||||
scaleIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms out the camera, if possible.
|
||||
*/
|
||||
export function camZoomOut() {
|
||||
if (scaleIndex > 0) {
|
||||
scaleIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current zoom scale of the camera.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function camScale() {
|
||||
return SCALE_VALUES[scaleIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the camera transform that's used to map world coordinates to the canvas.
|
||||
* @return {DOMMatrix}
|
||||
*/
|
||||
export function camGetTransform() {
|
||||
const tx = new DOMMatrix();
|
||||
const canvasRect = MAP_CANVAS.getBoundingClientRect();
|
||||
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
|
||||
const scale = SCALE_VALUES[scaleIndex];
|
||||
tx.scaleSelf(scale, scale, scale);
|
||||
tx.translateSelf(translation.x, translation.y, 0);
|
||||
if (panOrigin && panTranslation) {
|
||||
tx.translateSelf(panTranslation.x, panTranslation.y, 0);
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
export function camPanStart(point) {
|
||||
panOrigin = point;
|
||||
panTranslation = {x: 0, y: 0};
|
||||
}
|
||||
|
||||
export function camPanActive() {
|
||||
return panOrigin !== null;
|
||||
}
|
||||
|
||||
export function camPanNonzero() {
|
||||
return panTranslation !== null && (panTranslation.x !== 0 || panTranslation.y !== 0);
|
||||
}
|
||||
|
||||
export function camPanMove(point) {
|
||||
if (panOrigin) {
|
||||
lastPanPoint = point;
|
||||
const scale = SCALE_VALUES[scaleIndex];
|
||||
const dx = point.x - panOrigin.x;
|
||||
const dy = point.y - panOrigin.y;
|
||||
panTranslation = {x: dx / scale, y: dy / scale};
|
||||
}
|
||||
}
|
||||
|
||||
export function camPanFinish() {
|
||||
if (panTranslation) {
|
||||
translation.x += panTranslation.x;
|
||||
translation.y += panTranslation.y;
|
||||
}
|
||||
panOrigin = null;
|
||||
panTranslation = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point on the map coordinates to world coordinates.
|
||||
* @param {DOMPoint} point
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function camTransformMapToWorld(point) {
|
||||
return camGetTransform().invertSelf().transformPoint(point);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point in the world to map coordinates.
|
||||
* @param {DOMPoint} point
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function camTransformWorldToMap(point) {
|
||||
return camGetTransform().transformPoint(point);
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
import { draw, MAP_COMPONENTS_HOVERED, MAP_CANVAS, MAP_RAIL_SYSTEM } from "src/map/mapRenderer";
|
||||
import {
|
||||
camPanActive,
|
||||
camPanFinish,
|
||||
camPanMove,
|
||||
camPanNonzero,
|
||||
camPanStart, camResetView, camTransformWorldToMap,
|
||||
camZoomIn,
|
||||
camZoomOut
|
||||
} from "src/map/mapCamera";
|
||||
import { getMousePoint } from "src/map/canvasUtils";
|
||||
|
||||
const HOVER_RADIUS = 10;
|
||||
|
||||
export let LAST_MOUSE_POINT = null;
|
||||
|
||||
const SELECTION_MODE_NORMAL = 1;
|
||||
const SELECTION_MODE_CHOOSE = 2;
|
||||
let selectionMode = SELECTION_MODE_NORMAL;
|
||||
const componentSelectionListeners = new Map();
|
||||
|
||||
/**
|
||||
* Initializes all event listeners for the map controls.
|
||||
*/
|
||||
export function initListeners() {
|
||||
registerListener(MAP_CANVAS, "wheel", onMouseWheel);
|
||||
registerListener(MAP_CANVAS, "mousedown", onMouseDown);
|
||||
registerListener(MAP_CANVAS, "mouseup", onMouseUp);
|
||||
registerListener(MAP_CANVAS, "mousemove", onMouseMove);
|
||||
registerListener(window, "keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function registerListener(element, type, callback) {
|
||||
element.removeEventListener(type, callback);
|
||||
element.addEventListener(type, callback);
|
||||
}
|
||||
|
||||
export function registerComponentSelectionListener(name, callback) {
|
||||
componentSelectionListeners.set(name, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse scroll wheel. This is used to control camera zoom.
|
||||
* @param {WheelEvent} event
|
||||
*/
|
||||
function onMouseWheel(event) {
|
||||
if (!event.shiftKey) return;
|
||||
const s = event.deltaY;
|
||||
if (s < 0) {
|
||||
camZoomIn();
|
||||
} else if (s > 0) {
|
||||
camZoomOut();
|
||||
}
|
||||
draw();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse down clicks. We only use this to start tracking panning.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseDown(event) {
|
||||
const p = getMousePoint(event);
|
||||
if (event.shiftKey) {
|
||||
camPanStart({ x: p.x, y: p.y });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse up events. This means we do the following:
|
||||
* - Finish any camera panning, if it was active.
|
||||
* - Select any components that the mouse is close enough to.
|
||||
* - If no components were selected, clear the list of selected components.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseUp(event) {
|
||||
if (selectionMode === SELECTION_MODE_NORMAL) {
|
||||
handleNormalSelectionMouseUp(event);
|
||||
}
|
||||
camPanFinish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the mouse up event in normal selection mode. This means changing the
|
||||
* set of selected components.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function handleNormalSelectionMouseUp(event) {
|
||||
if (MAP_COMPONENTS_HOVERED.length > 0) {
|
||||
if (!event.shiftKey) {// If the user isn't holding SHIFT, clear the set of selected components first.
|
||||
MAP_RAIL_SYSTEM.selectedComponents.length = 0;
|
||||
}
|
||||
// If the user is clicking on a component that's already selected, deselect it.
|
||||
for (let i = 0; i < MAP_COMPONENTS_HOVERED.length; i++) {
|
||||
const hoveredComponent = MAP_COMPONENTS_HOVERED[i];
|
||||
const idx = MAP_RAIL_SYSTEM.selectedComponents.findIndex(c => c.id === hoveredComponent.id);
|
||||
if (idx > -1) {
|
||||
MAP_RAIL_SYSTEM.selectedComponents.splice(idx, 1);
|
||||
} else {
|
||||
MAP_RAIL_SYSTEM.selectedComponents.push(hoveredComponent);
|
||||
}
|
||||
}
|
||||
componentSelectionListeners.forEach(callback => callback(MAP_RAIL_SYSTEM.selectedComponents));
|
||||
} else if (!camPanNonzero()) {
|
||||
MAP_RAIL_SYSTEM.selectedComponents.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when the mouse moves.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseMove(event) {
|
||||
const p = getMousePoint(event);
|
||||
LAST_MOUSE_POINT = p;
|
||||
if (camPanActive()) {
|
||||
if (event.shiftKey) {
|
||||
camPanMove({ x: p.x, y: p.y });
|
||||
} else {
|
||||
camPanFinish();
|
||||
}
|
||||
} else {
|
||||
MAP_COMPONENTS_HOVERED.length = 0;
|
||||
// Populate with list of hovered elements.
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.components[i];
|
||||
const componentPoint = new DOMPoint(c.position.x, c.position.z, 0, 1);
|
||||
const mapComponentPoint = camTransformWorldToMap(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) {
|
||||
MAP_COMPONENTS_HOVERED.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
function onKeyDown(event) {
|
||||
if (event.ctrlKey && event.code === "Space") {
|
||||
camResetView();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
This is the main script which organizes the drawing of the rail system map.
|
||||
*/
|
||||
|
||||
import { drawMap } from "./drawing";
|
||||
import { camResetView, camScale, camTransformMapToWorld } from "./mapCamera";
|
||||
import { initListeners, LAST_MOUSE_POINT } from "src/map/mapEventListener";
|
||||
|
||||
/**
|
||||
* The div containing the canvas element.
|
||||
* @type {Element | null}
|
||||
*/
|
||||
let mapContainerDiv = null;
|
||||
|
||||
/**
|
||||
* The canvas element.
|
||||
* @type {HTMLCanvasElement | null}
|
||||
*/
|
||||
export let MAP_CANVAS = null;
|
||||
|
||||
/**
|
||||
* The map's 2D rendering context.
|
||||
* @type {CanvasRenderingContext2D | null}
|
||||
*/
|
||||
export let MAP_CTX = null;
|
||||
|
||||
/**
|
||||
* The rail system that we're currently rendering.
|
||||
* @type {RailSystem | null}
|
||||
*/
|
||||
export let MAP_RAIL_SYSTEM = null;
|
||||
|
||||
/**
|
||||
* The set of components that are currently hovered over.
|
||||
* @type {Object[]}
|
||||
*/
|
||||
export const MAP_COMPONENTS_HOVERED = [];
|
||||
|
||||
export function initMap(rs) {
|
||||
MAP_RAIL_SYSTEM = rs;
|
||||
console.log("Initializing map for rail system: " + rs.name);
|
||||
camResetView();
|
||||
MAP_CANVAS = document.getElementById("railSystemMapCanvas");
|
||||
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
|
||||
MAP_CTX = MAP_CANVAS?.getContext("2d");
|
||||
|
||||
initListeners();
|
||||
|
||||
// Do an initial draw.
|
||||
draw();
|
||||
}
|
||||
|
||||
export function draw() {
|
||||
if (!(MAP_CANVAS && MAP_RAIL_SYSTEM && MAP_RAIL_SYSTEM.components)) {
|
||||
console.warn("Attempted to draw map without canvas or railSystem.");
|
||||
return;
|
||||
}
|
||||
if (MAP_CANVAS.width !== mapContainerDiv.clientWidth) {
|
||||
MAP_CANVAS.width = mapContainerDiv.clientWidth;
|
||||
}
|
||||
if (MAP_CANVAS.height !== mapContainerDiv.clientHeight) {
|
||||
MAP_CANVAS.height = mapContainerDiv.clientHeight - 6;
|
||||
}
|
||||
const width = MAP_CANVAS.width;
|
||||
const height = MAP_CANVAS.height;
|
||||
MAP_CTX.resetTransform();
|
||||
MAP_CTX.fillStyle = `rgb(240, 240, 240)`;
|
||||
MAP_CTX.fillRect(0, 0, width, height);
|
||||
|
||||
drawMap();
|
||||
drawDebugInfo();
|
||||
}
|
||||
|
||||
function drawDebugInfo() {
|
||||
MAP_CTX.resetTransform();
|
||||
MAP_CTX.fillStyle = "black";
|
||||
MAP_CTX.strokeStyle = "black";
|
||||
MAP_CTX.font = "10px Sans-Serif";
|
||||
const lastWorldPoint = camTransformMapToWorld(LAST_MOUSE_POINT);
|
||||
const lines = [
|
||||
"Scale factor: " + camScale(),
|
||||
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
|
||||
`Components: ${MAP_RAIL_SYSTEM.components.length}`,
|
||||
`Hovered components: ${MAP_COMPONENTS_HOVERED.length}`
|
||||
];
|
||||
for (let i = 0; i < MAP_COMPONENTS_HOVERED.length; i++) {
|
||||
lines.push(" " + MAP_COMPONENTS_HOVERED[i].name);
|
||||
}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
MAP_CTX.fillText(lines[i], 10, 20 + (i * 15));
|
||||
}
|
||||
}
|
||||
|
||||
export function isComponentHovered(component) {
|
||||
return MAP_COMPONENTS_HOVERED.some(c => c.id === component.id);
|
||||
}
|
||||
|
||||
export function isComponentSelected(component) {
|
||||
return MAP_RAIL_SYSTEM.selectedComponents.some(c => c.id === component.id);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="text-h2">About Rail Signal</div>
|
||||
<hr>
|
||||
<p>
|
||||
This application was developed by <a href="https://andrewlalis.github.io">Andrew Lalis</a>
|
||||
after several years of tinkering with various ways to automate rail
|
||||
networks in Minecraft. However, Rail Signal was designed from the
|
||||
ground up to be free from the constraints of any particular system.
|
||||
<em>Theoretically</em>, you could use Rail Signal to manage a real-world
|
||||
rail network, although I wouldn't recommend it, due to the complex and
|
||||
secure nature of real-world networks.
|
||||
</p>
|
||||
<p>
|
||||
Of course, Rail Signal was originally designed for Minecraft, so we've
|
||||
started by adding Rail Signal compatible drivers for Computercraft
|
||||
and Immersive Railroading by default. If you have a different system
|
||||
and would like to integrate it with Rail Signal, please <a href="https://github.com/andrewlalis/RailSignalAPI/issues" target="_blank">create a new issue</a>
|
||||
on the GitHub repository.
|
||||
</p>
|
||||
<div class="text-h4"><q-icon :name="fasMicrochip"/> Technologies</div>
|
||||
<p>
|
||||
This web app was built using <a href="https://quasar.dev">Quasar</a>
|
||||
and <a href="https://vuejs.org/">VueJS</a>. The API that powers this
|
||||
application was built using <a href="https://spring.io/">Spring</a>
|
||||
and Java 17. For more technical information, please visit Rail Signal's
|
||||
<a href="https://github.com/andrewlalis/RailSignalAPI">GitHub repository</a>.
|
||||
</p>
|
||||
<div class="text-h4"><q-icon :name="fasHeart"/> Support</div>
|
||||
<p>
|
||||
If you're enjoying this app, please consider making a <a href="https://paypal.me/andrewlalis" target="_blank">donation to
|
||||
Andrew's work on paypal</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {fasHeart, fasMicrochip} from "@quasar/extras/fontawesome-v6";
|
||||
|
||||
export default {
|
||||
name: "AboutPage",
|
||||
setup() {
|
||||
return {fasHeart, fasMicrochip};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -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,137 @@
|
|||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="text-h2">Welcome to Rail Signal</div>
|
||||
<hr>
|
||||
<p>
|
||||
To begin, expand the menu on the left, and select a rail system, or
|
||||
create a new one. The remainder of this page will serve as a quick
|
||||
guide on how to use Rail Signal to manage your rail system more
|
||||
effectively.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<index-page-section title="Introduction to Rail Networks">
|
||||
<q-img src="~assets/img/guide/layout.png"/>
|
||||
<p>
|
||||
The above diagram illustrates all of the basic concepts you need to
|
||||
know in order to build and manage your rail networks.
|
||||
</p>
|
||||
<p>
|
||||
Each rail system can be conceptually split up into lots of small
|
||||
<strong>segments</strong>, each of which represents a single part of
|
||||
the network that a single train should go through at once. For
|
||||
example, in our diagram, each shaded area is a segment. We only want
|
||||
one train to go through the junction at once, or there might be a
|
||||
crash!
|
||||
</p>
|
||||
<p>
|
||||
At the places where segments meet, we see a <strong style="color: #963ae0">segment boundary</strong>
|
||||
which is also denoted with a red dotted line for convenience. This is
|
||||
a physical point on a track where trains travel from one segment to
|
||||
another. What's special about segment boundaries is that they're where
|
||||
we can used devices to track trains moving in and out of segments. To
|
||||
put it simply, imagine there's a little computer next to each segment
|
||||
boundary point that sends a message saying, "Hey! A train just passed!"
|
||||
every time that it detects a train going over it.
|
||||
</p>
|
||||
<p>
|
||||
Now that we've covered segments and segment boundaries, we can now
|
||||
display a segment's status using a <strong>signal</strong>. A signal
|
||||
is a device that is linked to a segment, and whenever the segment's
|
||||
status updates (<em>when a train enters or leaves it</em>), the signal
|
||||
will be updated as well. Usually, signals are placed near the segment
|
||||
boundary, so that approaching trains know whether they're safe to
|
||||
continue, but with Rail Signal, you can place a signal anywhere, and
|
||||
connect it to any segment.
|
||||
</p>
|
||||
<p>
|
||||
Finally, unless you're just making a boring single-line loop, you'll
|
||||
most likely have some <strong style="color: #f5bc42">switches</strong>
|
||||
in your network. Switches are just sections of rail that allow trains
|
||||
to choose between two different paths to take. Rail Signal gives you
|
||||
the ability to manage these automatically, so you can use this web
|
||||
interface to configure switches instead of doing it manually.
|
||||
</p>
|
||||
</index-page-section>
|
||||
<index-page-section title="Paths and Path Nodes">
|
||||
<p>
|
||||
We mentioned segment boundaries and switches earlier, as simple
|
||||
components that you can add to your network in order to link it to the
|
||||
internet. There's more to it than that, however.
|
||||
</p>
|
||||
<p>
|
||||
Behind the scenes, your Rail Signal models your network as a set of
|
||||
<strong>path nodes</strong>, where each node can be connected to any
|
||||
other number of nodes. A train travels through your network by moving
|
||||
from node to node, until it reaches its desired destination. Both the
|
||||
segment boundary and switch are types of path nodes.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Segment boundaries may only be connected to at most two nodes. This
|
||||
is because a segment boundary is fundamentally just a point on a
|
||||
single rail line.
|
||||
</li>
|
||||
<li>
|
||||
Switches are connected to nodes based on their set of defined
|
||||
configurations. In the example diagram, our switch allows two
|
||||
possible configurations:
|
||||
<ul>
|
||||
<li>Between the <strong style="color: #3cadab">blue</strong> and <strong style="color: #7169b4">purple</strong> segments.</li>
|
||||
<li>Between the <strong style="color: #81d07b">green</strong> and <strong style="color: #7169b4">purple</strong> segments.</li>
|
||||
</ul>
|
||||
This implies that our switch node is connected to three other nodes:
|
||||
each of the segment boundaries that it allows traffic between.
|
||||
</li>
|
||||
</ul>
|
||||
</index-page-section>
|
||||
<index-page-section title="Drivers">
|
||||
<p>
|
||||
While you can play around in this web app as long as you'd like, the
|
||||
main point is to connect to an external rail system. That's done through
|
||||
a <strong>driver</strong>, which is a dedicated piece of code that sends
|
||||
and receives messages from the Rail Signal server. Usually, driver
|
||||
software will be installed into physical components in your system, like
|
||||
signals and trackside detectors, and switch levers. It's the responsibility
|
||||
of driver software to tell Rail Signal when a train crosses a segment
|
||||
boundary, or when a switch updates, or anything else it should know
|
||||
about.
|
||||
</p>
|
||||
<p>
|
||||
For Minecraft-related systems which use Lua computers, you can generally
|
||||
run <code>pastebin run jKyAiE8k</code> to run an installer script that
|
||||
automatically downloads the compatible drivers and walks you through an
|
||||
installation process.
|
||||
</p>
|
||||
</index-page-section>
|
||||
<index-page-section title="Advanced Usage">
|
||||
<q-img src="~assets/img/guide/layout2.png"/>
|
||||
<p>
|
||||
The above diagram shows a more typical network arrangement for a large
|
||||
scale, two-way mainline. Here, we see that each side of the main line
|
||||
has its own segment, so that trains can travel past each other without
|
||||
issue. We make the entire junction a single segment, so that only one
|
||||
train can pass through at a time. More advanced setups might have
|
||||
separate segments for bypass lines to avoid traffic jams. Beside each
|
||||
entrance and exit to the junction, we've placed a signal. On the
|
||||
outbound segments, the signal will report the status of the outbound
|
||||
segment, while on the inbound segments, the signal will show the
|
||||
status of the junction segment.
|
||||
</p>
|
||||
</index-page-section>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IndexPageSection from "components/IndexPageSection.vue";
|
||||
export default {
|
||||
name: "IndexPage",
|
||||
components: { IndexPageSection }
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<q-page>
|
||||
<div v-if="railSystem && railSystem.loaded">
|
||||
<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="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="settings">
|
||||
<settings-view :rail-system="railSystem"/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
<router-view />
|
||||
</div>
|
||||
|
||||
<q-inner-loading
|
||||
:showing="!railSystem || !railSystem.loaded"
|
||||
label="Loading rail system..."
|
||||
/>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
import MapView from "components/rs/MapView.vue";
|
||||
import SegmentsView from "components/rs/SegmentsView.vue";
|
||||
import SettingsView from "components/rs/SettingsView.vue";
|
||||
import { loadData, unloadData } from "src/api/railSystems";
|
||||
|
||||
export default {
|
||||
name: "RailSystemPage",
|
||||
components: { SettingsView, SegmentsView, MapView },
|
||||
data() {
|
||||
return {
|
||||
panel: "map",
|
||||
railSystem: null,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {rsStore};
|
||||
},
|
||||
mounted() {
|
||||
this.updateRailSystem();
|
||||
},
|
||||
created() {
|
||||
this.$watch(
|
||||
() => this.$route.params,
|
||||
this.updateRailSystem,
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
async updateRailSystem() {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
console.log(">>>> updating rail system.")
|
||||
if (this.railSystem) {
|
||||
this.rsStore.selectedRailSystem = null;
|
||||
await unloadData(this.railSystem);
|
||||
}
|
||||
if (this.$route.params.id) {
|
||||
const newRsId = parseInt(this.$route.params.id);
|
||||
const rs = this.rsStore.railSystems.find(r => r.id === newRsId);
|
||||
if (rs) {
|
||||
this.railSystem = rs;
|
||||
this.rsStore.selectedRailSystem = rs;
|
||||
await loadData(rs);
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -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,32 @@
|
|||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: () => import('pages/IndexPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: () => import('pages/AboutPage.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,14 @@
|
|||
import {defineStore} from "pinia";
|
||||
|
||||
export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||
state: () => ({
|
||||
/**
|
||||
* @type {RailSystem[]}
|
||||
*/
|
||||
railSystems: [],
|
||||
/**
|
||||
* @type {RailSystem | null}
|
||||
*/
|
||||
selectedRailSystem: null
|
||||
})
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -2,8 +2,11 @@ package nl.andrewl.railsignalapi;
|
|||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
|
||||
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(exclude = {
|
||||
UserDetailsServiceAutoConfiguration.class
|
||||
})
|
||||
public class RailSignalApiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Branch;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface BranchRepository extends JpaRepository<Branch, Long> {
|
||||
Optional<Branch> findByIdAndRailSystem(long id, RailSystem railSystem);
|
||||
Optional<Branch> findByIdAndRailSystemId(long id, long railSystemId);
|
||||
Optional<Branch> findByNameAndRailSystem(String name, RailSystem railSystem);
|
||||
List<Branch> findAllByRailSystemOrderByName(RailSystem railSystem);
|
||||
List<Branch> findAllByNameAndRailSystem(String name, RailSystem railSystem);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import nl.andrewl.railsignalapi.model.component.Position;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ComponentRepository<T extends Component> extends JpaRepository<T, Long>, JpaSpecificationExecutor<T> {
|
||||
Optional<T> findByIdAndRailSystemId(long id, long rsId);
|
||||
|
||||
boolean existsByNameAndRailSystem(String name, RailSystem rs);
|
||||
boolean existsByNameAndRailSystemId(String name, long rsId);
|
||||
|
||||
@Query("SELECT c FROM Component c " +
|
||||
"WHERE c.railSystem = :rs AND " +
|
||||
"c.position.x >= :#{#lower.x} AND c.position.y >= :#{#lower.y} AND c.position.z >= :#{#lower.z} AND " +
|
||||
"c.position.x <= :#{#upper.x} AND c.position.y <= :#{#upper.y} AND c.position.z <= :#{#upper.z}")
|
||||
List<T> findAllInBounds(RailSystem rs, Position lower, Position upper);
|
||||
|
||||
List<T> findAllByRailSystem(RailSystem rs);
|
||||
|
||||
void deleteAllByRailSystem(RailSystem rs);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.Label;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface LabelRepository extends JpaRepository<Label, Long> {
|
||||
List<Label> findAllByRailSystem(RailSystem rs);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.LinkToken;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface LinkTokenRepository extends JpaRepository<LinkToken, Long> {
|
||||
Iterable<LinkToken> findAllByTokenPrefix(String prefix);
|
||||
boolean existsByLabel(String label);
|
||||
List<LinkToken> findAllByRailSystem(RailSystem rs);
|
||||
Optional<LinkToken> findByIdAndRailSystemId(long ltId, long rsId);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SegmentRepository extends JpaRepository<Segment, Long> {
|
||||
boolean existsByNameAndRailSystem(String name, RailSystem rs);
|
||||
|
||||
List<Segment> findAllByRailSystemIdOrderByName(long rsId);
|
||||
|
||||
Optional<Segment> findByIdAndRailSystemId(long id, long rsId);
|
||||
|
||||
void deleteAllByRailSystem(RailSystem rs);
|
||||
}
|
|
@ -1,25 +1,8 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Branch;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.Signal;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SignalRepository extends JpaRepository<Signal, Long> {
|
||||
Optional<Signal> findByIdAndRailSystem(long id, RailSystem railSystem);
|
||||
Optional<Signal> findByIdAndRailSystemId(long id, long railSystemId);
|
||||
boolean existsByNameAndRailSystem(String name, RailSystem railSystem);
|
||||
|
||||
@Query("SELECT DISTINCT s FROM Signal s " +
|
||||
"LEFT JOIN s.branchConnections bc " +
|
||||
"WHERE bc.branch = :branch")
|
||||
List<Signal> findAllConnectedToBranch(Branch branch);
|
||||
|
||||
List<Signal> findAllByRailSystemOrderByName(RailSystem railSystem);
|
||||
public interface SignalRepository extends ComponentRepository<Signal> {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.PathNode;
|
||||
import nl.andrewl.railsignalapi.model.component.SwitchConfiguration;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SwitchConfigurationRepository extends JpaRepository<SwitchConfiguration, Long> {
|
||||
void deleteAllByNodesContaining(PathNode p);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.Switch;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SwitchRepository extends ComponentRepository<Switch> {
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Train;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TrainRepository extends JpaRepository<Train, Long> {
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package nl.andrewl.railsignalapi.live;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* A downlink connection to one or more components (linked by a {@link nl.andrewl.railsignalapi.model.LinkToken})
|
||||
* which we can send messages to.
|
||||
*/
|
||||
public abstract class ComponentDownlink {
|
||||
@Getter
|
||||
private final long tokenId;
|
||||
|
||||
public ComponentDownlink(long tokenId) {
|
||||
this.tokenId = tokenId;
|
||||
}
|
||||
|
||||
public abstract void send(Object msg) throws Exception;
|
||||
public abstract void shutdown() throws Exception;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof ComponentDownlink cd && cd.tokenId == this.tokenId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Long.hashCode(tokenId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package nl.andrewl.railsignalapi.live;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentDataMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.SegmentStatusMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.live.websocket.AppUpdateService;
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||
import nl.andrewl.railsignalapi.model.component.Switch;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A service that manages all the active component downlink connections.
|
||||
*
|
||||
* We keep track of active component downlinks by storing a mapping which maps
|
||||
* each downlink to the set of components it is responsible for, and another
|
||||
* mapping that maps each online component to the set of downlinks that are
|
||||
* responsible for it.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ComponentDownlinkService {
|
||||
private final Map<ComponentDownlink, Set<Long>> componentDownlinks = new HashMap<>();
|
||||
private final Map<Long, Set<ComponentDownlink>> downlinksByCId = new HashMap<>();
|
||||
|
||||
private final LinkTokenRepository tokenRepository;
|
||||
private final ComponentRepository<Component> componentRepository;
|
||||
private final AppUpdateService appUpdateService;
|
||||
|
||||
/**
|
||||
* Registers a new active downlink to one or more components. Sets all
|
||||
* linked components as online, and sends messages to any connected apps
|
||||
* to notify them of the update components.
|
||||
* @param downlink The downlink to register.
|
||||
*/
|
||||
@Transactional
|
||||
public synchronized void registerDownlink(ComponentDownlink downlink) throws Exception {
|
||||
Set<Component> components = tokenRepository.findById(downlink.getTokenId()).orElseThrow().getComponents();
|
||||
componentDownlinks.put(downlink, components.stream().map(Component::getId).collect(Collectors.toSet()));
|
||||
for (var c : components) {
|
||||
c.setOnline(true);
|
||||
componentRepository.saveAndFlush(c); // Make sure to flush, so that online status is immediately visible everywhere.
|
||||
|
||||
// Immediately send a data message to the downlink and app for each component that comes online.
|
||||
var msg = new ComponentDataMessage(c);
|
||||
downlink.send(msg);
|
||||
appUpdateService.sendUpdate(c.getRailSystem().getId(), msg);
|
||||
|
||||
// Send initial data updates to make sure that devices' state is up to date immediately.
|
||||
if (c instanceof Signal sig) {
|
||||
downlink.send(new SegmentStatusMessage(c.getId(), sig.getSegment().getId(), sig.getSegment().isOccupied()));
|
||||
} else if (c instanceof Switch sw) {
|
||||
long activeConfigId;
|
||||
if (sw.getActiveConfiguration() != null) {
|
||||
activeConfigId = sw.getActiveConfiguration().getId();
|
||||
} else {
|
||||
activeConfigId = sw.getPossibleConfigurations().stream().findAny().orElseThrow().getId();
|
||||
}
|
||||
downlink.send(new SwitchUpdateMessage(c.getId(), activeConfigId));
|
||||
}
|
||||
|
||||
Set<ComponentDownlink> downlinks = downlinksByCId.computeIfAbsent(c.getId(), aLong -> new HashSet<>());
|
||||
downlinks.add(downlink);
|
||||
}
|
||||
log.info("Registered downlink with token id {}.", downlink.getTokenId());
|
||||
}
|
||||
|
||||
/**
|
||||
* De-registers a downlink to components. This should be called when this
|
||||
* downlink is closed.
|
||||
* @param downlink The downlink to de-register.
|
||||
*/
|
||||
@Transactional
|
||||
public synchronized void deregisterDownlink(ComponentDownlink downlink) {
|
||||
Set<Long> componentIds = componentDownlinks.remove(downlink);
|
||||
if (componentIds != null) {
|
||||
for (var cId : componentIds) {
|
||||
componentRepository.findById(cId).ifPresent(component -> {
|
||||
component.setOnline(false);
|
||||
componentRepository.save(component);
|
||||
appUpdateService.sendComponentUpdate(component.getRailSystem().getId(), component.getId());
|
||||
});
|
||||
Set<ComponentDownlink> downlinks = downlinksByCId.get(cId);
|
||||
if (downlinks != null) {
|
||||
downlinks.remove(downlink);
|
||||
if (downlinks.isEmpty()) {
|
||||
downlinksByCId.remove(cId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
downlink.shutdown();
|
||||
} catch (Exception e) {
|
||||
log.warn("An error occurred while shutting down a component downlink.", e);
|
||||
}
|
||||
log.info("De-registered downlink with token id {}.", downlink.getTokenId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public synchronized void deregisterDownlink(long tokenId) {
|
||||
List<ComponentDownlink> removeSet = componentDownlinks.keySet().stream()
|
||||
.filter(downlink -> downlink.getTokenId() == tokenId).toList();
|
||||
for (var downlink : removeSet) {
|
||||
deregisterDownlink(downlink);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(long componentId, Object msg) {
|
||||
var downlinks = downlinksByCId.get(componentId);
|
||||
if (downlinks != null) {
|
||||
for (var downlink : downlinks) {
|
||||
try {
|
||||
downlink.send(msg);
|
||||
} catch (Exception e) {
|
||||
log.warn("An error occurred while sending a message to downlink with token id " + downlink.getTokenId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(ComponentMessage msg) {
|
||||
sendMessage(msg.cId, msg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package nl.andrewl.railsignalapi.live;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.SegmentBoundaryUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.service.SegmentService;
|
||||
import nl.andrewl.railsignalapi.service.SwitchService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* A central service that manages all incoming component messages from any
|
||||
* connected component links.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ComponentUplinkMessageHandler {
|
||||
private final SwitchService switchService;
|
||||
private final SegmentService segmentService;
|
||||
|
||||
@Transactional
|
||||
public void messageReceived(ComponentMessage msg) {
|
||||
if (msg instanceof SegmentBoundaryUpdateMessage sb) {
|
||||
segmentService.onBoundaryUpdate(sb);
|
||||
} else if (msg instanceof SwitchUpdateMessage sw) {
|
||||
switchService.onSwitchUpdate(sw);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
|
||||
|
||||
/**
|
||||
* A message that's sent to devices which contains the full component response
|
||||
* for a specific component.
|
||||
*/
|
||||
@Getter
|
||||
public class ComponentDataMessage extends ComponentMessage {
|
||||
private final ComponentResponse data;
|
||||
|
||||
public ComponentDataMessage(Component c) {
|
||||
super(c.getId(), "COMPONENT_DATA");
|
||||
this.data = ComponentResponse.of(c);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Base class for all messages that will be sent to components.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = SegmentBoundaryUpdateMessage.class, name = "SEGMENT_BOUNDARY_UPDATE"),
|
||||
@JsonSubTypes.Type(value = SwitchUpdateMessage.class, name = "SWITCH_UPDATE"),
|
||||
@JsonSubTypes.Type(value = ErrorMessage.class, name = "ERROR")
|
||||
})
|
||||
@NoArgsConstructor
|
||||
public abstract class ComponentMessage {
|
||||
/**
|
||||
* The id of the component that this message is for.
|
||||
*/
|
||||
public long cId;
|
||||
|
||||
/**
|
||||
* The type of message.
|
||||
*/
|
||||
public String type;
|
||||
|
||||
public ComponentMessage(long cId, String type) {
|
||||
this.cId = cId;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
/**
|
||||
* A message that's sent regarding an error that occurred.
|
||||
*/
|
||||
public class ErrorMessage extends ComponentMessage {
|
||||
public String message;
|
||||
|
||||
public ErrorMessage(long cId, String message) {
|
||||
super(cId, "ERROR");
|
||||
this.message = message;
|
||||
}
|
||||
}
|