Cleaned up map rendering code into modules.
This commit is contained in:
parent
9c0d588543
commit
9b6eb7b667
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8" id="railSystemMapCanvasContainer">
|
<div class="col-md-8" id="railSystemMapCanvasContainer">
|
||||||
<canvas id="railSystemMapCanvas" height="600">
|
<canvas id="railSystemMapCanvas" height="700">
|
||||||
Your browser doesn't support canvas.
|
Your browser doesn't support canvas.
|
||||||
</canvas>
|
</canvas>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,17 +21,20 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {RailSystem} from "src/api/railSystems";
|
import {RailSystem} from "src/api/railSystems";
|
||||||
import {draw, initMap} from "src/render/mapRenderer";
|
import {draw, initMap} from "src/map/mapRenderer";
|
||||||
import SelectedComponentView from "components/rs/SelectedComponentView.vue";
|
import SelectedComponentView from "components/rs/SelectedComponentView.vue";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import AddComponentForm from "components/rs/add_component/AddComponentForm.vue";
|
import AddComponentForm from "components/rs/add_component/AddComponentForm.vue";
|
||||||
|
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||||
|
import { registerComponentSelectionListener } from "src/map/mapEventListener";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "MapView",
|
name: "MapView",
|
||||||
components: { AddComponentForm, SelectedComponentView },
|
components: { AddComponentForm, SelectedComponentView },
|
||||||
setup() {
|
setup() {
|
||||||
|
const rsStore = useRailSystemsStore();
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
return {quasar};
|
return {quasar, rsStore};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
railSystem: {
|
railSystem: {
|
||||||
|
@ -48,9 +51,11 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
initMap(this.railSystem);
|
initMap(this.railSystem);
|
||||||
|
registerComponentSelectionListener("addComponentFormHide", () => this.addComponent.visible = false);
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
initMap(this.railSystem);
|
initMap(this.railSystem);
|
||||||
|
registerComponentSelectionListener("addComponentFormHide", () => this.addComponent.visible = false);
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
railSystem: {
|
railSystem: {
|
||||||
|
@ -58,6 +63,11 @@ export default {
|
||||||
draw();
|
draw();
|
||||||
},
|
},
|
||||||
deep: true
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,10 +15,8 @@
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
:label="node.name"
|
:label="node.name"
|
||||||
clickable
|
|
||||||
/>
|
/>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption>Configuration #{{config.id}}</q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section v-if="!isConfigActive(config)" side top>
|
<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-btn dense size="sm" color="positive" @click="setActiveSwitchConfig(config.id)">Set Active</q-btn>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { MAP_CANVAS } from "src/map/mapRenderer";
|
||||||
|
|
||||||
export function roundedRect(ctx, x, y, w, h, r) {
|
export function roundedRect(ctx, x, y, w, h, r) {
|
||||||
if (w < 2 * r) r = w / 2;
|
if (w < 2 * r) r = w / 2;
|
||||||
if (h < 2 * r) r = h / 2;
|
if (h < 2 * r) r = h / 2;
|
||||||
|
@ -48,3 +50,15 @@ function distCompare(p0, a, b) {
|
||||||
const distB = (p0.x-b.x)*(p0.x-b.x) + (p0.y-b.y)*(p0.y-b.y);
|
const distB = (p0.x-b.x)*(p0.x-b.x) + (p0.y-b.y)*(p0.y-b.y);
|
||||||
return distA - distB;
|
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,133 @@
|
||||||
|
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 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) {
|
||||||
|
const finishingDrag = camPanNonzero();
|
||||||
|
camPanFinish();
|
||||||
|
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 (!finishingDrag) {
|
||||||
|
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);
|
||||||
|
}
|
|
@ -1,250 +0,0 @@
|
||||||
/*
|
|
||||||
Helper functions to actually perform rendering of different components.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getScaleFactor, getWorldTransform, isComponentHovered, isComponentSelected } from "./mapRenderer";
|
|
||||||
import { circle, roundedRect, sortPoints } from "./canvasUtils";
|
|
||||||
import randomColor from "randomcolor";
|
|
||||||
|
|
||||||
export function drawMap(ctx, rs) {
|
|
||||||
const worldTx = getWorldTransform();
|
|
||||||
ctx.setTransform(worldTx);
|
|
||||||
drawSegments(ctx, rs);
|
|
||||||
drawNodeConnections(ctx, rs, worldTx);
|
|
||||||
drawComponents(ctx, rs, worldTx);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSegments(ctx, rs) {
|
|
||||||
const segmentPoints = new Map();
|
|
||||||
// Gather for each segment a set of points representing its bounds.
|
|
||||||
rs.segments.forEach(segment => segmentPoints.set(segment.id, []));
|
|
||||||
for (let i = 0; i < rs.components.length; i++) {
|
|
||||||
const c = rs.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 < rs.segments.length; i++) {
|
|
||||||
const unsortedPoints = segmentPoints.get(rs.segments[i].id);
|
|
||||||
segmentPoints.set(rs.segments[i].id, sortPoints(unsortedPoints));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < rs.segments.length; i++) {
|
|
||||||
const color = randomColor({ luminosity: 'light', format: 'rgb', seed: rs.segments[i].id });
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.lineWidth = 5;
|
|
||||||
ctx.lineCap = "round";
|
|
||||||
ctx.lineJoin = "round";
|
|
||||||
ctx.font = "3px Sans-Serif";
|
|
||||||
|
|
||||||
const points = segmentPoints.get(rs.segments[i].id);
|
|
||||||
if (points.length === 0) continue;
|
|
||||||
const avgPoint = {x: points[0].x, y: points[0].y};
|
|
||||||
if (points.length === 1) {
|
|
||||||
circle(ctx, points[0].x, points[0].y, 5);
|
|
||||||
ctx.fill();
|
|
||||||
} else {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(points[0].x, points[0].y);
|
|
||||||
for (let j = 1; j < points.length; j++) {
|
|
||||||
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;
|
|
||||||
ctx.lineTo(points[0].x, points[0].y);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the segment name.
|
|
||||||
ctx.fillStyle = randomColor({luminosity: 'dark', format: 'rgb', seed: rs.segments[i].id});
|
|
||||||
ctx.fillText(rs.segments[i].name, avgPoint.x, avgPoint.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawNodeConnections(ctx, rs, worldTx) {
|
|
||||||
for (let i = 0; i < rs.components.length; i++) {
|
|
||||||
const c = rs.components[i];
|
|
||||||
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
|
|
||||||
drawConnectedNodes(ctx, worldTx, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawComponents(ctx, rs, worldTx) {
|
|
||||||
// Draw switch configurations first
|
|
||||||
for (let i = 0; i < rs.components.length; i++) {
|
|
||||||
if (rs.components[i].type === "SWITCH") {
|
|
||||||
drawSwitchConfigurations(ctx, worldTx, rs.components[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < rs.components.length; i++) {
|
|
||||||
drawComponent(ctx, worldTx, rs.components[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawComponent(ctx, worldTx, component) {
|
|
||||||
const tx = DOMMatrix.fromMatrix(worldTx);
|
|
||||||
tx.translateSelf(component.position.x, component.position.z, 0);
|
|
||||||
const s = getScaleFactor();
|
|
||||||
tx.scaleSelf(1/s, 1/s, 1/s);
|
|
||||||
tx.scaleSelf(20, 20, 20);
|
|
||||||
ctx.setTransform(tx);
|
|
||||||
|
|
||||||
if (component.type === "SIGNAL") {
|
|
||||||
drawSignal(ctx, component);
|
|
||||||
} else if (component.type === "SEGMENT_BOUNDARY") {
|
|
||||||
drawSegmentBoundary(ctx, component);
|
|
||||||
} else if (component.type === "SWITCH") {
|
|
||||||
drawSwitch(ctx, component);
|
|
||||||
} else if (component.type === "LABEL") {
|
|
||||||
drawLabel(ctx, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setTransform(tx.translate(0.75, -0.75));
|
|
||||||
if (component.online !== undefined && component.online !== null) {
|
|
||||||
drawOnlineIndicator(ctx, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setTransform(tx);
|
|
||||||
// Draw hovered status.
|
|
||||||
if (isComponentHovered(component) || isComponentSelected(component)) {
|
|
||||||
ctx.fillStyle = `rgba(255, 255, 0, 0.5)`;
|
|
||||||
circle(ctx, 0, 0, 0.75);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSignal(ctx, signal) {
|
|
||||||
roundedRect(ctx, -0.3, -0.5, 0.6, 1, 0.25);
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.fill();
|
|
||||||
if (signal.segment) {
|
|
||||||
if (signal.segment.occupied === true) {
|
|
||||||
ctx.fillStyle = `rgb(255, 0, 0)`;
|
|
||||||
} else if (signal.segment.occupied === false) {
|
|
||||||
ctx.fillStyle = `rgb(0, 255, 0)`;
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = `rgb(255, 255, 0)`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = `rgb(0, 0, 255)`;
|
|
||||||
}
|
|
||||||
circle(ctx, 0, -0.2, 0.15);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSegmentBoundary(ctx) {
|
|
||||||
ctx.fillStyle = `rgb(150, 58, 224)`;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, -0.5);
|
|
||||||
ctx.lineTo(-0.5, 0);
|
|
||||||
ctx.lineTo(0, 0.5);
|
|
||||||
ctx.lineTo(0.5, 0);
|
|
||||||
ctx.lineTo(0, -0.5);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSwitchConfigurations(ctx, worldTx, sw) {
|
|
||||||
const tx = DOMMatrix.fromMatrix(worldTx);
|
|
||||||
tx.translateSelf(sw.position.x, sw.position.z, 0);
|
|
||||||
const s = getScaleFactor();
|
|
||||||
tx.scaleSelf(1/s, 1/s, 1/s);
|
|
||||||
tx.scaleSelf(20, 20, 20);
|
|
||||||
ctx.setTransform(tx);
|
|
||||||
|
|
||||||
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
|
|
||||||
const config = sw.possibleConfigurations[i];
|
|
||||||
ctx.strokeStyle = randomColor({
|
|
||||||
seed: config.id,
|
|
||||||
format: 'rgb',
|
|
||||||
luminosity: 'bright'
|
|
||||||
});
|
|
||||||
if (sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id) {
|
|
||||||
ctx.lineWidth = 0.6;
|
|
||||||
} else {
|
|
||||||
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;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, 0);
|
|
||||||
ctx.lineTo(diff.x, diff.y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSwitch(ctx, sw) {
|
|
||||||
ctx.fillStyle = `rgb(245, 188, 66)`;
|
|
||||||
ctx.strokeStyle = `rgb(245, 188, 66)`;
|
|
||||||
ctx.lineWidth = 0.2;
|
|
||||||
circle(ctx, 0, 0.3, 0.2);
|
|
||||||
ctx.fill();
|
|
||||||
circle(ctx, -0.3, -0.3, 0.2);
|
|
||||||
ctx.fill();
|
|
||||||
circle(ctx, 0.3, -0.3, 0.2);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, 0.3);
|
|
||||||
ctx.lineTo(0, 0);
|
|
||||||
ctx.lineTo(0.3, -0.3);
|
|
||||||
ctx.moveTo(0, 0);
|
|
||||||
ctx.lineTo(-0.3, -0.3);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawLabel(ctx, lbl) {
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
circle(ctx, 0, 0, 0.1);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.strokeStyle = "black";
|
|
||||||
ctx.font = "0.5px Sans-Serif";
|
|
||||||
ctx.fillText(lbl.text, 0.1, -0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawOnlineIndicator(ctx, component) {
|
|
||||||
ctx.lineWidth = 0.1;
|
|
||||||
if (component.online) {
|
|
||||||
ctx.fillStyle = `rgba(52, 174, 235, 128)`;
|
|
||||||
ctx.strokeStyle = `rgba(52, 174, 235, 128)`;
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = `rgba(153, 153, 153, 128)`;
|
|
||||||
ctx.strokeStyle = `rgba(153, 153, 153, 128)`;
|
|
||||||
}
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(0, 0.2, 0.125, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
for (let r = 0; r < 3; r++) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(0, 0, 0.1 + 0.2 * r, 7 * Math.PI / 6, 11 * Math.PI / 6);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function drawConnectedNodes(ctx, worldTx, component) {
|
|
||||||
const s = getScaleFactor();
|
|
||||||
ctx.lineWidth = 5 / s;
|
|
||||||
ctx.strokeStyle = "black";
|
|
||||||
for (let i = 0; i < component.connectedNodes.length; i++) {
|
|
||||||
const node = component.connectedNodes[i];
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(component.position.x, component.position.z);
|
|
||||||
ctx.lineTo(node.position.x, node.position.z);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
/*
|
|
||||||
This component is responsible for the rendering of a RailSystem in a 2d map
|
|
||||||
view.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { drawMap } from "./drawing";
|
|
||||||
|
|
||||||
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
|
|
||||||
const SCALE_INDEX_NORMAL = 7;
|
|
||||||
const HOVER_RADIUS = 10;
|
|
||||||
|
|
||||||
let mapContainerDiv = null;
|
|
||||||
let mapCanvas = null;
|
|
||||||
let railSystem = null;
|
|
||||||
|
|
||||||
let mapScaleIndex = SCALE_INDEX_NORMAL;
|
|
||||||
let mapTranslation = {x: 0, y: 0};
|
|
||||||
let mapDragOrigin = null;
|
|
||||||
let mapDragTranslation = null;
|
|
||||||
let lastMousePoint = new DOMPoint(0, 0, 0, 0);
|
|
||||||
const hoveredElements = [];
|
|
||||||
|
|
||||||
export function initMap(rs) {
|
|
||||||
railSystem = rs;
|
|
||||||
console.log("Initializing map for rail system: " + rs.name);
|
|
||||||
resetView();
|
|
||||||
mapCanvas = document.getElementById("railSystemMapCanvas");
|
|
||||||
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
|
|
||||||
mapCanvas.removeEventListener("wheel", onMouseWheel);
|
|
||||||
mapCanvas.addEventListener("wheel", onMouseWheel);
|
|
||||||
mapCanvas.removeEventListener("mousedown", onMouseDown);
|
|
||||||
mapCanvas.addEventListener("mousedown", onMouseDown);
|
|
||||||
mapCanvas.removeEventListener("mouseup", onMouseUp);
|
|
||||||
mapCanvas.addEventListener("mouseup", onMouseUp);
|
|
||||||
mapCanvas.removeEventListener("mousemove", onMouseMove);
|
|
||||||
mapCanvas.addEventListener("mousemove", onMouseMove);
|
|
||||||
|
|
||||||
// Do an initial draw.
|
|
||||||
draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
if (!(mapCanvas && railSystem && railSystem.components)) {
|
|
||||||
console.warn("Attempted to draw map without canvas or railSystem.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ctx = mapCanvas.getContext("2d");
|
|
||||||
if (mapCanvas.width !== mapContainerDiv.clientWidth) {
|
|
||||||
mapCanvas.width = mapContainerDiv.clientWidth;
|
|
||||||
}
|
|
||||||
if (mapCanvas.height !== mapContainerDiv.clientHeight) {
|
|
||||||
mapCanvas.height = mapContainerDiv.clientHeight - 6;
|
|
||||||
}
|
|
||||||
const width = mapCanvas.width;
|
|
||||||
const height = mapCanvas.height;
|
|
||||||
ctx.resetTransform();
|
|
||||||
ctx.fillStyle = `rgb(240, 240, 240)`;
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
drawMap(ctx, railSystem);
|
|
||||||
drawDebugInfo(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawDebugInfo(ctx) {
|
|
||||||
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 components: ${hoveredElements.length}`
|
|
||||||
]
|
|
||||||
for (let i = 0; i < hoveredElements.length; i++) {
|
|
||||||
lines.push(" " + hoveredElements[i].name);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
ctx.fillText(lines[i], 10, 20 + (i * 15));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getScaleFactor() {
|
|
||||||
return SCALE_VALUES[mapScaleIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a matrix that transforms world coordinates to canvas.
|
|
||||||
* @returns {DOMMatrix}
|
|
||||||
*/
|
|
||||||
export function getWorldTransform() {
|
|
||||||
const canvasRect = mapCanvas.getBoundingClientRect();
|
|
||||||
const scale = getScaleFactor();
|
|
||||||
const tx = new DOMMatrix();
|
|
||||||
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
|
|
||||||
tx.scaleSelf(scale, scale, scale);
|
|
||||||
tx.translateSelf(mapTranslation.x, mapTranslation.y, 0);
|
|
||||||
if (mapDragOrigin !== null && mapDragTranslation !== null) {
|
|
||||||
tx.translateSelf(mapDragTranslation.x, mapDragTranslation.y, 0);
|
|
||||||
}
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isComponentHovered(component) {
|
|
||||||
return hoveredElements.some(c => c.id === component.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isComponentSelected(component) {
|
|
||||||
return railSystem.selectedComponents.some(c => c.id === component.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a point on the map coordinates to world coordinates.
|
|
||||||
* @param {DOMPoint} p
|
|
||||||
* @returns {DOMPoint}
|
|
||||||
*/
|
|
||||||
export function mapPointToWorld(p) {
|
|
||||||
return getWorldTransform().invertSelf().transformPoint(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a point in the world to map coordinates.
|
|
||||||
* @param {DOMPoint} p
|
|
||||||
* @returns {DOMPoint}
|
|
||||||
*/
|
|
||||||
export function worldPointToMap(p) {
|
|
||||||
return getWorldTransform().transformPoint(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
EVENT HANDLING
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {WheelEvent} event
|
|
||||||
*/
|
|
||||||
function onMouseWheel(event) {
|
|
||||||
const s = event.deltaY;
|
|
||||||
if (s > 0) {
|
|
||||||
mapScaleIndex = Math.max(0, mapScaleIndex - 1);
|
|
||||||
} else if (s < 0) {
|
|
||||||
mapScaleIndex = Math.min(SCALE_VALUES.length - 1, mapScaleIndex + 1);
|
|
||||||
}
|
|
||||||
draw();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MouseEvent} event
|
|
||||||
*/
|
|
||||||
function onMouseDown(event) {
|
|
||||||
const p = getMousePoint(event);
|
|
||||||
mapDragOrigin = {x: p.x, y: p.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseUp() {
|
|
||||||
let finishedDrag = false;
|
|
||||||
if (mapDragTranslation !== null) {
|
|
||||||
mapTranslation.x += mapDragTranslation.x;
|
|
||||||
mapTranslation.y += mapDragTranslation.y;
|
|
||||||
finishedDrag = true;
|
|
||||||
}
|
|
||||||
if (hoveredElements.length > 0) {
|
|
||||||
railSystem.selectedComponents.length = 0;
|
|
||||||
railSystem.selectedComponents.push(...hoveredElements);
|
|
||||||
} else if (!finishedDrag) {
|
|
||||||
railSystem.selectedComponents.length = 0;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
Loading…
Reference in New Issue