devexpress-diagram
Version:
DevExpress Diagram Control
454 lines (433 loc) • 23.2 kB
text/typescript
import { Offsets } from "@devexpress/utils/lib/geometry/offsets";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Diagnostics } from "../Diagnostics";
import { CacheImageInfo, ImageCache } from "../Images/ImageCache";
import { ImageInfo } from "../Images/ImageInfo";
import { ImageLoader } from "../Images/ImageLoader";
import { DiagramModelOperation } from "../ModelOperationSettings";
import { EventDispatcher, GeometryUtils, ObjectUtils } from "../Utils";
import { Connector, ConnectorPosition } from "./Connectors/Connector";
import { IConnectorRoutingModel } from "./Connectors/Routing/ConnectorRoutingModel";
import { DiagramItem, ItemKey } from "./DiagramItem";
import { DiagramModel } from "./Model";
import { ItemChange, ItemChangeType } from "./ModelChange";
import { ModelUtils } from "./ModelUtils";
import { IPermissionsProvider, PermissionsProvider } from "./Permissions/PermissionsProvider";
import { ShapeDescription } from "./Shapes/Descriptions/ShapeDescription";
import { Shape } from "./Shapes/Shape";
import { ShapeParameters } from "./Shapes/ShapeParameters";
export class ModelManipulator {
model: DiagramModel;
permissionsProvider: IPermissionsProvider;
routingModel : IConnectorRoutingModel;
imageLoader: ImageLoader;
onModelChanged: EventDispatcher<IModelChangesListener> = new EventDispatcher();
onModelSizeChanged: EventDispatcher<IModelSizeListener> = new EventDispatcher();
constructor(model: DiagramModel, routingModel: IConnectorRoutingModel, permissionsProvider: PermissionsProvider) {
this.initializeCore(model, routingModel);
this.permissionsProvider = permissionsProvider;
this.imageLoader = new ImageLoader(this.updateShapeImage.bind(this));
}
initialize(model: DiagramModel, routingModel: IConnectorRoutingModel): void {
this.initializeCore(model, routingModel);
this.model.loadAllImages(this.imageLoader);
this.updateModelSize();
}
private initializeCore(model: DiagramModel, routingModel: IConnectorRoutingModel) {
this.model = model;
this.routingModel = routingModel;
if(this.routingModel)
this.routingModel.initialize(model);
}
commitPageChanges(): void {
this.raisePageSizeChanged(this.model.pageSize.clone(), this.model.pageLandscape);
this.raiseModelSizeChanged(this.model.size.clone());
this.raisePageColorChanged(this.model.pageColor);
this.raiseModelRectangleChanged(ModelUtils.createRectangle(this.model.items));
}
commitItemsCreateChanges(): void {
Diagnostics.timer("new model: model changes");
this.commitItemsChangesCore(ItemChangeType.Create, this.model.items);
Diagnostics.endTimer();
}
commitItemUpdateChanges(item: DiagramItem): void {
this.commitItemsChangesCore(ItemChangeType.UpdateStructure, [ item ]);
}
private commitItemsChangesCore(changeType: ItemChangeType, items: DiagramItem[]) {
const changes: ItemChange[] = [];
items.forEach(item => {
changes.push(new ItemChange(item, changeType));
});
if(changes.length)
this.raiseModelChanged(changes);
}
insertToContainer(item: DiagramItem, container: Shape): void {
if(item.container && container && item.container.key !== container.key)
throw Error("To insert an item to a container it's necessary to remove it from the current container.");
if(container) {
if(container.children.indexOf(item) === -1)
container.children.push(item);
item.container = container;
this.raiseModelChanged([new ItemChange(item, ItemChangeType.Update)]);
}
}
removeFromContainer(item: DiagramItem): void {
if(item.container) {
const index = item.container.children.indexOf(item);
item.container.children.splice(index, 1);
item.container = undefined;
this.raiseModelChanged([new ItemChange(item, ItemChangeType.Update)]);
}
}
changeStyle(item: DiagramItem, styleProperty: string, styleValue: string): void {
this.changeStyleCore(item, item.style, styleProperty, styleValue);
}
changeStyleText(item: DiagramItem, styleProperty: string, styleValue: string): void {
this.changeStyleCore(item, item.styleText, styleProperty, styleValue);
}
changeStyleCore(item: DiagramItem, styleObj: any, styleProperty: string, styleValue: string): void {
if(styleValue !== undefined)
styleObj[styleProperty] = styleValue;
else
delete styleObj[styleProperty];
this.raiseModelChanged([new ItemChange(item, ItemChangeType.UpdateProperties)]);
}
changeZIndex(item: DiagramItem, zIndex: number): void {
item.zIndex = zIndex;
this.raiseModelChanged([new ItemChange(item, ItemChangeType.Update)]);
}
changeLocked(item: DiagramItem, locked: boolean): void {
item.locked = locked;
this.raiseModelChanged([new ItemChange(item, ItemChangeType.UpdateClassName)]);
}
changeCustomData(item: DiagramItem, data: any): void {
item.customData = ObjectUtils.cloneObject(data);
this.raiseModelChanged([new ItemChange(item, ItemChangeType.UpdateStructure)]);
}
addShape(shape: Shape, key?: ItemKey): Shape {
if(shape.attachedConnectors.length)
throw Error("A creating shape should not contain existing connectors.");
shape.key = key !== undefined ? key : this.model.getNextKey();
return this.insertShape(shape);
}
insertShape(shape: Shape): Shape {
this.model.pushItem(shape);
const allowed = this.permissionsProvider.canAddItems([shape]);
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.Create, allowed)]);
this.model.loadAllImages(this.imageLoader);
return shape;
}
resizeShape(shape: Shape, position: Point, size: Size): void {
shape.position = position;
shape.size = size;
let allowed = this.permissionsProvider.isStoredPermissionsGranted();
const resizeInteractingItem = this.getInteractingItem(shape, DiagramModelOperation.ResizeShape);
if(resizeInteractingItem) {
const oldSize = (<Shape>resizeInteractingItem).size.clone();
const size = shape.size.clone();
if(!size.equals(oldSize))
allowed = this.permissionsProvider.canResizeShapes([{ shape, size, oldSize }]);
}
const moveInteractingItem = this.getInteractingItem(shape, DiagramModelOperation.MoveShape);
if(moveInteractingItem) {
const oldPosition = (<Shape>moveInteractingItem).position.clone();
const position = shape.position.clone();
if(!position.equals(oldPosition))
allowed = this.permissionsProvider.canMoveShapes([{ shape, position, oldPosition }]);
}
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.UpdateProperties, allowed)]);
}
moveShape(shape: Shape, position: Point): void {
shape.position = position;
let allowed = this.permissionsProvider.isStoredPermissionsGranted();
const addInteractingItem = this.getInteractingItem(shape, DiagramModelOperation.AddShape);
if(addInteractingItem)
allowed = this.permissionsProvider.canAddItems([shape]);
const moveInteractingItem = this.getInteractingItem(shape, DiagramModelOperation.MoveShape);
if(moveInteractingItem) {
const oldPosition = (<Shape>moveInteractingItem).position.clone();
const position = shape.position.clone();
if(!position.equals(oldPosition))
allowed = this.permissionsProvider.canMoveShapes([{ shape, position, oldPosition }]);
}
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.UpdateProperties, allowed)]);
}
changeShapeParameters(shape: Shape, parameters: ShapeParameters): void {
shape.parameters.forEach((p) => {
const parameter = parameters.get(p.key);
if(parameter)
p.value = parameter.value;
});
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.UpdateProperties)]);
}
changeShapeText(shape: Shape, text: string): void {
shape.text = text;
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.UpdateStructure)]);
}
changeShapeImage(shape: Shape, image: ImageInfo): void {
shape.image = image;
const cachedImage = ImageCache.instance.createUnloadedInfoByShapeImageInfo(image);
this.imageLoader.load(cachedImage);
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.UpdateStructure)]);
}
changeShapeExpanded(shape: Shape, expanded: boolean): void {
shape.expanded = expanded;
shape.toggleExpandedSize();
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.UpdateStructure)]);
}
deleteShape(shape: Shape, allowed: boolean): void {
if(shape.attachedConnectors.length)
throw Error("A removing shape should not contain existing connectors.");
this.removeShape(shape, allowed);
}
removeShape(shape: Shape, allowed: boolean): void {
this.model.removeItem(shape);
this.raiseModelChanged([new ItemChange(shape, ItemChangeType.Remove, allowed)]);
}
updateShapeImage(cacheImageInfo: CacheImageInfo): void {
if(!cacheImageInfo.imageUrl) return;
const shapes = this.model.findShapesByImageUrl(cacheImageInfo.imageUrl);
shapes.forEach(shape => {
if(cacheImageInfo.base64)
shape.image.loadBase64Content(cacheImageInfo.base64);
else
shape.image.setUnableToLoadFlag();
});
this.commitItemsChangesCore(ItemChangeType.UpdateStructure, shapes);
}
updateShapeDescription(description: ShapeDescription): void {
const shapes = this.model.findShapesByDescription(description);
this.commitItemsChangesCore(ItemChangeType.UpdateProperties, shapes);
}
addConnector(connector: Connector, key?: ItemKey): Connector {
if(connector.beginItem || connector.endItem)
throw Error("Creating connector should not contain begin/end items");
connector.key = key !== undefined ? key : this.model.getNextKey();
return this.insertConnector(connector);
}
insertConnector(connector: Connector): Connector {
this.model.pushItem(connector);
const routingStrategy = this.routingModel.createStrategy(connector.properties.lineOption);
if(routingStrategy)
connector.changeRoutingStrategy(routingStrategy);
else
connector.clearRoutingStrategy();
const allowed = this.permissionsProvider.canAddItems([connector]);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.Create, allowed)]);
return connector;
}
deleteConnector(connector: Connector): void {
if(connector.beginItem || connector.endItem)
throw Error("Creating connector should not contain begin/end items");
this.removeConnector(connector);
}
removeConnector(connector: Connector): void {
this.model.removeItem(connector);
const allowed = this.permissionsProvider.canDeleteItems([connector]);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.Remove, allowed)]);
}
addDeleteConnectorPoint(connector: Connector, callBack: (connector : Connector) => void): void {
const oldConnectorPoints = this.getConnectorInteractingPoints(connector);
callBack(connector);
this.addDeleteConnectorPointCore(connector, oldConnectorPoints);
}
moveConnectorPoint(connector: Connector, pointIndex: number, callBack: (connector : Connector) => void): void {
callBack(connector);
this.moveConnectorPointCore(connector, pointIndex);
}
changeConnectorPoints(connector: Connector, callBack: (connector : Connector) => void): void {
callBack(connector);
connector.points.forEach((_, i) => this.moveConnectorPointCore(connector, i));
}
private moveConnectorPointCore(connector: Connector, pointIndex: number) {
const interactingItem = this.getInteractingItem(connector);
let allowed = this.permissionsProvider.isStoredPermissionsGranted();
if(interactingItem) {
let changeConnectionPoints = (0 < pointIndex && pointIndex < connector.points.length - 1);
changeConnectionPoints = changeConnectionPoints || (pointIndex === 0 && !connector.beginItem);
changeConnectionPoints = changeConnectionPoints || (pointIndex === connector.points.length - 1 && !connector.endItem);
if(changeConnectionPoints) {
const oldConnectorPoints = (interactingItem as Connector).points.map(p => p.clone());
const newConnectorPoints = connector.points.map(p => p.clone());
if(!GeometryUtils.arePointsEqual(oldConnectorPoints, newConnectorPoints))
allowed = this.permissionsProvider.canChangeConnectorPoints(connector, oldConnectorPoints, newConnectorPoints);
}
}
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateProperties, allowed)]);
}
private getConnectorInteractingPoints(connector: Connector) : Point[] {
const interactingItem = this.getInteractingItem(connector);
return interactingItem ? (interactingItem as Connector).points.map(p => p.clone()) : connector.points.map(p => p.clone());
}
private addDeleteConnectorPointCore(connector: Connector, oldConnectorPoints: Point[]) {
let allowed = this.permissionsProvider.isStoredPermissionsGranted();
const newConnectorPoints = connector.points.map(p => p.clone());
if(!GeometryUtils.arePointsEqual(oldConnectorPoints, newConnectorPoints))
allowed = this.permissionsProvider.canChangeConnectorPoints(connector, oldConnectorPoints, newConnectorPoints);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateProperties, allowed)]);
}
addConnection(connector: Connector, item: DiagramItem, connectionPointIndex: number, position: ConnectorPosition): void {
const existingItem = connector.getExtremeItem(position);
const existingConnectionPointIndex = connector.getExtremeConnectionPointIndex(position);
if(existingItem === item && existingConnectionPointIndex === connectionPointIndex)
return;
else if(existingItem)
throw Error("Connector is already connected");
item.attachedConnectors.push(connector);
if(position === ConnectorPosition.Begin) {
connector.beginItem = item;
connector.beginConnectionPointIndex = connectionPointIndex;
}
else {
connector.endItem = item;
connector.endConnectionPointIndex = connectionPointIndex;
}
connector.invalidateRenderPoints();
const allowed = this.permissionsProvider.canChangeConnection(connector, item, undefined, position, connectionPointIndex);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateProperties, allowed)]);
}
setConnectionPointIndex(connector: Connector, connectionPointIndex: number, position: ConnectorPosition): void {
if(!connector.getExtremeItem(position))
throw Error("Connection should be connected");
if(position === ConnectorPosition.Begin)
connector.beginConnectionPointIndex = connectionPointIndex;
else
connector.endConnectionPointIndex = connectionPointIndex;
connector.invalidateRenderPoints();
const item = connector.getExtremeItem(position);
const allowed = this.permissionsProvider.canChangeConnection(connector, item, item, position, connectionPointIndex);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateProperties, allowed)]);
}
deleteConnection(connector: Connector, position: ConnectorPosition): void {
const item = connector.getExtremeItem(position);
if(!item)
return;
item.attachedConnectors.splice(item.attachedConnectors.indexOf(connector), 1);
if(position === ConnectorPosition.Begin) {
connector.beginItem = null;
connector.beginConnectionPointIndex = -1;
}
else {
connector.endItem = null;
connector.endConnectionPointIndex = -1;
}
connector.invalidateRenderPoints();
const allowed = this.permissionsProvider.canChangeConnection(connector, undefined, item, position, -1);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateProperties, allowed)]);
}
changeConnectorProperty(connector: Connector, propertyName: string, value: any): void {
connector.properties[propertyName] = value;
if(propertyName === "lineOption") {
const routingStrategy = this.routingModel ? this.routingModel.createStrategy(connector.properties.lineOption) : undefined;
if(routingStrategy)
connector.changeRoutingStrategy(routingStrategy);
else
connector.clearRoutingStrategy();
}
else
connector.invalidateRenderPoints();
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateProperties)]);
}
changeConnectorText(connector: Connector, text: string, position: number): void {
connector.setText(text, position);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateStructure)]);
}
changeConnectorTextPosition(connector: Connector, position: number, newPosition: number): void {
const text = connector.getText(position);
connector.setText(null, position);
connector.setText(text, newPosition);
this.raiseModelChanged([new ItemChange(connector, ItemChangeType.UpdateProperties)]);
}
changeModelSize(size: Size, offset: Offsets): void {
this.model.size.width = size.width;
this.model.size.height = size.height;
this.raiseModelSizeChanged(this.model.size.clone(), offset);
if(offset.left || offset.top) {
this.model.snapStartPoint = this.model.snapStartPoint.clone().offset(offset.left, offset.top);
this.raiseSnapPointChange(this.model.snapStartPoint);
}
}
changePageSize(value: Size): void {
if(!this.model.pageSize.equals(value)) {
this.model.pageSize = value;
this.model.size = new Size(this.model.pageWidth, this.model.pageHeight);
this.raiseModelSizeChanged(this.model.size.clone());
this.raisePageSizeChanged(this.model.pageSize, this.model.pageLandscape);
}
}
changePageLandscape(value: boolean): void {
if(this.model.pageLandscape !== value) {
this.model.pageLandscape = value;
if(this.model.pageSize.width !== this.model.pageSize.height) {
this.model.size = new Size(this.model.pageWidth, this.model.pageHeight);
this.raiseModelSizeChanged(this.model.size.clone());
this.raisePageSizeChanged(this.model.pageSize, this.model.pageLandscape);
}
}
}
changePageColor(value: number): void {
if(this.model.pageColor !== value) {
this.model.pageColor = value;
this.raisePageColorChanged(value);
}
}
updateModelSize() {
const offset = this.getModelSizeUpdateOffset();
if(!offset.isEmpty()) {
const newWidth = Math.max(this.model.size.width + offset.left + offset.right, this.model.pageWidth);
const newHeight = Math.max(this.model.size.height + offset.top + offset.bottom, this.model.pageHeight);
this.model.size = new Size(newWidth, newHeight);
}
}
getModelSizeUpdateOffset(): Offsets {
const oldRectangle = this.model.getRectangle(false);
const newRectangle = this.model.getRectangle(true);
if(!newRectangle.equals(oldRectangle))
this.raiseModelRectangleChanged(newRectangle);
return this.createModelRectangleOffset(newRectangle);
}
private createModelRectangleOffset(rectangle: Rectangle): Offsets {
const pageWidth : number = this.model.pageWidth;
const pageHeight : number = this.model.pageHeight;
const size : Size = this.model.size;
return new Offsets(
-Math.floor(rectangle.x / pageWidth) * pageWidth,
-Math.floor((size.width - rectangle.right) / pageWidth) * pageWidth,
-Math.floor(rectangle.y / pageHeight) * this.model.pageHeight,
-Math.floor((size.height - rectangle.bottom) / pageHeight) * pageHeight
);
}
private raiseModelChanged(changes: ItemChange[]) {
this.onModelChanged.raise1(l => l.notifyModelChanged(changes));
}
private raisePageColorChanged(color: number) {
this.onModelChanged.raise1(l => l.notifyPageColorChanged(color));
}
private raisePageSizeChanged(pageSize: Size, pageLandscape: boolean) {
this.onModelChanged.raise1(l => l.notifyPageSizeChanged(pageSize, pageLandscape));
}
private raiseModelSizeChanged(size: Size, offset?: Offsets) {
this.onModelSizeChanged.raise1(l => l.notifyModelSizeChanged(size, offset));
}
raiseModelRectangleChanged(rectangle: Rectangle): void {
this.onModelSizeChanged.raise1(l => l.notifyModelRectangleChanged(rectangle));
}
private raiseSnapPointChange(point: Point) {
this.onModelSizeChanged.raise1(l => l.notifySnapPointPositionChanged(point));
}
private getInteractingItem(item: DiagramItem, operation?: DiagramModelOperation): DiagramItem {
return this.permissionsProvider.getInteractingItem(item, operation);
}
}
export interface IModelChangesListener {
notifyModelChanged(changes: ItemChange[]);
notifyPageColorChanged(color: number);
notifyPageSizeChanged(pageSize: Size, pageLandscape: boolean);
}
export interface IModelSizeListener {
notifyModelSizeChanged(size: Size, offset: Offsets);
notifyModelRectangleChanged(rectangle: Rectangle);
notifySnapPointPositionChanged(point: Point);
}