/** @module editorElements.Wire */
import { PolyLine, PolyLinePoints, PolyLinePoint, Group } from '../svgObjects';
import Logic from '../Logic';
import stateClasses from './stateClasses';
import findPath from '../findPath';
import NetworkElement from './NetworkElement';
/**
* Wire represents connection of two {@link Connector}s.
* @extends NetworkElement
*/
export default class Wire extends NetworkElement {
/**
* @param {App} appInstance instance of [App](./module-App.html)
* @param {string} fromId id of the first connector this wire will be connected to
* @param {string} toId id of the second connector this wire will be connected to
* @param {Boolean} [refresh=true] if `true`, the [App](./module-App.html) will refresh after creating this wire
*/
constructor(appInstance, fromId, toId, refresh = true, route = true) {
super(appInstance);
this.gridSize = appInstance.gridSize;
this.connection = {
from: {
id: fromId,
box: this.appInstance.getBoxByConnectorId(fromId),
connector: this.appInstance.getConnectorById(fromId)
},
to: {
id: toId,
box: this.appInstance.getBoxByConnectorId(toId),
connector: this.appInstance.getConnectorById(toId)
}
};
if (this.connection.from.connector.isOutputConnector) {
if (this.connection.to.connector.isInputConnector) {
// desired state
} else {
// connecting two output connectors
throw 'Can not place wire between two output connectors';
}
} else {
if (this.connection.to.connector.isInputConnector) {
// connecting two input connectors
throw 'Can not place wire between two input connectors';
} else {
// swap them and we are ready to go
[this.connection.from, this.connection.to] = [
this.connection.to,
this.connection.from
];
}
}
if (route) {
this.routeWire(true, refresh);
} else {
this.temporaryWire();
}
this.elementState = Logic.state.unknown;
this.setState(this.connection.from.connector.state);
if (refresh) {
const { connector } = this.connection.to;
this.appInstance.startNewSimulation(connector, connector.state);
}
this.svgObj.$el.addClass('wire');
}
get boxes() {
return [this.connection.from.box, this.connection.to.box];
}
get connectors() {
return [this.connection.from.connector, this.connection.to.connector];
}
/**
* get data of this wire as a JSON-ready object
* @return {Object} javascript object containing essential data for this wire
*/
get exportData() {
return {
fromId: this.connection.from.id,
toId: this.connection.to.id
};
}
/**
* set the state of this wire to match the state of the input connector it is connected to
* @param {Logic.state} state [description]
*/
setState(state) {
this.svgObj.removeClasses(...stateClasses);
this.svgObj.addClass(stateClasses[state]);
this.connection.to.connector.setState(state);
this.elementState = state;
}
/**
* get the current [Logic.state](./modules-Logic.html#.state) of this wire
* @return {Logic.state}
*/
get state() {
return this.elementState;
}
/**
* update the state of this wire
*/
updateWireState() {
for (const box of this.boxes) {
box.refreshState();
}
}
/**
* get the jQuery element for this wire
* @return {jQuery.element}
*/
get() {
return this.svgObj.get();
}
/**
* get the PolyLine points for a temporary wire placement connecting the two connectors
* @return {PolyLinePoints} new instance of {@link PolyLinePoints}
*/
getTemporaryWirePoints() {
let points = new PolyLinePoints();
points.append(new PolyLinePoint(this.wireStart.x, this.wireStart.y));
points.append(new PolyLinePoint(this.wireEnd.x, this.wireEnd.y));
return points;
}
/**
* route the wire using the temporary wire points
*/
temporaryWire() {
this.wireStart = this.appInstance.getConnectorPosition(
this.connection.from.connector,
false
);
this.wireEnd = this.appInstance.getConnectorPosition(this.connection.to.connector, false);
this.setWirePath(this.getTemporaryWirePoints());
}
/**
* route the wire using the modified A* wire routing algorithm
*/
routeWire(snapToGrid = true, refresh = true) {
this.wireStart = this.appInstance.getConnectorPosition(
this.connection.from.connector,
snapToGrid
);
this.wireEnd = this.appInstance.getConnectorPosition(
this.connection.to.connector,
snapToGrid
);
this.points = this.findRoute(
{
x: this.wireStart.x / this.gridSize,
y: this.wireStart.y / this.gridSize
},
{
x: this.wireEnd.x / this.gridSize,
y: this.wireEnd.y / this.gridSize
}
);
this.setWirePath(this.points);
if (refresh) this.updateWireState();
// regenerate inconvenient nodes
this.generateInconvenientNodes();
}
/**
* set the wire to follow the specified points
* @param {PolyLinePoints} points instance of {@link PolyLinePoints}
*/
setWirePath(points) {
// set the line
if (this.svgObj !== undefined) {
// this.svgObj.updatePoints(points);
for (let child of this.svgObj.children) {
child.updatePoints(points);
}
} else {
this.svgObj = new Group();
let hitbox = new PolyLine(points, 10, 'white');
hitbox.addClass('hitbox');
hitbox.addAttr({ opacity: 0 });
this.svgObj.addChild(hitbox);
let mainLine = new PolyLine(points, 2);
mainLine.addClass('main', 'stateUnknown');
this.svgObj.addChild(mainLine);
}
}
pathToPolyLine(path) {
let totalPath = new PolyLinePoints();
for (const point of path) {
totalPath.append(new PolyLinePoint(point.x * this.gridSize, point.y * this.gridSize));
}
return totalPath;
}
/**
* find a nice route for the wire
* @param {Object} start object containing numeric attributes `x` and `y` that represent the first endpoint of the wire in grid pixel
* @param {Object} end object containing numeric attributes `x` and `y` that represent the second endpoint of the wire in grid pixels
* @return {PolyLinePoints} [description]
*/
findRoute(start, end) {
let nonRoutable = this.appInstance.getNonRoutableNodes();
let punishedButRoutable;
if (this.svgObj === undefined) {
punishedButRoutable = this.appInstance.getInconvenientNodes();
} else {
punishedButRoutable = this.appInstance.getInconvenientNodes(this.svgObj.id);
}
let path = findPath(start, end, nonRoutable, punishedButRoutable, this.gridSize);
if (path) {
return this.pathToPolyLine(path);
}
// if a path was not found, try again but don't take into account the punished and non routable node
path = findPath(start, end, new Set(), new Set(), this.gridSize);
if (path) {
return this.pathToPolyLine(path);
}
// if the path was still not found, give up and return temporary points
return this.getTemporaryWirePoints();
}
/**
* generate a set of nodes, that are inconvenient for wiring, but can be used, just are not preferred
* @return {Set} set of nodes (objects containing x and y coordinates) that are not preferred for wiring
*/
generateInconvenientNodes() {
this.inconvenientNodes = new Set();
let prevPoint;
this.points.forEach(point => {
const x = this.appInstance.SVGToGrid(point.x),
y = this.appInstance.SVGToGrid(point.y);
if (prevPoint === undefined) {
// if the prevPoint is undefined, add the first point
this.inconvenientNodes.add({ x, y });
} else {
// else add all the point between the prevPoint (excluded) and point (included)
if (prevPoint.x === x) {
// if the line is horizontal
let from = Math.min(prevPoint.y, y);
let to = Math.max(prevPoint.y, y);
while (from <= to) {
this.inconvenientNodes.add({ x: x, y: from });
from++;
}
} else if (prevPoint.y === y) {
// if the line is vertical
let from = Math.min(prevPoint.x, x);
let to = Math.max(prevPoint.x, x);
while (from <= to) {
this.inconvenientNodes.add({ x: from, y: y });
from++;
}
} else {
// line is neither horizontal nor vertical, throw an error for better future debugging
// console.error("getInconvenientNodes: line between two points is neither horizontal nor vertical");
}
}
// set new prevPoint
prevPoint = { x, y };
});
}
}