diff --git a/railsignal-app/package-lock.json b/railsignal-app/package-lock.json index 98c6a16..2f97e8a 100644 --- a/railsignal-app/package-lock.json +++ b/railsignal-app/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^0.27.2", + "bootstrap": "^4.6.1", "pinia": "^2.0.14", "three": "^0.140.0", "vue": "^3.2.33", @@ -285,6 +286,19 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "node_modules/bootstrap": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz", + "integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1248,6 +1262,12 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "peer": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1500,6 +1520,17 @@ } } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/postcss": { "version": "8.4.13", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", @@ -2130,6 +2161,12 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "bootstrap": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz", + "integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==", + "requires": {} + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2744,6 +2781,12 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "peer": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2924,6 +2967,12 @@ } } }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "peer": true + }, "postcss": { "version": "8.4.13", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", diff --git a/railsignal-app/src/App.vue b/railsignal-app/src/App.vue index 10a2fad..c2becb2 100644 --- a/railsignal-app/src/App.vue +++ b/railsignal-app/src/App.vue @@ -4,10 +4,7 @@ - + diff --git a/railsignal-app/src/components/RailSystemsManager.vue b/railsignal-app/src/components/RailSystemsManager.vue index 568c206..8664935 100644 --- a/railsignal-app/src/components/RailSystemsManager.vue +++ b/railsignal-app/src/components/RailSystemsManager.vue @@ -26,6 +26,12 @@ export default { name: "RailSystemsManager.vue", setup() { const rsStore = useRailSystemsStore(); + rsStore.$subscribe(mutation => { + const evt = mutation.events; + if (evt.key === "selectedRailSystem" && evt.newValue !== null) { + rsStore.fetchSelectedRailSystemData(); + } + }); return { rsStore }; diff --git a/railsignal-app/src/components/railsystem/AddSegment.vue b/railsignal-app/src/components/railsystem/AddSegment.vue index 9e4f637..f3c3aa1 100644 --- a/railsignal-app/src/components/railsystem/AddSegment.vue +++ b/railsignal-app/src/components/railsystem/AddSegment.vue @@ -1,14 +1,23 @@ - - - - \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/MapView.vue b/railsignal-app/src/components/railsystem/MapView.vue index c0c1d0f..8a61c5c 100644 --- a/railsignal-app/src/components/railsystem/MapView.vue +++ b/railsignal-app/src/components/railsystem/MapView.vue @@ -1,26 +1,32 @@ \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/SegmentsView.vue b/railsignal-app/src/components/railsystem/SegmentsView.vue new file mode 100644 index 0000000..65e5673 --- /dev/null +++ b/railsignal-app/src/components/railsystem/SegmentsView.vue @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/component/AddSegmentBoundary.vue b/railsignal-app/src/components/railsystem/component/AddSegmentBoundary.vue new file mode 100644 index 0000000..ae63966 --- /dev/null +++ b/railsignal-app/src/components/railsystem/component/AddSegmentBoundary.vue @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/component/AddSignal.vue b/railsignal-app/src/components/railsystem/component/AddSignal.vue new file mode 100644 index 0000000..6313c47 --- /dev/null +++ b/railsignal-app/src/components/railsystem/component/AddSignal.vue @@ -0,0 +1,62 @@ + + + + + \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/component/ComponentView.vue b/railsignal-app/src/components/railsystem/component/ComponentView.vue new file mode 100644 index 0000000..5332df1 --- /dev/null +++ b/railsignal-app/src/components/railsystem/component/ComponentView.vue @@ -0,0 +1,55 @@ + + + + + \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/component/PathNodeComponentView.vue b/railsignal-app/src/components/railsystem/component/PathNodeComponentView.vue new file mode 100644 index 0000000..4f4c1ed --- /dev/null +++ b/railsignal-app/src/components/railsystem/component/PathNodeComponentView.vue @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/component/SegmentBoundaryNodeComponentView.vue b/railsignal-app/src/components/railsystem/component/SegmentBoundaryNodeComponentView.vue new file mode 100644 index 0000000..b2f3ba9 --- /dev/null +++ b/railsignal-app/src/components/railsystem/component/SegmentBoundaryNodeComponentView.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/component/SignalComponentView.vue b/railsignal-app/src/components/railsystem/component/SignalComponentView.vue new file mode 100644 index 0000000..a9b0833 --- /dev/null +++ b/railsignal-app/src/components/railsystem/component/SignalComponentView.vue @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/railsignal-app/src/components/railsystem/mapRenderer.js b/railsignal-app/src/components/railsystem/mapRenderer.js new file mode 100644 index 0000000..17007f8 --- /dev/null +++ b/railsignal-app/src/components/railsystem/mapRenderer.js @@ -0,0 +1,248 @@ +/* +This component is responsible for the rendering of a RailSystem in a 2d map +view. + */ + +const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0]; +const SCALE_INDEX_NORMAL = 7; +const HOVER_RADIUS = 10; + +let mapCanvas = null; +let railSystem = null; + +let mapScaleIndex = SCALE_INDEX_NORMAL; +let mapTranslation = {x: 0, y: 0}; +let mapDragOrigin = null; +let mapDragTranslation = null; +let lastMousePoint = new DOMPoint(0, 0, 0, 0); +const hoveredElements = []; + +export function initMap(rs) { + railSystem = rs; + console.log("Initializing map for rail system: " + rs.name); + hoveredElements.length = 0; + mapCanvas = document.getElementById("railSystemMapCanvas"); + mapCanvas.removeEventListener("wheel", onMouseWheel); + mapCanvas.addEventListener("wheel", onMouseWheel); + mapCanvas.removeEventListener("mousedown", onMouseDown); + mapCanvas.addEventListener("mousedown", onMouseDown); + mapCanvas.removeEventListener("mouseup", onMouseUp); + mapCanvas.addEventListener("mouseup", onMouseUp); + mapCanvas.removeEventListener("mousemove", onMouseMove); + mapCanvas.addEventListener("mousemove", onMouseMove); + + // Do an initial draw. + draw(); +} + +export function draw() { + if (!(mapCanvas && railSystem && railSystem.components)) { + console.warn("Attempted to draw map without canvas or railSystem."); + return; + } + const ctx = mapCanvas.getContext("2d"); + const width = mapCanvas.width; + const height = mapCanvas.height; + ctx.resetTransform(); + ctx.fillStyle = `rgb(240, 240, 240)`; + ctx.fillRect(0, 0, width, height); + const worldTx = getWorldTransform(); + ctx.setTransform(worldTx); + + for (let i = 0; i < railSystem.components.length; i++) { + drawComponent(ctx, worldTx, railSystem.components[i]); + } + + // Draw debug info. + ctx.resetTransform(); + ctx.fillStyle = "black"; + ctx.strokeStyle = "black"; + ctx.font = "10px Sans-Serif"; + const lastWorldPoint = mapPointToWorld(lastMousePoint); + const lines = [ + "Scale factor: " + getScaleFactor(), + `(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`, + `Components: ${railSystem.components.length}`, + `Hovered elements: ${hoveredElements.length}` + ] + for (let i = 0; i < lines.length; i++) { + ctx.fillText(lines[i], 10, 20 + (i * 15)); + } +} + +function drawComponent(ctx, worldTx, component) { + const tx = DOMMatrix.fromMatrix(worldTx); + tx.translateSelf(component.position.x, component.position.z, 0); + const s = getScaleFactor(); + tx.scaleSelf(1/s, 1/s, 1/s); + tx.scaleSelf(5, 5, 5); + ctx.setTransform(tx); + if (isComponentHovered(component)) { + ctx.fillStyle = `rgba(255, 255, 0, 32)`; + ctx.beginPath(); + ctx.ellipse(0, 0, 1.8, 1.8, 0, 0, Math.PI * 2); + ctx.fill(); + } + if (component.type === "SIGNAL") { + drawSignal(ctx, component); + } else if (component.type === "SEGMENT_BOUNDARY") { + drawSegmentBoundary(ctx, component); + } +} + +function drawSignal(ctx, signal) { + roundedRect(ctx, -0.7, -1, 1.4, 2, 0.25); + ctx.fillStyle = "black"; + ctx.fill(); + + // ctx.fillStyle = "green"; + // ctx.beginPath(); + // ctx.ellipse(0, 0, 0.8, 0.8, 0, 0, Math.PI * 2); + // ctx.fill(); + // + // ctx.strokeStyle = "black"; + // ctx.lineWidth = 0.5; + // ctx.beginPath(); + // ctx.ellipse(0, 0, 1, 1, 0, 0, Math.PI * 2); + // ctx.stroke(); +} + +function drawSegmentBoundary(ctx, segmentBoundary) { + ctx.fillStyle = "blue"; + ctx.beginPath(); + ctx.ellipse(0, 0, 1, 1, 0, 0, Math.PI * 2); + ctx.fill(); +} + +export function getScaleFactor() { + return SCALE_VALUES[mapScaleIndex]; +} + +function getWorldTransform() { + const canvasRect = mapCanvas.getBoundingClientRect(); + const scale = getScaleFactor(); + const tx = new DOMMatrix(); + tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0); + tx.scaleSelf(scale, scale, scale); + tx.translateSelf(mapTranslation.x, mapTranslation.y, 0); + if (mapDragOrigin !== null && mapDragTranslation !== null) { + tx.translateSelf(mapDragTranslation.x, mapDragTranslation.y, 0); + } + return tx; +} + +function isComponentHovered(component) { + for (let i = 0; i < hoveredElements.length; i++) { + if (hoveredElements[i].id === component.id) return true; + } + return false; +} + +/** + * Maps a point on the map coordinates to world coordinates. + * @param {DOMPoint} p + * @returns {DOMPoint} + */ +function mapPointToWorld(p) { + return getWorldTransform().invertSelf().transformPoint(p); +} + +/** + * Maps a point in the world to map coordinates. + * @param {DOMPoint} p + * @returns {DOMPoint} + */ +function worldPointToMap(p) { + return getWorldTransform().transformPoint(p); +} + +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(); +} + +/* +EVENT HANDLING +*/ + +/** + * @param {WheelEvent} event + */ +function onMouseWheel(event) { + const s = event.deltaY; + if (s > 0) { + mapScaleIndex = Math.max(0, mapScaleIndex - 1); + } else if (s < 0) { + mapScaleIndex = Math.min(SCALE_VALUES.length - 1, mapScaleIndex + 1); + } + draw(); + event.stopPropagation(); +} + +/** + * @param {MouseEvent} event + */ +function onMouseDown(event) { + const p = getMousePoint(event); + mapDragOrigin = {x: p.x, y: p.y}; +} + +function onMouseUp() { + if (mapDragTranslation !== null) { + mapTranslation.x += mapDragTranslation.x; + mapTranslation.y += mapDragTranslation.y; + } + if (hoveredElements.length === 1) { + railSystem.selectedComponent = hoveredElements[0]; + } else { + railSystem.selectedComponent = null; + } + mapDragOrigin = null; + mapDragTranslation = null; +} + +/** + * @param {MouseEvent} event + */ +function onMouseMove(event) { + const p = getMousePoint(event); + lastMousePoint = p; + if (mapDragOrigin !== null) { + const scale = getScaleFactor(); + const dx = p.x - mapDragOrigin.x; + const dy = p.y - mapDragOrigin.y; + mapDragTranslation = {x: dx / scale, y: dy / scale}; + } else { + hoveredElements.length = 0; + // Populate with list of hovered elements. + for (let i = 0; i < railSystem.components.length; i++) { + const c = railSystem.components[i]; + const componentPoint = new DOMPoint(c.position.x, c.position.z, 0, 1); + const mapComponentPoint = worldPointToMap(componentPoint); + const dist2 = (p.x - mapComponentPoint.x) * (p.x - mapComponentPoint.x) + (p.y - mapComponentPoint.y) * (p.y - mapComponentPoint.y); + if (dist2 < HOVER_RADIUS * HOVER_RADIUS) { + hoveredElements.push(c); + } + } + } + draw(); +} + +/** + * Gets the point at which the user clicked on the map. + * @param {MouseEvent} event + * @returns {DOMPoint} + */ +function getMousePoint(event) { + const rect = mapCanvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + return new DOMPoint(x, y, 0, 1); +} diff --git a/railsignal-app/src/stores/railSystemsStore.js b/railsignal-app/src/stores/railSystemsStore.js index d9747b1..ac9e411 100644 --- a/railsignal-app/src/stores/railSystemsStore.js +++ b/railsignal-app/src/stores/railSystemsStore.js @@ -4,13 +4,16 @@ import axios from "axios"; export const useRailSystemsStore = defineStore('RailSystemsStore', { state: () => ({ railSystems: [], + /** + * @type {{segments: [Object], components: [Object], selectedComponent: Object} | null} + */ selectedRailSystem: null, - selectedComponent: null + apiUrl: import.meta.env.VITE_API_URL }), actions: { refreshRailSystems() { return new Promise((resolve, reject) => { - axios.get(import.meta.env.VITE_API_URL + "/rs") + axios.get(this.apiUrl + "/rs") .then(response => { this.railSystems = response.data; resolve(); @@ -22,10 +25,7 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', { }, createRailSystem(name) { return new Promise((resolve, reject) => { - axios.post( - import.meta.env.VITE_API_URL + "/rs", - {name: name} - ) + axios.post(this.apiUrl + "/rs", {name: name}) .then(() => { this.refreshRailSystems() .then(() => resolve()) @@ -36,13 +36,69 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', { }, removeRailSystem(rs) { return new Promise((resolve, reject) => { - axios.delete(import.meta.env.VITE_API_URL + "/rs/" + rs.id) + axios.delete(this.apiUrl + "/rs/" + rs.id) .then(() => { + this.selectedRailSystem = null; this.refreshRailSystems() .then(() => resolve) .catch(error => reject(error)); }) }) + }, + refreshSegments(rs) { + return new Promise(resolve => { + axios.get(`${this.apiUrl}/rs/${rs.id}/s`) + .then(response => { + rs.segments = response.data; + resolve(); + }); + }); + }, + refreshComponents(rs) { + return new Promise(resolve => { + axios.get(`${this.apiUrl}/rs/${rs.id}/c`) + .then(response => { + rs.components = response.data; + resolve(); + }); + }); + }, + fetchSelectedRailSystemData() { + if (!this.selectedRailSystem) return; + this.refreshSegments(this.selectedRailSystem); + this.refreshComponents(this.selectedRailSystem); + }, + addSegment(name) { + const rs = this.selectedRailSystem; + axios.post(`${this.apiUrl}/rs/${rs.id}/s`, {name: name}) + .then(() => this.refreshSegments(rs)) + .catch(error => console.log(error)); + }, + removeSegment(id) { + const rs = this.selectedRailSystem; + axios.delete(`${this.apiUrl}/rs/${rs.id}/s/${id}`) + .then(() => this.refreshSegments(rs)) + .catch(error => console.log(error)); + }, + addComponent(data) { + const rs = this.selectedRailSystem; + axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data) + .then(() => this.refreshComponents(rs)) + .catch(error => console.log(error)); + }, + removeComponent(id) { + const rs = this.selectedRailSystem; + axios.delete(`${this.apiUrl}/rs/${rs.id}/c/${id}`) + .then(() => this.refreshComponents(rs)) + .catch(error => console.log(error)); + }, + fetchComponentData(component) { + return new Promise(resolve => { + const rs = this.selectedRailSystem; + axios.get(`${this.apiUrl}/rs/${rs.id}/c/${component.id}`) + .then(response => resolve(response.data)) + .catch(error => console.log(error)); + }); } } }); \ No newline at end of file diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java b/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java index 1a83b3c..67b2763 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java @@ -2,8 +2,8 @@ package nl.andrewl.railsignalapi.rest; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; -import nl.andrewl.railsignalapi.rest.dto.component.ComponentResponse; -import nl.andrewl.railsignalapi.rest.dto.component.SimpleComponentResponse; +import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload; +import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse; import nl.andrewl.railsignalapi.service.ComponentService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -17,7 +17,7 @@ public class ComponentsApiController { private final ComponentService componentService; @GetMapping - public List getAllComponents(@PathVariable long rsId) { + public List getAllComponents(@PathVariable long rsId) { return componentService.getComponents(rsId); } @@ -36,4 +36,9 @@ public class ComponentsApiController { componentService.removeComponent(rsId, cId); return ResponseEntity.noContent().build(); } + + @PatchMapping(path = "/{cId}/connectedNodes") + public ComponentResponse updateConnectedNodes(@PathVariable long rsId, @PathVariable long cId, @RequestBody PathNodeUpdatePayload payload) { + return componentService.updatePath(rsId, cId, payload); + } } diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/SegmentsApiController.java b/src/main/java/nl/andrewl/railsignalapi/rest/SegmentsApiController.java index 80418a1..46f7c40 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/SegmentsApiController.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/SegmentsApiController.java @@ -2,8 +2,10 @@ package nl.andrewl.railsignalapi.rest; import lombok.RequiredArgsConstructor; import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse; +import nl.andrewl.railsignalapi.rest.dto.SegmentPayload; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; import nl.andrewl.railsignalapi.service.SegmentService; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -28,4 +30,10 @@ public class SegmentsApiController { public FullSegmentResponse createSegment(@PathVariable long rsId, @RequestBody SegmentPayload payload) { return segmentService.create(rsId, payload); } + + @DeleteMapping(path = "/{sId}") + public ResponseEntity removeSegment(@PathVariable long rsId, @PathVariable long sId) { + segmentService.remove(rsId, sId); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/FullSegmentResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/FullSegmentResponse.java index ae8cbde..d23c336 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/FullSegmentResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/FullSegmentResponse.java @@ -1,8 +1,8 @@ package nl.andrewl.railsignalapi.rest.dto; import nl.andrewl.railsignalapi.model.Segment; -import nl.andrewl.railsignalapi.rest.dto.component.SegmentBoundaryNodeResponse; -import nl.andrewl.railsignalapi.rest.dto.component.SignalResponse; +import nl.andrewl.railsignalapi.rest.dto.component.out.SegmentBoundaryNodeResponse; +import nl.andrewl.railsignalapi.rest.dto.component.out.SignalResponse; import java.util.List; diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/PathNodeUpdatePayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/PathNodeUpdatePayload.java new file mode 100644 index 0000000..9afbb29 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/PathNodeUpdatePayload.java @@ -0,0 +1,5 @@ +package nl.andrewl.railsignalapi.rest.dto; + +public record PathNodeUpdatePayload ( + long[] connectedNodeIds +) {} diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/SegmentPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/SegmentPayload.java similarity index 52% rename from src/main/java/nl/andrewl/railsignalapi/rest/SegmentPayload.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/SegmentPayload.java index 9bda7f6..ea99db5 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/SegmentPayload.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/SegmentPayload.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest; +package nl.andrewl.railsignalapi.rest.dto; public record SegmentPayload(String name) { } diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java new file mode 100644 index 0000000..a9066b7 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java @@ -0,0 +1,9 @@ +package nl.andrewl.railsignalapi.rest.dto.component.in; + +import nl.andrewl.railsignalapi.model.component.Position; + +public abstract class ComponentPayload { + public String name; + public String type; + public Position position; +} diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SignalPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SignalPayload.java new file mode 100644 index 0000000..c29cbf2 --- /dev/null +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SignalPayload.java @@ -0,0 +1,5 @@ +package nl.andrewl.railsignalapi.rest.dto.component.in; + +public class SignalPayload extends ComponentPayload { + public long segmentId; +} diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/ComponentResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java similarity index 92% rename from src/main/java/nl/andrewl/railsignalapi/rest/dto/component/ComponentResponse.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java index 7fd74f7..2d78269 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/ComponentResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest.dto.component; +package nl.andrewl.railsignalapi.rest.dto.component.out; import nl.andrewl.railsignalapi.model.component.*; diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/PathNodeResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/PathNodeResponse.java similarity index 86% rename from src/main/java/nl/andrewl/railsignalapi/rest/dto/component/PathNodeResponse.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/PathNodeResponse.java index 1c83196..3d2bfee 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/PathNodeResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/PathNodeResponse.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest.dto.component; +package nl.andrewl.railsignalapi.rest.dto.component.out; import nl.andrewl.railsignalapi.model.component.PathNode; diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SegmentBoundaryNodeResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SegmentBoundaryNodeResponse.java similarity index 88% rename from src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SegmentBoundaryNodeResponse.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SegmentBoundaryNodeResponse.java index ba09712..1af60e4 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SegmentBoundaryNodeResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SegmentBoundaryNodeResponse.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest.dto.component; +package nl.andrewl.railsignalapi.rest.dto.component.out; import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SignalResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SignalResponse.java similarity index 84% rename from src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SignalResponse.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SignalResponse.java index 56dd8d5..f5c6dab 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SignalResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SignalResponse.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest.dto.component; +package nl.andrewl.railsignalapi.rest.dto.component.out; import nl.andrewl.railsignalapi.model.component.Signal; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SimpleComponentResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SimpleComponentResponse.java similarity index 87% rename from src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SimpleComponentResponse.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SimpleComponentResponse.java index 6b6a9da..c26e66f 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SimpleComponentResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SimpleComponentResponse.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest.dto.component; +package nl.andrewl.railsignalapi.rest.dto.component.out; import nl.andrewl.railsignalapi.model.component.Component; import nl.andrewl.railsignalapi.model.component.Position; diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SwitchConfigurationResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchConfigurationResponse.java similarity index 86% rename from src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SwitchConfigurationResponse.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchConfigurationResponse.java index f65a5b6..eab8df2 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SwitchConfigurationResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchConfigurationResponse.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest.dto.component; +package nl.andrewl.railsignalapi.rest.dto.component.out; import nl.andrewl.railsignalapi.model.component.SwitchConfiguration; diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SwitchResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchResponse.java similarity index 90% rename from src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SwitchResponse.java rename to src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchResponse.java index b854bc0..efb04a1 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/SwitchResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchResponse.java @@ -1,4 +1,4 @@ -package nl.andrewl.railsignalapi.rest.dto.component; +package nl.andrewl.railsignalapi.rest.dto.component.out; import nl.andrewl.railsignalapi.model.component.Switch; diff --git a/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java b/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java index c39803c..6d4ba3a 100644 --- a/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java +++ b/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java @@ -11,8 +11,8 @@ import nl.andrewl.railsignalapi.dao.SwitchConfigurationRepository; import nl.andrewl.railsignalapi.model.RailSystem; import nl.andrewl.railsignalapi.model.Segment; import nl.andrewl.railsignalapi.model.component.*; -import nl.andrewl.railsignalapi.rest.dto.component.ComponentResponse; -import nl.andrewl.railsignalapi.rest.dto.component.SimpleComponentResponse; +import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload; +import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,10 +31,10 @@ public class ComponentService { private final SwitchConfigurationRepository switchConfigurationRepository; @Transactional(readOnly = true) - public List getComponents(long rsId) { + public List getComponents(long rsId) { var rs = railSystemRepository.findById(rsId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - return componentRepository.findAllByRailSystem(rs).stream().map(SimpleComponentResponse::new).toList(); + return componentRepository.findAllByRailSystem(rs).stream().map(ComponentResponse::of).toList(); } @Transactional(readOnly = true) @@ -123,7 +123,22 @@ public class ComponentService { } @Transactional - public void remove(long rsId, long componentId) { - + public ComponentResponse updatePath(long rsId, long cId, PathNodeUpdatePayload payload) { + var c = componentRepository.findByIdAndRailSystemId(cId, rsId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + if (!(c instanceof PathNode p)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component is not a PathNode."); + Set newNodes = new HashSet<>(); + for (var id : payload.connectedNodeIds()) { + var c1 = componentRepository.findByIdAndRailSystemId(id, rsId); + if (c1.isPresent() && c1.get() instanceof PathNode pn) { + newNodes.add(pn); + } else { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with id " + id + " is not a PathNode in the same rail system."); + } + } + p.getConnectedNodes().retainAll(newNodes); + p.getConnectedNodes().addAll(newNodes); + p = componentRepository.save(p); + return ComponentResponse.of(p); } } diff --git a/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java b/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java index 3b7dfe7..fbad3a2 100644 --- a/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java +++ b/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java @@ -6,7 +6,7 @@ import nl.andrewl.railsignalapi.dao.RailSystemRepository; import nl.andrewl.railsignalapi.dao.SegmentRepository; import nl.andrewl.railsignalapi.model.Segment; import nl.andrewl.railsignalapi.model.component.Component; -import nl.andrewl.railsignalapi.rest.SegmentPayload; +import nl.andrewl.railsignalapi.rest.dto.SegmentPayload; import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; import org.springframework.http.HttpStatus; @@ -53,5 +53,6 @@ public class SegmentService { .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); componentRepository.deleteAll(segment.getSignals()); componentRepository.deleteAll(segment.getBoundaryNodes()); + segmentRepository.delete(segment); } }