UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

927 lines (834 loc) 46.8 kB
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter"; import { Point } from "@devexpress/utils/lib/geometry/point"; import { Size } from "@devexpress/utils/lib/geometry/size"; import { AddConnectionHistoryItem } from "../History/Common/AddConnectionHistoryItem"; import { AddConnectorHistoryItem } from "../History/Common/AddConnectorHistoryItem"; import { AddShapeHistoryItem } from "../History/Common/AddShapeHistoryItem"; import { DeleteConnectionHistoryItem } from "../History/Common/DeleteConnectionHistoryItem"; import { ResizeShapeHistoryItem } from "../History/Common/ResizeShapeHistoryItem"; import { History } from "../History/History"; import { ChangeConnectorPropertyHistoryItem } from "../History/Properties/ChangeConnectorPropertyHistoryItem"; import { ChangeConnectorTextHistoryItem } from "../History/Properties/ChangeConnectorTextHistoryItem"; import { ChangeCustomDataHistoryItem } from "../History/Properties/ChangeCustomDataHistoryItem"; import { ChangeLockedHistoryItem } from "../History/Properties/ChangeLockedHistoryItem"; import { ChangeShapeImageHistoryItem } from "../History/Properties/ChangeShapeImageHistoryItem"; import { ChangeShapeTextHistoryItem } from "../History/Properties/ChangeShapeTextHistoryItem"; import { ChangeZindexHistoryItem } from "../History/Properties/ChangeZindexHistoryItem"; import { ChangeStyleHistoryItem } from "../History/StyleProperties/ChangeStyleHistoryItem"; import { ChangeStyleTextHistoryItem } from "../History/StyleProperties/ChangeStyleTextHistoryItem"; import { KeySet } from "../ListUtils"; import { Connector, CONNECTOR_DEFAULT_TEXT_POSITION, ConnectorPosition } from "../Model/Connectors/Connector"; import { DiagramItem, ItemDataKey, ItemKey } from "../Model/DiagramItem"; import { DiagramModel } from "../Model/Model"; import { ModelUtils } from "../Model/ModelUtils"; import { IShapeDescriptionManager } from "../Model/Shapes/Descriptions/ShapeDescriptionManager"; import { Shape } from "../Model/Shapes/Shape"; import { ShapeTypes } from "../Model/Shapes/ShapeTypes"; import { ITextMeasurer, TextOwner } from "../Render/Measurer/ITextMeasurer"; import { Selection } from "../Selection/Selection"; import { IShapeSizeSettings } from "../Settings"; import { ObjectUtils } from "../Utils"; import { ColorUtils } from "@devexpress/utils/lib/utils/color"; import { Data } from "../Utils/Data"; import { isColorProperty } from "../Utils/Svg"; import { getOptimalTextRectangle } from "../Utils/TextUtils"; import { DataSourceEdgeDataImporter, DataSourceItemDataImporter, DataSourceNodeDataImporter } from "./DataImporter"; import { DataLayoutParameters } from "./DataLayoutParameters"; import { DataSourceEdgeItem, DataSourceItem, DataSourceNodeItem } from "./DataSourceItems"; import { IDataImportParameters, IEdgeDataImporter, IItemDataImporter, INodeDataImporter } from "./Interfaces"; import { ReplaceConnectorPointsHistoryItem } from "../History/Common/ChangeConnectorPointsHistoryItem"; import { DiagramUnit } from "../Enums"; interface IDataSourceItemsChanges { remained: ItemDataKey[]; remainedNew: ItemDataKey[]; removed: ItemDataKey[]; added: ItemDataKey[]; } interface IDataSourceChanges { nodes: IDataSourceItemsChanges; edges: IDataSourceItemsChanges; } export abstract class DataSource { nodes: DataSourceNodeItem[] = []; edges: DataSourceEdgeItem[] = []; protected autoGeneratedDataKeys: KeySet = {}; protected addInternalKeyOnInsert: boolean = false; protected useNodeParentId = false; protected useNodeContainerId = false; protected useNodeChildren = false; protected useNodeItems = false; protected canUseAutoSize = false; protected canUpdateEdgeDataSource = false; nodeDataImporter: INodeDataImporter; edgeDataImporter: IEdgeDataImporter; protected nodeDataSource: any[]; protected edgeDataSource: any[]; constructor(public key: string, nodeDataSource: any[], edgeDataSource: any[], parameters?: IDataImportParameters, nodeDataImporter?: INodeDataImporter, edgeDataImporter?: IEdgeDataImporter) { if(key === undefined || key === null) throw new Error("DataSource key must be specified"); this.key = key.toString(); this.loadParameters(parameters || {}); this.nodeDataImporter = this.createNodeDataImporter(nodeDataImporter); this.edgeDataImporter = this.createEdgeDataImporter(edgeDataImporter); this.nodeDataSource = nodeDataSource || []; this.edgeDataSource = edgeDataSource || []; this.canUpdateEdgeDataSource = !!edgeDataSource; this.fetchData(); } protected loadParameters(parameters: IDataImportParameters): void { this.addInternalKeyOnInsert = !!parameters.addInternalKeyOnInsert; } isAutoGeneratedKey(dataKey: any): boolean { return dataKey && !!this.autoGeneratedDataKeys[dataKey]; } private createNodeDataImporter(source: INodeDataImporter): INodeDataImporter { const result = new DataSourceNodeDataImporter(); if(source) this.assignNodeDataImporterProperties(source, result); return result; } private createEdgeDataImporter(source: IEdgeDataImporter): IEdgeDataImporter { const result = new DataSourceEdgeDataImporter(); if(source) this.assignEdgeDataImporterProperties(source, result); return result; } private assignItemDataImporterProperties(source: IItemDataImporter, importer: DataSourceItemDataImporter) { if(source.getKey) importer.getKey = source.getKey; if(source.setKey) importer.setKey = source.setKey; if(source.getCustomData) importer.getCustomData = source.getCustomData; if(source.setCustomData) importer.setCustomData = source.setCustomData; if(source.getLocked) importer.getLocked = source.getLocked; if(source.setLocked) importer.setLocked = source.setLocked; if(source.getStyle) importer.getStyle = source.getStyle; if(source.setStyle) importer.setStyle = source.setStyle; if(source.getStyleText) importer.getStyleText = source.getStyleText; if(source.setStyleText) importer.setStyleText = source.setStyleText; if(source.getZIndex) importer.getZIndex = source.getZIndex; if(source.setZIndex) importer.setZIndex = source.setZIndex; } private assignNodeDataImporterProperties(source: INodeDataImporter, importer: DataSourceNodeDataImporter) { this.assignItemDataImporterProperties(source, importer); if(source.getType) importer.getType = source.getType; if(source.setType) importer.setType = source.setType; if(source.getImage) importer.getImage = source.getImage; if(source.setImage) importer.setImage = source.setImage; if(source.getText) importer.getText = source.getText; if(source.setText) importer.setText = source.setText; if(source.getLeft) importer.getLeft = source.getLeft; if(source.setLeft) importer.setLeft = source.setLeft; if(source.getTop) importer.getTop = source.getTop; if(source.setTop) importer.setTop = source.setTop; if(source.getWidth) importer.getWidth = source.getWidth; if(source.setWidth) importer.setWidth = source.setWidth; if(source.getHeight) importer.getHeight = source.getHeight; if(source.setHeight) importer.setHeight = source.setHeight; if(source.getChildren) importer.getChildren = source.getChildren; if(source.setChildren) importer.setChildren = source.setChildren; if(source.getParentKey) importer.getParentKey = source.getParentKey; if(source.setParentKey) importer.setParentKey = source.setParentKey; if(source.getItems) importer.getItems = source.getItems; if(source.setItems) importer.setItems = source.setItems; if(source.getContainerKey) importer.getContainerKey = source.getContainerKey; if(source.setContainerKey) importer.setContainerKey = source.setContainerKey; } private assignEdgeDataImporterProperties(source: IEdgeDataImporter, importer: DataSourceEdgeDataImporter) { this.assignItemDataImporterProperties(source, importer); if(source.getFrom) importer.getFrom = source.getFrom; if(source.setFrom) importer.setFrom = source.setFrom; if(source.getFromPointIndex) importer.getFromPointIndex = source.getFromPointIndex; if(source.setFromPointIndex) importer.setFromPointIndex = source.setFromPointIndex; if(source.getTo) importer.getTo = source.getTo; if(source.setTo) importer.setTo = source.setTo; if(source.getToPointIndex) importer.getToPointIndex = source.getToPointIndex; if(source.setToPointIndex) importer.setToPointIndex = source.setToPointIndex; if(source.getPoints) importer.getPoints = source.getPoints; if(source.setPoints) importer.setPoints = source.setPoints; if(source.getText) importer.getText = source.getText; if(source.setText) importer.setText = source.setText; if(source.getLineOption) importer.getLineOption = source.getLineOption; if(source.setLineOption) importer.setLineOption = source.setLineOption; if(source.getStartLineEnding) importer.getStartLineEnding = source.getStartLineEnding; if(source.setStartLineEnding) importer.setStartLineEnding = source.setStartLineEnding; if(source.getEndLineEnding) importer.getEndLineEnding = source.getEndLineEnding; if(source.setEndLineEnding) importer.setEndLineEnding = source.setEndLineEnding; } protected fetchData() { this.nodes = []; this.edges = []; this.autoGeneratedDataKeys = {}; this.useNodeParentId = this.nodeDataImporter.getParentKey !== undefined; this.useNodeContainerId = this.nodeDataImporter.getContainerKey !== undefined; this.useNodeItems = this.nodeDataImporter.getItems !== undefined; this.useNodeChildren = this.nodeDataImporter.getChildren !== undefined; this.canUseAutoSize = this.nodeDataImporter.getWidth === undefined && this.nodeDataImporter.getText !== undefined; if(this.useEdgesArray() && this.useNodeParentId) throw new Error("You cannot use edges array and parentKey simultaneously."); if(this.useEdgesArray() && this.useNodeItems) throw new Error("You cannot use edges array and items array simultaneously."); if(this.useNodeParentId && this.useNodeItems) throw new Error("You cannot use parentKey and items array simultaneously."); if(this.useNodeContainerId && this.useNodeChildren) throw new Error("You cannot use containerKey and children array simultaneously."); this.nodeDataSource.forEach(nodeDataObj => { this.addNode(nodeDataObj); }); if(this.useEdgesArray()) this.edgeDataSource.forEach(edgeDataObj => { this.addEdge(edgeDataObj); }); else this.nodes.forEach(node => { this.addNodeEdgesByParentId(node); }); } private containers: KeySet = null; protected isContainer(itemKey: ItemDataKey): boolean { if(!this.containers && this.useNodeContainerId) this.containers = this.nodeDataSource .map(i => this.nodeDataImporter.getContainerKey(i)) .filter(i => i !== undefined && i !== null) .reduce((map, i) => { map[i] = true; return map; }, {}); return this.containers && this.containers[itemKey]; } refetchData(nodeDataSource?: any[], edgeDataSource?: any[]): IDataSourceChanges { this.nodeDataSource = nodeDataSource || this.nodeDataSource; this.edgeDataSource = edgeDataSource || this.edgeDataSource; const oldNodes = this.nodes.slice(); const oldEdges = this.edges.slice(); this.fetchData(); const changedNodes = this.getItemChanges(oldNodes, this.nodes, (item1: DataSourceNodeItem, item2: DataSourceNodeItem) => { return (item1.key === item2.key) || (item1.dataObj === item2.dataObj); }); const changedEdges = this.getItemChanges(oldEdges, this.edges, (item1: DataSourceEdgeItem, item2: DataSourceEdgeItem) => { if(this.useNodeParentId || this.useNodeItems) return (item1.key === item2.key) || (item1.from === item2.from && item1.to === item2.to); return (item1.key === item2.key) || (item1.dataObj === item2.dataObj); }); return { nodes: changedNodes, edges: changedEdges }; } protected getItemChanges(oldItems: DataSourceItem[], newItems: DataSourceItem[], areEqual: (item1: DataSourceItem, item2: DataSourceItem) => boolean): IDataSourceItemsChanges { const remainedItems = oldItems.filter(item => this.containsItem(newItems, item, areEqual)); const remainedNewItems = newItems.filter(item => this.containsItem(oldItems, item, areEqual)); const removedItems = oldItems.filter(item => !this.containsItem(newItems, item, areEqual)); const addedItems = newItems.filter(item => !this.containsItem(oldItems, item, areEqual)); return { remained: remainedItems.map(item => item.key), remainedNew: remainedNewItems.map(item => item.key), removed: removedItems.map(item => item.key), added: addedItems.map(item => item.key) }; } protected containsItem(items: DataSourceItem[], item: DataSourceItem, areEqual: (item1: DataSourceItem, item2: DataSourceItem) => boolean): boolean { let result = false; items.forEach(i => { if(!result && areEqual(i, item)) result = true; }); return result; } protected useEdgesArray() { return Array.isArray(this.edgeDataSource) && (this.edgeDataSource.length || !(this.useNodeParentId || this.useNodeItems)); } protected addNode(nodeDataObj: any, parentNodeDataObj?: any, containerKey?: ItemDataKey, containerNodeDataObj?: any) { const childNodeDataObjs = this.nodeDataImporter.getChildren && this.nodeDataImporter.getChildren(nodeDataObj); const hasChildren = childNodeDataObjs && Array.isArray(childNodeDataObjs) && childNodeDataObjs.length; const isContainer = hasChildren || this.isContainer(this.nodeDataImporter.getKey(nodeDataObj)); const type = this.nodeDataImporter.getType && this.nodeDataImporter.getType(nodeDataObj) || (isContainer && ShapeTypes.VerticalContainer) || ShapeTypes.Rectangle; const text = this.nodeDataImporter.getText && (this.nodeDataImporter.getText(nodeDataObj) || ""); const node = this.addNodeInternal(nodeDataObj, type, text, parentNodeDataObj, containerKey, containerNodeDataObj); this.assignNodeProperties(node, nodeDataObj); if(hasChildren) childNodeDataObjs.forEach(childNodeDataObj => { this.addNode(childNodeDataObj, undefined, node.key, nodeDataObj); }); if(this.useNodeItems) { const itemDataObjs = this.nodeDataImporter.getItems(nodeDataObj); if(Array.isArray(itemDataObjs) && itemDataObjs.length) itemDataObjs.forEach(itemDataObj => { const itemNode = this.addNode(itemDataObj, nodeDataObj, containerKey, containerNodeDataObj); this.addEdgeInternal(undefined, node.key, itemNode.key); }); } return node; } protected addNodeEdgesByParentId(node: DataSourceNodeItem) { if(this.useNodeParentId) { const parentKey = this.nodeDataImporter.getParentKey(node.dataObj); if(parentKey !== undefined && parentKey !== null) { const parentNode = this.findNode(parentKey); if(parentNode) this.addEdgeInternal(undefined, this.getNodeKey(node.dataObj, this.nodeDataImporter.getParentKey), this.getNodeKey(node.dataObj, this.nodeDataImporter.getKey)); } } } protected addNodeInternal(nodeDataObj: any, type: string, text: string, parentNodeDataObj?: any, containerKey?: ItemDataKey, containerNodeDataObj?: any) { let externalKey = this.nodeDataImporter.getKey(nodeDataObj); const key = (externalKey !== undefined && externalKey !== null) ? externalKey : ModelUtils.getGuidItemKey(); const node = new DataSourceNodeItem(this.key, key, nodeDataObj, type, text, parentNodeDataObj, containerKey, containerNodeDataObj); this.nodes.push(node); if(externalKey === undefined || externalKey === null) { externalKey = key; this.autoGeneratedDataKeys[key] = true; } return node; } protected addEdge(edgeDataObj: any) { const edge = this.addEdgeInternal(edgeDataObj, this.getNodeKey(edgeDataObj, this.edgeDataImporter.getFrom), this.getNodeKey(edgeDataObj, this.edgeDataImporter.getTo)); this.assignEdgeProperties(edge, edgeDataObj); return edge; } protected addEdgeInternal(edgeDataObj: any, from: ItemDataKey, to: ItemDataKey): DataSourceEdgeItem { let externalKey = edgeDataObj && this.edgeDataImporter.getKey(edgeDataObj); const key = (externalKey !== undefined && externalKey !== null) ? externalKey : ModelUtils.getGuidItemKey(); const edge = new DataSourceEdgeItem(this.key, key, edgeDataObj, from, to); this.edges.push(edge); if(externalKey === undefined || externalKey === null) { externalKey = key; this.autoGeneratedDataKeys[key] = true; } return edge; } protected assignItemProperties(item: DataSourceItem, dataObj: any, importer: IItemDataImporter) { if(importer.getCustomData) item.customData = ObjectUtils.cloneObject(importer.getCustomData(dataObj)); if(importer.getLocked) item.locked = importer.getLocked(dataObj); if(importer.getStyle) { const style = importer.getStyle(dataObj); item.style = typeof style === "string" ? Data.cssTextToObject(style) : style; } if(importer.getStyleText) { const style = importer.getStyleText(dataObj); item.styleText = typeof style === "string" ? Data.cssTextToObject(style) : style; } if(importer.getZIndex) item.zIndex = importer.getZIndex(dataObj); } protected assignNodeProperties(item: DataSourceNodeItem, dataObj: any) { this.assignItemProperties(item, dataObj, this.nodeDataImporter); if(this.nodeDataImporter.getImage) item.image = this.nodeDataImporter.getImage(dataObj); if(this.nodeDataImporter.getLeft) item.left = this.nodeDataImporter.getLeft(dataObj); if(this.nodeDataImporter.getTop) item.top = this.nodeDataImporter.getTop(dataObj); if(this.nodeDataImporter.getWidth) item.width = this.nodeDataImporter.getWidth(dataObj); if(this.nodeDataImporter.getHeight) item.height = this.nodeDataImporter.getHeight(dataObj); if(this.nodeDataImporter.getContainerKey) item.containerKey = this.nodeDataImporter.getContainerKey(dataObj); } protected assignEdgeProperties(item: DataSourceEdgeItem, dataObj: any) { this.assignItemProperties(item, dataObj, this.edgeDataImporter); if(this.edgeDataImporter.getFromPointIndex) item.fromPointIndex = this.edgeDataImporter.getFromPointIndex(dataObj); if(this.edgeDataImporter.getToPointIndex) item.toPointIndex = this.edgeDataImporter.getToPointIndex(dataObj); if(this.edgeDataImporter.getPoints) item.points = this.edgeDataImporter.getPoints(dataObj); if(this.edgeDataImporter.getText) { const texts = this.edgeDataImporter.getText(dataObj); item.texts = {}; if(typeof texts === "object") for(const key in texts) { if(!Object.prototype.hasOwnProperty.call(texts, key)) continue; let position = parseFloat(key); const text = texts[key]; if(!isNaN(position) && typeof text === "string" && text !== "") { position = Math.min(1, Math.max(0, position)); item.texts[position] = text; } } else if(typeof texts === "string" && texts !== "") item.texts[CONNECTOR_DEFAULT_TEXT_POSITION] = texts; } if(this.edgeDataImporter.getLineOption) item.lineOption = this.edgeDataImporter.getLineOption(dataObj); if(this.edgeDataImporter.getStartLineEnding) item.startLineEnding = this.edgeDataImporter.getStartLineEnding(dataObj); if(this.edgeDataImporter.getEndLineEnding) item.endLineEnding = this.edgeDataImporter.getEndLineEnding(dataObj); } findNode(key: ItemDataKey): DataSourceNodeItem { return this.nodes.filter(i => key !== undefined && i.key === key)[0]; } findEdge(key: ItemDataKey): DataSourceEdgeItem { return this.edges.filter(i => key !== undefined && i.key === key)[0]; } protected getNodeKey(nodeDataObj: any, getKey: (obj: any) => ItemDataKey) { return getKey(nodeDataObj); } createModelItems(history: History, model: DiagramModel, shapeDescriptionManager: IShapeDescriptionManager, selection: Selection, layoutParameters: DataLayoutParameters, snapToGrid: boolean, gridSize: number, measurer?: ITextMeasurer) { this.beginChangesNotification(); history.clear(); history.beginTransaction(); ModelUtils.deleteAllItems(history, model, selection); model.initializeKeyCounter(); const DEFAULT_STEP = 2000; let rowIndex = 0; let colIndex = 0; const externalToInnerMap: {[externalID: number]: ItemKey} = {}; const shapes: Shape[] = []; const connectors: Connector[] = []; this.nodes.forEach(node => { const point = new Point(colIndex++ * DEFAULT_STEP, rowIndex * DEFAULT_STEP); const shape = this.createShapeByNode(history, model, selection, shapeDescriptionManager, node, point, layoutParameters, snapToGrid, gridSize, measurer); if(node.key !== undefined) externalToInnerMap[node.key] = shape.key; if(colIndex > 4) { colIndex = 0; rowIndex++; } shapes.push(shape); }); this.nodes.forEach(node => { if(node.containerKey !== undefined && node.containerKey !== null) { const shapeKey = externalToInnerMap[node.key]; const shape = model.findShape(shapeKey); const containerShapeKey = externalToInnerMap[node.containerKey]; const containerShape = model.findShape(containerShapeKey); if(containerShape) ModelUtils.insertToContainer(history, model, shape, containerShape); } }); this.edges.forEach(edge => { const toShape = model.findShape(externalToInnerMap[edge.to]); const fromShape = model.findShape(externalToInnerMap[edge.from]); const connector = this.createConnectorByEdge(history, model, selection, edge, fromShape, toShape); if(connector) { connectors.push(connector); ModelUtils.updateConnectorContainer(history, model, connector); } }); if(layoutParameters.needAutoLayout) this.applyLayout(history, model, shapes, connectors, layoutParameters, snapToGrid, gridSize); ModelUtils.tryUpdateModelRectangle(history); history.endTransaction(true); this.endChangesNotification(true); } updateModelItems(history: History, model: DiagramModel, shapeDescriptionManager: IShapeDescriptionManager, selection: Selection, layoutParameters: DataLayoutParameters, addNewHistoryItem: boolean, updateDataKeys: ItemDataKey[], updateTemplateItem: (item: DiagramItem) => void, changes: IDataSourceChanges, snapToGrid: boolean, gridSize: number, measurer?: ITextMeasurer): void { this.beginChangesNotification(); history.beginTransaction(); const itemsToUpdate = []; let layoutShapes = []; const layoutConnectors = []; const shapesToRemove = changes.nodes.removed.map(key => model.findShapeByDataKey(key)).filter(item => item); shapesToRemove.forEach(shape => { shape.attachedConnectors.forEach(connector => { if(connector.beginItem && connector.beginItem !== shape) layoutShapes.push(connector.beginItem); if(connector.endItem && connector.endItem !== shape) layoutShapes.push(connector.endItem); }); }); ModelUtils.deleteItems(history, model, selection, shapesToRemove, true); const connectorsToRemove = changes.edges.removed.map(key => model.findConnectorByDataKey(key)).filter(item => item); connectorsToRemove.forEach(connector => { if(connector.beginItem) layoutShapes.push(connector.beginItem); if(connector.endItem) layoutShapes.push(connector.endItem); }); ModelUtils.deleteItems(history, model, selection, connectorsToRemove, true); layoutShapes = this.purgeLayoutShapes(layoutShapes, shapesToRemove); const nodeKeysToUpdate = updateDataKeys || []; nodeKeysToUpdate.forEach(dataKey => { if(changes.nodes.remained.indexOf(dataKey) === -1) return; const node = this.findNode(dataKey); if(node) { let shape = model.findShapeByDataKey(dataKey); if(shape) { const position = shape.position.clone(); this.changeShapeByDataItem(history, model, shape, node, position); this.changeItemByDataItem(history, shape, node); } else shape = this.createShapeByNode(history, model, selection, shapeDescriptionManager, node, new Point(0, 0), layoutParameters, snapToGrid, gridSize, measurer); this.updateShapeContainer(history, model, shape, node); layoutShapes.push(shape); itemsToUpdate.push(shape); } }); changes.nodes.remained.forEach((dataKey, index) => { const shape = model.findShapeByDataKey(dataKey); if(shape) shape.dataKey = changes.nodes.remainedNew[index]; }); changes.nodes.added.forEach(dataKey => { const node = this.findNode(dataKey); const shape = this.createShapeByNode(history, model, selection, shapeDescriptionManager, node, new Point(0, 0), layoutParameters, snapToGrid, gridSize, measurer); this.updateShapeContainer(history, model, shape, node); layoutShapes.push(shape); }); changes.edges.added.forEach(dataKey => { const edge = this.findEdge(dataKey); const fromShape = model.findShapeByDataKey(edge.from); const toShape = model.findShapeByDataKey(edge.to); const connector = this.createConnectorByEdge(history, model, selection, edge, fromShape, toShape); if(connector) { ModelUtils.updateConnectorContainer(history, model, connector); layoutConnectors.push(connector); } }); const edgeKeysToUpdate = updateDataKeys || []; changes.edges.remained.forEach(dataKey => { const edge = this.findEdge(dataKey); if(edge && ((changes.nodes.added.indexOf(edge.from) !== -1) || (changes.nodes.added.indexOf(edge.to) !== -1))) edgeKeysToUpdate.push(dataKey); }); edgeKeysToUpdate.forEach(dataKey => { if(changes.edges.remained.indexOf(dataKey) === -1) return; const edge = this.findEdge(dataKey); if(edge) { const fromShape = model.findShapeByDataKey(edge.from); const toShape = model.findShapeByDataKey(edge.to); let connector = model.findConnectorByDataKey(dataKey); if(connector) { this.changeConnectorPointsByDataItem(history, connector, this.getConnectorPointsByEdge(model, edge, fromShape, toShape)); this.changeConnectorByDataItem(history, model, connector, fromShape, toShape, edge); this.changeItemByDataItem(history, connector, edge); } else connector = this.createConnectorByEdge(history, model, selection, edge, fromShape, toShape); if(connector) { ModelUtils.updateConnectorContainer(history, model, connector); layoutConnectors.push(connector); itemsToUpdate.push(connector); } } }); changes.edges.remained.forEach((dataKey, index) => { const connector = model.findConnectorByDataKey(dataKey); if(connector) connector.dataKey = changes.edges.remainedNew[index]; }); if(itemsToUpdate.length && updateTemplateItem) itemsToUpdate.forEach(item => { item.hasTemplate && updateTemplateItem(item); }); if(layoutParameters.needAutoLayout && (layoutShapes.length || layoutConnectors.length)) this.applyLayout(history, model, layoutShapes, layoutConnectors, layoutParameters, snapToGrid, gridSize); ModelUtils.tryUpdateModelRectangle(history); history.endTransaction(!addNewHistoryItem); this.endChangesNotification(false); } purgeLayoutShapes(layoutShapes: Shape[], shapesToRemove: DiagramItem[]): Shape[] { const shapesToRemoveKeySet = shapesToRemove.reduce((acc, shape) => (acc[shape.key] = true) && acc, {}); return layoutShapes.reduce((acc, shape) => { if(acc.keySet[shape.key] === undefined && shapesToRemoveKeySet[shape.key] === undefined) { acc.uniqueShapes.push(shape); acc.keySet[shape.key] = true; } return acc; }, { uniqueShapes: [], keySet: {} }).uniqueShapes; } protected applyShapeAutoSize(history: History, measurer: ITextMeasurer, shapeSizeSettings: IShapeSizeSettings, shape: Shape, snapToGrid: boolean, gridSize: number): void { if(!shape.description.enableText) return; const shapeTextSize = shape.textRectangle.createSize(); const shapeSize = shape.size; const textHorOffset = shapeTextSize.width - shapeSize.width; const textVerOffset = shapeTextSize.height - shapeSize.height; const maxWidth = shape.getMaxWidth(shapeSizeSettings.shapeMaxWidth); const maxHeight = shape.getMaxHeight(shapeSizeSettings.shapeMaxHeight); const sizeToPx = (size: number | undefined, isHorizontal: boolean) => typeof (size) === "number" ? UnitConverter.twipsToPixelsF(size + (isHorizontal ? textHorOffset : textVerOffset)) : undefined; const newShapeTextSize = getOptimalTextRectangle(shape.text, shape.styleText, TextOwner.Shape, measurer, shapeTextSize.clone().applyConverter(UnitConverter.twipsToPixelsF), shape.description.keepRatioOnAutoSize, sizeToPx(shape.getMinWidth(shapeSizeSettings.shapeMinWidth), true), sizeToPx(maxWidth, true), sizeToPx(shape.getMinHeight(shapeSizeSettings.shapeMinHeight), false), sizeToPx(maxHeight, false)) .clone().applyConverter(UnitConverter.pixelsToTwips); if(!newShapeTextSize.equals(shapeTextSize)) { let shapeNewSize = shape.description.getSizeByText(newShapeTextSize, shape); if(snapToGrid && gridSize) shapeNewSize = new Size( Math.min(gridSize * Math.ceil(shapeNewSize.width / gridSize), maxWidth || Number.MAX_VALUE), Math.min(gridSize * Math.ceil(shapeNewSize.height / gridSize), maxHeight || Number.MAX_VALUE)); history.addAndRedo(new ResizeShapeHistoryItem(shape.key, shape.position, shapeNewSize)); } } applyLayout(history: History, model: DiagramModel, shapes: Shape[], connectors: Connector[], layoutParameters: DataLayoutParameters, snapToGrid: boolean, gridSize: number): void { const graphInfo = ModelUtils.getGraphInfoByItems(model, shapes, connectors); graphInfo.forEach(info => { const layout = layoutParameters.getLayoutBuilder(info.graph).build(); const nonGraphItems = ModelUtils.getNonGraphItems(model, info.container, layout.nodeToLayout, shapes, connectors); ModelUtils.applyLayout(history, model, info.container, info.graph, layout, nonGraphItems, layoutParameters.layoutSettings, snapToGrid, gridSize, layoutParameters.skipPointIndices); }); } private changeItemByDataItem(history: History, item: DiagramItem, dataItem: DataSourceItem) { if(dataItem.customData !== undefined && !ObjectUtils.compareObjects(dataItem.customData, item.customData)) history.addAndRedo(new ChangeCustomDataHistoryItem(item.key, dataItem.customData)); if(dataItem.zIndex !== undefined && dataItem.zIndex !== item.zIndex) history.addAndRedo(new ChangeZindexHistoryItem(item, dataItem.zIndex)); if(dataItem.style !== undefined) for(const key in dataItem.style) { if(!Object.prototype.hasOwnProperty.call(dataItem.style, key)) continue; const value = this.getPreparedStyleValue(dataItem.style[key], isColorProperty(key)); if(value !== item.style[key]) history.addAndRedo(new ChangeStyleHistoryItem(item.key, key, value)); } const defaultStyle = item.style.getDefaultInstance(); item.style.forEach(key => { if((dataItem.style && dataItem.style[key] === undefined) && item.style[key] !== defaultStyle[key]) history.addAndRedo(new ChangeStyleHistoryItem(item.key, key, defaultStyle[key])); }); if(dataItem.styleText !== undefined) for(const key in dataItem.styleText) { if(!Object.prototype.hasOwnProperty.call(dataItem.styleText, key)) continue; const value = this.getPreparedStyleValue(dataItem.styleText[key], isColorProperty(key)); if(value !== item.styleText[key]) history.addAndRedo(new ChangeStyleTextHistoryItem(item.key, key, value)); } const defaultTextStyle = item.styleText.getDefaultInstance(); item.styleText.forEach(key => { if((dataItem.styleText && dataItem.styleText[key] === undefined) && item.styleText[key] !== defaultTextStyle[key]) history.addAndRedo(new ChangeStyleTextHistoryItem(item.key, key, defaultTextStyle[key])); }); if(dataItem.locked !== undefined && dataItem.locked !== item.locked) history.addAndRedo(new ChangeLockedHistoryItem(item, dataItem.locked)); } getPreparedStyleValue(value: any, isColorProperty: boolean): any { if(isColorProperty) { const colorValue = ColorUtils.stringToHash(value); if(colorValue !== null) value = colorValue; } return value; } private createShapeByNode(history: History, model: DiagramModel, selection: Selection, shapeDescriptionManager: IShapeDescriptionManager, node: DataSourceNodeItem, point: Point, layoutParameters: DataLayoutParameters, snapToGrid: boolean, gridSize: number, measurer?: ITextMeasurer): Shape { const insert = new AddShapeHistoryItem(shapeDescriptionManager.get(node.type), point, "", node.key); history.addAndRedo(insert); const shape = model.findShape(insert.shapeKey); ModelUtils.updateNewShapeProperties(history, selection, insert.shapeKey); this.changeShapeByDataItem(history, model, shape, node, point); this.changeItemByDataItem(history, shape, node); if(measurer && this.canUseAutoSize && layoutParameters.autoSizeEnabled) this.applyShapeAutoSize(history, measurer, layoutParameters.sizeSettings, shape, snapToGrid, gridSize); return shape; } private changeShapeByDataItem(history: History, model: DiagramModel, shape: Shape, node: DataSourceNodeItem, point: Point) { if(node.left !== undefined) point.x = ModelUtils.getTwipsValue(model.units, node.left); if(node.top !== undefined) point.y = ModelUtils.getTwipsValue(model.units, node.top); ModelUtils.setShapePosition(history, model, shape, point, false); const size = shape.size.clone(); if(node.width !== undefined) size.width = ModelUtils.getTwipsValue(model.units, node.width); if(node.height !== undefined) size.height = ModelUtils.getTwipsValue(model.units, node.height); ModelUtils.setShapeSize(history, model, shape, point, size); ModelUtils.updateShapeAttachedConnectors(history, model, shape); if(node.text !== undefined && node.text !== shape.text) history.addAndRedo(new ChangeShapeTextHistoryItem(shape, node.text)); if(node.image !== undefined && node.image !== shape.image.actualUrl) history.addAndRedo(new ChangeShapeImageHistoryItem(shape, node.image)); } private updateShapeContainer(history: History, model: DiagramModel, shape: Shape, node: DataSourceNodeItem) { const containerShape = (node.containerKey !== undefined) ? model.findShapeByDataKey(node.containerKey) : undefined; if(containerShape !== shape.container) if(containerShape) ModelUtils.insertToContainer(history, model, shape, containerShape); else ModelUtils.removeFromContainer(history, model, shape); } private getConnectorPointsByEdge(model: DiagramModel, edge: DataSourceEdgeItem, fromShape: Shape, toShape: Shape): Point[] { const result: Point[] = []; const modelPoints = this.createModelPointFromDataSourceEdgeItemPoints(model.units, edge); if(!modelPoints || modelPoints.length <= 1) { if(!fromShape || !toShape) return undefined; result.push(fromShape.position.clone()); result.push(toShape.position.clone()); return result; } const lastIndex = modelPoints.length - 1; for(let i = 0; i <= lastIndex; i++) { const modelPoint = modelPoints[i]; if(modelPoint !== null) result.push(modelPoint); else if(!fromShape && !toShape) return undefined; else if(i === 0 && fromShape) result.push(fromShape.position.clone()); else if(i === lastIndex && toShape) result.push(toShape.position.clone()); } return result; } private createModelPointFromDataSourceEdgeItemPoints(units: DiagramUnit, edge : DataSourceEdgeItem) : Point[] { const result: Point[] = []; if(!Array.isArray(edge.points)) return undefined; edge.points.forEach(dep => result.push(this.isValidDataSourceEdgeItemPoint(dep) ? this.createModelPoint(units, dep) : null)); return result; } private createModelPoint(units: DiagramUnit, point: any) : Point { return new Point( ModelUtils.getTwipsValue(units, point.x), ModelUtils.getTwipsValue(units, point.y) ); } private isValidDataSourceEdgeItemPoint(point: any) : boolean { return point !== undefined && point !== null && point.x !== undefined && point.y !== undefined && point.x !== null && point.y !== null; } private createConnectorByEdge(history: History, model: DiagramModel, selection: Selection, edge: DataSourceEdgeItem, fromShape: Shape, toShape: Shape): Connector { let connector: Connector; const dataKey = edge.key; const points = this.getConnectorPointsByEdge(model, edge, fromShape, toShape); if(points && points.length > 1) { const insert = new AddConnectorHistoryItem(points, dataKey); history.addAndRedo(insert); connector = model.findConnector(insert.connectorKey); ModelUtils.updateNewConnectorProperties(history, selection, insert.connectorKey); this.changeConnectorByDataItem(history, model, connector, fromShape, toShape, edge); this.changeItemByDataItem(history, connector, edge); } return connector; } private changeConnectorByDataItem(history: History, model: DiagramModel, connector: Connector, fromShape: Shape, toShape: Shape, edge: DataSourceEdgeItem) { const fromPointIndex = edge.fromPointIndex !== undefined ? edge.fromPointIndex : connector.beginConnectionPointIndex; if(connector.beginItem !== fromShape || connector.beginConnectionPointIndex !== fromPointIndex) { if(connector.beginItem) history.addAndRedo(new DeleteConnectionHistoryItem(connector, ConnectorPosition.Begin)); if(fromShape) history.addAndRedo(new AddConnectionHistoryItem(connector, fromShape, fromPointIndex, ConnectorPosition.Begin)); } const toPointIndex = edge.toPointIndex !== undefined ? edge.toPointIndex : connector.endConnectionPointIndex; if(connector.endItem !== toShape || connector.endConnectionPointIndex !== toPointIndex) { if(connector.endItem) history.addAndRedo(new DeleteConnectionHistoryItem(connector, ConnectorPosition.End)); if(toShape) history.addAndRedo(new AddConnectionHistoryItem(connector, toShape, toPointIndex, ConnectorPosition.End)); } ModelUtils.updateConnectorAttachedPoints(history, model, connector); if(edge.texts !== undefined && !this.compareTexts(edge, connector)) { connector.texts.forEach(text => { history.addAndRedo(new ChangeConnectorTextHistoryItem(connector, text.position, undefined)); }); for(const key in edge.texts) { if(!Object.prototype.hasOwnProperty.call(edge.texts, key)) continue; const position = parseFloat(key); history.addAndRedo(new ChangeConnectorTextHistoryItem(connector, position, edge.texts[key])); } } if(edge.lineOption !== undefined && edge.lineOption !== connector.properties.lineOption) history.addAndRedo(new ChangeConnectorPropertyHistoryItem(connector.key, "lineOption", edge.lineOption)); if(edge.startLineEnding !== undefined && edge.startLineEnding !== connector.properties.startLineEnding) history.addAndRedo(new ChangeConnectorPropertyHistoryItem(connector.key, "startLineEnding", edge.startLineEnding)); if(edge.endLineEnding !== undefined && edge.endLineEnding !== connector.properties.endLineEnding) history.addAndRedo(new ChangeConnectorPropertyHistoryItem(connector.key, "endLineEnding", edge.endLineEnding)); } private changeConnectorPointsByDataItem(history: History, connector: Connector, newPoints: Point[]) { if(newPoints && newPoints.length > 1 && newPoints.join(",") !== connector.points.join(",")) history.addAndRedo(new ReplaceConnectorPointsHistoryItem(connector.key, newPoints)); } protected compareTexts(edgeObj: DataSourceEdgeItem, connector: Connector): boolean { const texts = edgeObj.texts || {}; let result = Object.keys(texts).length === connector.getTextCount(); if(result) for(const key in texts) { if(!Object.prototype.hasOwnProperty.call(texts, key)) continue; const position = parseFloat(key); if(!this.compareStrings(connector.getText(position), texts[key])) result = false; } return result; } protected compareStrings(str1: string, str2: string): boolean { if(typeof str1 === "string" && typeof str2 === "string") return str1 === str2; return this.isEmptyString(str1) && this.isEmptyString(str2); } protected isEmptyString(str: string): boolean { return str === "" || str === null || str === undefined; } protected abstract beginChangesNotification(): void; protected abstract endChangesNotification(preventReloadContent : boolean) : void; }