You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

504 lines
15 KiB
TypeScript

import { FcModelValidationService } from './modelvalidation.service';
import {
FcConnector,
FcConnectorRectInfo,
FcCoords,
FcEdge,
FcItemInfo,
FcModel,
FcNode,
FcRectBox,
FlowchartConstants
} from './ngx-flowchart.models';
import { Observable, of, Subject } from 'rxjs';
import { EventEmitter } from '@angular/core';
import { debounceTime } from 'rxjs/operators';
export class FcModelService {
modelValidation: FcModelValidationService;
model: FcModel;
private readonly detectChangesSubject: Subject<any>;
selectedObjects: any[];
connectorsRectInfos: ConnectorRectInfoMap = {};
nodesHtmlElements: HtmlElementMap = {};
canvasHtmlElement: HTMLElement = null;
dragImage: HTMLImageElement = null;
svgHtmlElement: SVGElement = null;
dropNode: (event: Event, node: FcNode) => void;
createEdge: (event: Event, edge: FcEdge) => Observable<FcEdge>;
edgeAddedCallback: (edge: FcEdge) => void;
nodeRemovedCallback: (node: FcNode) => void;
edgeRemovedCallback: (edge: FcEdge) => void;
dropTargetId: string;
private readonly modelChanged: EventEmitter<any>;
private readonly debouncer = new Subject<any>();
connectors: ConnectorsModel;
nodes: NodesModel;
edges: EdgesModel;
constructor(modelValidation: FcModelValidationService,
model: FcModel,
modelChanged: EventEmitter<any>,
detectChangesSubject: Subject<any>,
selectedObjects: any[],
dropNode: (event: Event, node: FcNode) => void,
createEdge: (event: Event, edge: FcEdge) => Observable<FcEdge>,
edgeAddedCallback: (edge: FcEdge) => void,
nodeRemovedCallback: (node: FcNode) => void,
edgeRemovedCallback: (edge: FcEdge) => void,
canvasHtmlElement: HTMLElement,
svgHtmlElement: SVGElement) {
this.modelValidation = modelValidation;
this.model = model;
this.modelChanged = modelChanged;
this.detectChangesSubject = detectChangesSubject;
this.canvasHtmlElement = canvasHtmlElement;
this.svgHtmlElement = svgHtmlElement;
this.modelValidation.validateModel(this.model);
this.selectedObjects = selectedObjects;
this.dropNode = dropNode || (() => {});
this.createEdge = createEdge || ((event, edge) => of({...edge, label: 'label'}));
this.edgeAddedCallback = edgeAddedCallback || (() => {});
this.nodeRemovedCallback = nodeRemovedCallback || (() => {});
this.edgeRemovedCallback = edgeRemovedCallback || (() => {});
this.connectors = new ConnectorsModel(this);
this.nodes = new NodesModel(this);
this.edges = new EdgesModel(this);
this.debouncer
.pipe(debounceTime(100))
.subscribe(() => this.modelChanged.emit());
}
public notifyModelChanged() {
this.debouncer.next();
}
public detectChanges() {
setTimeout(() => {
this.detectChangesSubject.next();
}, 0);
}
public selectObject(object: any) {
if (this.isEditable()) {
if (this.selectedObjects.indexOf(object) === -1) {
this.selectedObjects.push(object);
}
}
}
public deselectObject(object: any) {
if (this.isEditable()) {
const index = this.selectedObjects.indexOf(object);
if (index === -1) {
throw new Error('Tried to deselect an unselected object');
}
this.selectedObjects.splice(index, 1);
}
}
public toggleSelectedObject(object: any) {
if (this.isSelectedObject(object)) {
this.deselectObject(object);
} else {
this.selectObject(object);
}
}
public isSelectedObject(object: any): boolean {
return this.selectedObjects.indexOf(object) !== -1;
}
public selectAll() {
this.model.nodes.forEach(node => {
if (!node.readonly) {
this.nodes.select(node);
}
});
this.model.edges.forEach(edge => {
this.edges.select(edge);
});
this.detectChanges();
}
public deselectAll() {
this.selectedObjects.splice(0, this.selectedObjects.length);
this.detectChanges();
}
public isEditObject(object: any): boolean {
return this.selectedObjects.length === 1 &&
this.selectedObjects.indexOf(object) !== -1;
}
private inRectBox(x: number, y: number, rectBox: FcRectBox): boolean {
return x >= rectBox.left && x <= rectBox.right &&
y >= rectBox.top && y <= rectBox.bottom;
}
public getItemInfoAtPoint(x: number, y: number): FcItemInfo {
return {
node: this.getNodeAtPoint(x, y),
edge: this.getEdgeAtPoint(x, y)
};
}
public getNodeAtPoint(x: number, y: number): FcNode {
for (const node of this.model.nodes) {
const element = this.nodes.getHtmlElement(node.id);
const nodeElementBox = element.getBoundingClientRect();
if (x >= nodeElementBox.left && x <= nodeElementBox.right
&& y >= nodeElementBox.top && y <= nodeElementBox.bottom) {
return node;
}
}
return null;
}
public getEdgeAtPoint(x: number, y: number): FcEdge {
const element = document.elementFromPoint(x, y);
const id = element.id;
let edgeIndex = -1;
if (id) {
if (id.startsWith('fc-edge-path-')) {
edgeIndex = Number(id.substring('fc-edge-path-'.length));
} else if (id.startsWith('fc-edge-label-')) {
edgeIndex = Number(id.substring('fc-edge-label-'.length));
}
}
if (edgeIndex > -1) {
return this.model.edges[edgeIndex];
}
return null;
}
public selectAllInRect(rectBox: FcRectBox) {
this.model.nodes.forEach((value) => {
const element = this.nodes.getHtmlElement(value.id);
const nodeElementBox = element.getBoundingClientRect();
if (!value.readonly) {
const x = nodeElementBox.left + nodeElementBox.width / 2;
const y = nodeElementBox.top + nodeElementBox.height / 2;
if (this.inRectBox(x, y, rectBox)) {
this.nodes.select(value);
} else {
if (this.nodes.isSelected(value)) {
this.nodes.deselect(value);
}
}
}
});
const canvasElementBox = this.canvasHtmlElement.getBoundingClientRect();
this.model.edges.forEach((value) => {
const start = this.edges.sourceCoord(value);
const end = this.edges.destCoord(value);
const x = (start.x + end.x) / 2 + canvasElementBox.left;
const y = (start.y + end.y) / 2 + canvasElementBox.top;
if (this.inRectBox(x, y, rectBox)) {
this.edges.select(value);
} else {
if (this.edges.isSelected(value)) {
this.edges.deselect(value);
}
}
});
}
public deleteSelected() {
const edgesToDelete = this.edges.getSelectedEdges();
edgesToDelete.forEach((edge) => {
this.edges.delete(edge);
});
const nodesToDelete = this.nodes.getSelectedNodes();
nodesToDelete.forEach((node) => {
this.nodes.delete(node);
});
}
public isEditable(): boolean {
return this.dropTargetId === undefined;
}
public isDropSource(): boolean {
return this.dropTargetId !== undefined;
}
public getDragImage(): HTMLImageElement {
if (!this.dragImage) {
this.dragImage = new Image();
this.dragImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
this.dragImage.style.visibility = 'hidden';
}
return this.dragImage;
}
}
interface HtmlElementMap { [id: string]: HTMLElement; }
interface ConnectorRectInfoMap { [id: string]: FcConnectorRectInfo; }
abstract class AbstractFcModel<T> {
modelService: FcModelService;
protected constructor(modelService: FcModelService) {
this.modelService = modelService;
}
public select(object: T) {
this.modelService.selectObject(object);
}
public deselect(object: T) {
this.modelService.deselectObject(object);
}
public toggleSelected(object: T) {
this.modelService.toggleSelectedObject(object);
}
public isSelected(object: T): boolean {
return this.modelService.isSelectedObject(object);
}
public isEdit(object: T): boolean {
return this.modelService.isEditObject(object);
}
}
class ConnectorsModel extends AbstractFcModel<FcConnector> {
constructor(modelService: FcModelService) {
super(modelService);
}
public getConnector(connectorId: string): FcConnector {
const model = this.modelService.model;
for (const node of model.nodes) {
for (const connector of node.connectors) {
if (connector.id === connectorId) {
return connector;
}
}
}
}
public getConnectorRectInfo(connectorId: string): FcConnectorRectInfo {
return this.modelService.connectorsRectInfos[connectorId];
}
public setConnectorRectInfo(connectorId: string, connectorRectInfo: FcConnectorRectInfo) {
this.modelService.connectorsRectInfos[connectorId] = connectorRectInfo;
this.modelService.detectChanges();
}
private _getCoords(connectorId: string, centered?: boolean): FcCoords {
const connectorRectInfo = this.getConnectorRectInfo(connectorId);
const canvas = this.modelService.canvasHtmlElement;
if (connectorRectInfo === null || connectorRectInfo === undefined || canvas === null) {
return {x: 0, y: 0};
}
let x = connectorRectInfo.type === FlowchartConstants.leftConnectorType ?
connectorRectInfo.nodeRectInfo.left() : connectorRectInfo.nodeRectInfo.right();
let y = connectorRectInfo.nodeRectInfo.top() + connectorRectInfo.nodeRectInfo.height() / 2;
if (!centered) {
x -= connectorRectInfo.width / 2;
y -= connectorRectInfo.height / 2;
}
const coords: FcCoords = {
x: Math.round(x),
y: Math.round(y)
};
return coords;
}
public getCoords(connectorId: string): FcCoords {
return this._getCoords(connectorId, false);
}
public getCenteredCoord(connectorId: string): FcCoords {
return this._getCoords(connectorId, true);
}
}
class NodesModel extends AbstractFcModel<FcNode> {
constructor(modelService: FcModelService) {
super(modelService);
}
public getConnectorsByType(node: FcNode, type: string): Array<FcConnector> {
return node.connectors.filter((connector) => {
return connector.type === type;
});
}
private _addConnector(node: FcNode, connector: FcConnector) {
node.connectors.push(connector);
try {
this.modelService.modelValidation.validateNode(node);
} catch (error) {
node.connectors.splice(node.connectors.indexOf(connector), 1);
throw error;
}
}
public delete(node: FcNode) {
if (this.isSelected(node)) {
this.deselect(node);
}
const model = this.modelService.model;
const index = model.nodes.indexOf(node);
if (index === -1) {
if (node === undefined) {
throw new Error('Passed undefined');
}
throw new Error('Tried to delete not existing node');
}
const connectorIds = this.getConnectorIds(node);
for (let i = 0; i < model.edges.length; i++) {
const edge = model.edges[i];
if (connectorIds.indexOf(edge.source) !== -1 || connectorIds.indexOf(edge.destination) !== -1) {
this.modelService.edges.delete(edge);
i--;
}
}
model.nodes.splice(index, 1);
this.modelService.notifyModelChanged();
this.modelService.nodeRemovedCallback(node);
}
public getSelectedNodes(): Array<FcNode> {
const model = this.modelService.model;
return model.nodes.filter((node) => {
return this.modelService.nodes.isSelected(node);
});
}
public handleClicked(node: FcNode, ctrlKey?: boolean) {
if (ctrlKey) {
this.modelService.nodes.toggleSelected(node);
} else {
this.modelService.deselectAll();
this.modelService.nodes.select(node);
}
}
private _addNode(node: FcNode) {
const model = this.modelService.model;
try {
model.nodes.push(node);
this.modelService.modelValidation.validateNodes(model.nodes);
} catch (error) {
model.nodes.splice(model.nodes.indexOf(node), 1);
throw error;
}
}
public getConnectorIds(node: FcNode): Array<string> {
return node.connectors.map((connector) => {
return connector.id;
});
}
public getNodeByConnectorId(connectorId: string): FcNode {
const model = this.modelService.model;
for (const node of model.nodes) {
const connectorIds = this.getConnectorIds(node);
if (connectorIds.indexOf(connectorId) > -1) {
return node;
}
}
return null;
}
public getHtmlElement(nodeId: string): HTMLElement {
return this.modelService.nodesHtmlElements[nodeId];
}
public setHtmlElement(nodeId: string, element: HTMLElement) {
this.modelService.nodesHtmlElements[nodeId] = element;
this.modelService.detectChanges();
}
}
class EdgesModel extends AbstractFcModel<FcEdge> {
constructor(modelService: FcModelService) {
super(modelService);
}
public sourceCoord(edge: FcEdge): FcCoords {
return this.modelService.connectors.getCenteredCoord(edge.source);
}
public destCoord(edge: FcEdge): FcCoords {
return this.modelService.connectors.getCenteredCoord(edge.destination);
}
public delete(edge: FcEdge) {
const model = this.modelService.model;
const index = model.edges.indexOf(edge);
if (index === -1) {
throw new Error('Tried to delete not existing edge');
}
if (this.isSelected(edge)) {
this.deselect(edge);
}
model.edges.splice(index, 1);
this.modelService.notifyModelChanged();
this.modelService.edgeRemovedCallback(edge);
}
public getSelectedEdges(): Array<FcEdge> {
const model = this.modelService.model;
return model.edges.filter((edge) => {
return this.modelService.edges.isSelected(edge);
});
}
public handleEdgeMouseClick(edge: FcEdge, ctrlKey?: boolean) {
if (ctrlKey) {
this.modelService.edges.toggleSelected(edge);
} else {
this.modelService.deselectAll();
this.modelService.edges.select(edge);
}
}
public putEdge(edge: FcEdge) {
const model = this.modelService.model;
model.edges.push(edge);
this.modelService.notifyModelChanged();
}
public _addEdge(event: Event, sourceConnector: FcConnector, destConnector: FcConnector, label: string) {
this.modelService.modelValidation.validateConnector(sourceConnector);
this.modelService.modelValidation.validateConnector(destConnector);
const edge: FcEdge = {};
edge.source = sourceConnector.id;
edge.destination = destConnector.id;
edge.label = label;
const model = this.modelService.model;
this.modelService.modelValidation.validateEdges(model.edges.concat([edge]), model.nodes);
this.modelService.createEdge(event, edge).subscribe(
(created) => {
model.edges.push(created);
this.modelService.notifyModelChanged();
this.modelService.edgeAddedCallback(created);
}
);
}
}