Added inline component adding form.

This commit is contained in:
Andrew Lalis 2022-06-03 19:07:12 +02:00
parent ad18c1b3d4
commit 9c0d588543
8 changed files with 432 additions and 234 deletions

View File

@ -48,3 +48,16 @@ export function removeSegment(rs, segmentId) {
.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);
})
}

View File

@ -5,176 +5,18 @@
Your browser doesn't support canvas. Your browser doesn't support canvas.
</canvas> </canvas>
</div> </div>
<q-scroll-area class="col-md-4" v-if="railSystem.selectedComponents.length > 0"> <q-scroll-area class="col-md-4">
<div class="row" v-for="component in railSystem.selectedComponents" :key="component.id"> <div class="row" v-for="component in railSystem.selectedComponents" :key="component.id">
<div class="col full-width"> <div class="col full-width">
<selected-component-view :component="component"/> <selected-component-view :component="component"/>
</div> </div>
</div> </div>
<add-component-form v-if="addComponent.visible" :rail-system="railSystem" @created="addComponent.visible = false"/>
</q-scroll-area> </q-scroll-area>
</div> </div>
<q-page-sticky position="bottom-right" :offset="[25, 25]"> <q-page-sticky position="bottom-right" :offset="[25, 25]">
<q-fab icon="add" direction="up" color="accent"> <q-fab icon="add" color="accent" v-model="addComponent.visible"/>
<q-fab-action @click="addSignalData.toggle = true">
<q-icon><img src="~assets/icons/signal_icon.svg"/></q-icon>
<q-tooltip>Add Signal</q-tooltip>
</q-fab-action>
<q-fab-action @click="addSegmentBoundaryData.toggle = true">
<q-icon><img src="~assets/icons/segment-boundary_icon.svg"/></q-icon>
<q-tooltip>Add Segment Boundary</q-tooltip>
</q-fab-action>
<q-fab-action @click="addSwitchData.toggle = true">
<q-icon><img src="~assets/icons/switch_icon.svg"/></q-icon>
<q-tooltip>Add Switch</q-tooltip>
</q-fab-action>
<q-fab-action @click="addLabelData.toggle = true">
<q-icon><img src="~assets/icons/label_icon.svg"/></q-icon>
<q-tooltip>Add Label</q-tooltip>
</q-fab-action>
</q-fab>
</q-page-sticky> </q-page-sticky>
<!-- Add Signal Dialog -->
<add-component-dialog
v-model="addSignalData"
type="SIGNAL"
:rail-system="railSystem"
title="Add Signal"
success-message="Signal added."
>
<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>
<!-- Add Segment boundary -->
<add-component-dialog
title="Add Segment Boundary"
success-message="Segment boundary added."
:rail-system="railSystem"
type="SEGMENT_BOUNDARY"
v-model="addSegmentBoundaryData"
>
<template #subtitle>
<p>
Add a segment boundary to the rail system.
</p>
</template>
<template #default>
<q-card-section>
<q-select
v-model="addSegmentBoundaryData.segments"
:options="railSystem.segments"
multiple
:option-value="segment => segment"
:option-label="segment => segment.name"
label="Segments"
/>
</q-card-section>
</template>
</add-component-dialog>
<!-- Add Switch dialog -->
<add-component-dialog
title="Add Switch"
success-message="Switch added."
:rail-system="railSystem"
type="SWITCH"
v-model="addSwitchData"
>
<template #subtitle>
<p>
Add a switch to the rail system.
</p>
</template>
<template #default>
<q-card-section>
<div class="row">
<q-list>
<q-item
v-for="config in addSwitchData.possibleConfigurations"
: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>
</q-list>
</div>
<div class="row">
<div class="col-sm-6">
<q-select
v-model="addSwitchData.pathNode1"
:options="getEligibleSwitchNodes()"
:option-value="segment => segment"
:option-label="segment => segment.name"
label="First Path Node"
/>
</div>
<div class="col-sm-6">
<q-select
v-model="addSwitchData.pathNode2"
:options="getEligibleSwitchNodes()"
:option-value="segment => segment"
:option-label="segment => segment.name"
label="Second Path Node"
/>
</div>
</div>
<div class="row">
<q-btn label="Add Configuration" color="primary" @click="addSwitchConfiguration"/>
</div>
</q-card-section>
</template>
</add-component-dialog>
<!-- Add Label dialog -->
<add-component-dialog
title="Add Label"
success-message="Added label."
:rail-system="railSystem"
type="LABEL"
v-model="addLabelData"
>
<template #subtitle>
<p>
Add a label to the rail system as a piece of text on the map. Labels
are purely a visual component, and do not interact with the system
in any way, besides being a helpful point of reference for users.
</p>
</template>
<template #default>
<q-card-section>
<q-input
label="Label Text"
type="text"
v-model="addLabelData.text"
/>
</q-card-section>
</template>
</add-component-dialog>
</template> </template>
<script> <script>
@ -182,11 +24,11 @@ import {RailSystem} from "src/api/railSystems";
import {draw, initMap} from "src/render/mapRenderer"; import {draw, initMap} from "src/render/mapRenderer";
import SelectedComponentView from "components/rs/SelectedComponentView.vue"; import SelectedComponentView from "components/rs/SelectedComponentView.vue";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import AddComponentDialog from "components/rs/add_component/AddComponentDialog.vue"; import AddComponentForm from "components/rs/add_component/AddComponentForm.vue";
export default { export default {
name: "MapView", name: "MapView",
components: { AddComponentDialog, SelectedComponentView }, components: { AddComponentForm, SelectedComponentView },
setup() { setup() {
const quasar = useQuasar(); const quasar = useQuasar();
return {quasar}; return {quasar};
@ -199,40 +41,8 @@ export default {
}, },
data() { data() {
return { return {
addSignalData: { addComponent: {
name: "", visible: false
position: {
x: 0, y: 0, z: 0
},
segment: null,
toggle: false
},
addSegmentBoundaryData: {
name: "",
position: {
x: 0, y: 0, z: 0
},
toggle: false,
segments: [],
connectedNodes: []
},
addSwitchData: {
name: "",
position: {
x: 0, y: 0, z: 0
},
toggle: false,
possibleConfigurations: [],
// Utility properties for the UI for adding configurations.
pathNode1: null,
pathNode2: null
},
addLabelData: {
position: {
x: 0, y: 0, z: 0
},
toggle: false,
text: ""
} }
} }
}, },
@ -249,41 +59,6 @@ export default {
}, },
deep: true deep: true
} }
},
methods: {
getEligibleSwitchNodes() {
return this.railSystem.components.filter(component => {
return component.connectedNodes !== undefined && component.connectedNodes !== null;
});
},
addSwitchConfiguration() {
if (
this.addSwitchData.pathNode1 === null ||
this.addSwitchData.pathNode2 === null ||
this.addSwitchData.pathNode1.id === this.addSwitchData.pathNode2.id ||
this.addSwitchData.possibleConfigurations.some(config => {
// Check if there's already a configuration containing both of these nodes.
return config.nodes.every(node =>
node.id === this.addSwitchData.pathNode1.id ||
node.id === this.addSwitchData.pathNode2.id);
})
) {
this.quasar.notify({
color: "warning",
message: "Invalid switch configuration."
});
return;
}
// All good!
this.addSwitchData.possibleConfigurations.push({
nodes: [
this.addSwitchData.pathNode1,
this.addSwitchData.pathNode2
],
// A unique key, just for the frontend to use. This is not used by the API.
key: this.addSwitchData.pathNode1.id + "_" + this.addSwitchData.pathNode2.id
});
}
} }
}; };
</script> </script>

View File

@ -13,9 +13,15 @@
<q-item-label>{{segment.name}}</q-item-label> <q-item-label>{{segment.name}}</q-item-label>
<q-item-label caption>Id: {{segment.id}}</q-item-label> <q-item-label caption>Id: {{segment.id}}</q-item-label>
</q-item-section> </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-menu touch-position context-menu>
<q-list dense style="min-width: 100px"> <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 clickable v-close-popup @click="remove(segment)">
<q-item-section>Delete</q-item-section> <q-item-section>Delete</q-item-section>
</q-item> </q-item>
@ -68,7 +74,7 @@
import { useRailSystemsStore } from "stores/railSystemsStore"; import { useRailSystemsStore } from "stores/railSystemsStore";
import { RailSystem } from "src/api/railSystems"; import { RailSystem } from "src/api/railSystems";
import { useQuasar } from "quasar"; import { useQuasar } from "quasar";
import { createSegment, removeSegment } from "src/api/segments"; import { createSegment, removeSegment, toggleOccupied } from "src/api/segments";
import { ref } from "vue"; import { ref } from "vue";
export default { export default {
@ -115,6 +121,9 @@ export default {
onReset() { onReset() {
this.segmentName = ""; this.segmentName = "";
}, },
toggleOccupiedInline(segment) {
toggleOccupied(this.rsStore.selectedRailSystem, segment.id)
},
remove(segment) { remove(segment) {
this.quasar.dialog({ this.quasar.dialog({
title: "Confirm", title: "Confirm",

View File

@ -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>

View File

@ -10,6 +10,38 @@
<q-item-label caption>ID: {{segment.id}}</q-item-label> <q-item-label caption>ID: {{segment.id}}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </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/> <q-separator/>
<path-node-item :path-node="segmentBoundary" :editable="true"/> <path-node-item :path-node="segmentBoundary" :editable="true"/>
</base-component-view> </base-component-view>
@ -18,6 +50,9 @@
<script> <script>
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue"; import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
import PathNodeItem from "components/rs/component_views/PathNodeItem.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 { export default {
name: "SegmentBoundaryComponentView", name: "SegmentBoundaryComponentView",
components: {PathNodeItem, BaseComponentView}, components: {PathNodeItem, BaseComponentView},
@ -26,6 +61,54 @@ export default {
type: Object, type: Object,
required: true 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> </script>

View File

@ -23,7 +23,7 @@ const hoveredElements = [];
export function initMap(rs) { export function initMap(rs) {
railSystem = rs; railSystem = rs;
console.log("Initializing map for rail system: " + rs.name); console.log("Initializing map for rail system: " + rs.name);
hoveredElements.length = 0; resetView();
mapCanvas = document.getElementById("railSystemMapCanvas"); mapCanvas = document.getElementById("railSystemMapCanvas");
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer"); mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
mapCanvas.removeEventListener("wheel", onMouseWheel); mapCanvas.removeEventListener("wheel", onMouseWheel);
@ -39,6 +39,16 @@ export function initMap(rs) {
draw(); draw();
} }
function resetView() {
mapTranslation.x = 0;
mapTranslation.y = 0;
mapDragOrigin = null;
mapDragTranslation = null;
lastMousePoint = new DOMPoint(0, 0, 0, 0);
hoveredElements.length = 0;
mapScaleIndex = SCALE_INDEX_NORMAL;
}
export function draw() { export function draw() {
if (!(mapCanvas && railSystem && railSystem.components)) { if (!(mapCanvas && railSystem && railSystem.components)) {
console.warn("Attempted to draw map without canvas or railSystem."); console.warn("Attempted to draw map without canvas or railSystem.");

View File

@ -36,4 +36,9 @@ public class SegmentsApiController {
segmentService.remove(rsId, sId); segmentService.remove(rsId, sId);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@PatchMapping(path = "/{sId}/occupied")
public FullSegmentResponse toggleOccupied(@PathVariable long rsId, @PathVariable long sId) {
return segmentService.toggleOccupied(rsId, sId);
}
} }

View File

@ -76,6 +76,16 @@ public class SegmentService {
} }
} }
@Transactional
public FullSegmentResponse toggleOccupied(long rsId, long sId) {
var segment = segmentRepository.findByIdAndRailSystemId(sId, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
segment.setOccupied(!segment.isOccupied());
segmentRepository.save(segment);
sendSegmentOccupiedStatus(segment);
return new FullSegmentResponse(segment);
}
/** /**
* Handles updates from segment boundary components. * Handles updates from segment boundary components.
* @param msg The update message. * @param msg The update message.