UNPKG

sprotty

Version:

A next-gen framework for graphical views

260 lines (225 loc) 10.4 kB
/******************************************************************************** * Copyright (c) 2017-2018 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { Point } from 'sprotty-protocol/lib/utils/geometry'; import { Alignable, EdgePlacement, Fadeable, Hoverable, Selectable } from 'sprotty-protocol/lib/model'; import { ModelIndexImpl, SChildElementImpl, SModelElementImpl } from '../base/model/smodel'; import { alignFeature, InternalBoundsAware, boundsFeature, layoutableChildFeature, layoutContainerFeature, ModelLayoutOptions, SShapeElementImpl } from '../features/bounds/model'; import { edgeLayoutFeature } from '../features/edge-layout/model'; import { deletableFeature } from '../features/edit/delete'; import { editFeature } from '../features/edit/model'; import { fadeFeature } from '../features/fade/model'; import { hoverFeedbackFeature, popupFeature } from '../features/hover/model'; import { moveFeature } from '../features/move/model'; import { connectableFeature, SConnectableElementImpl, SRoutableElementImpl } from '../features/routing/model'; import { selectFeature } from '../features/select/model'; import { ViewportRootElementImpl } from '../features/viewport/viewport-root'; import { FluentIterable, FluentIterableImpl } from '../utils/iterable'; /** * Root element for graph-like models. */ export class SGraphImpl extends ViewportRootElementImpl { layoutOptions?: ModelLayoutOptions; constructor(index = new SGraphIndex()) { super(index); } } /** * Model element class for nodes, which are the main entities in a graph. A node can be connected to * another node via an SEdge. Such a connection can be direct, i.e. the node is the source or target of * the edge, or indirect through a port, i.e. it contains an SPort which is the source or target of the edge. */ export class SNodeImpl extends SConnectableElementImpl implements Selectable, Fadeable, Hoverable { static readonly DEFAULT_FEATURES = [connectableFeature, deletableFeature, selectFeature, boundsFeature, moveFeature, layoutContainerFeature, fadeFeature, hoverFeedbackFeature, popupFeature]; override children: SChildElementImpl[]; layout?: string; selected: boolean = false; hoverFeedback: boolean = false; opacity: number = 1; override canConnect(routable: SRoutableElementImpl, role: string) { return this.children.find(c => c instanceof SPortImpl) === undefined; } override get incomingEdges(): FluentIterable<SEdgeImpl> { const index = this.index; if (index instanceof SGraphIndex) { return index.getIncomingEdges(this); } const allEdges = this.index.all().filter(e => e instanceof SEdgeImpl) as FluentIterable<SEdgeImpl>; return allEdges.filter(e => e.targetId === this.id); } override get outgoingEdges(): FluentIterable<SEdgeImpl> { const index = this.index; if (index instanceof SGraphIndex) { return index.getOutgoingEdges(this); } const allEdges = this.index.all().filter(e => e instanceof SEdgeImpl) as FluentIterable<SEdgeImpl>; return allEdges.filter(e => e.sourceId === this.id); } } /** * A port is a connection point for edges. It should always be contained in an SNode. */ export class SPortImpl extends SConnectableElementImpl implements Selectable, Fadeable, Hoverable { static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, boundsFeature, fadeFeature, hoverFeedbackFeature]; selected: boolean = false; hoverFeedback: boolean = false; opacity: number = 1; override get incomingEdges(): FluentIterable<SEdgeImpl> { const index = this.index; if (index instanceof SGraphIndex) { return index.getIncomingEdges(this); } return super.incomingEdges.filter(e => e instanceof SEdgeImpl) as FluentIterable<SEdgeImpl>; } override get outgoingEdges(): FluentIterable<SEdgeImpl> { const index = this.index; if (index instanceof SGraphIndex) { return index.getOutgoingEdges(this); } return super.outgoingEdges.filter(e => e instanceof SEdgeImpl) as FluentIterable<SEdgeImpl>; } } /** * Model element class for edges, which are the connectors in a graph. An edge has a source and a target, * each of which can be either a node or a port. The source and target elements are referenced via their * ids and can be resolved with the index stored in the root element. */ export class SEdgeImpl extends SRoutableElementImpl implements Fadeable, Selectable, Hoverable, InternalBoundsAware { static readonly DEFAULT_FEATURES = [editFeature, deletableFeature, selectFeature, fadeFeature, hoverFeedbackFeature]; selected: boolean = false; hoverFeedback: boolean = false; opacity: number = 1; } /** * A label can be attached to a node, edge, or port, and contains some text to be rendered in its view. */ export class SLabelImpl extends SShapeElementImpl implements Selectable, Alignable, Fadeable { static readonly DEFAULT_FEATURES = [boundsFeature, alignFeature, layoutableChildFeature, edgeLayoutFeature, fadeFeature]; text: string; selected: boolean = false; alignment: Point = Point.ORIGIN; opacity = 1; edgePlacement?: EdgePlacement; } /** * A compartment is used to group multiple child elements such as labels of a node. Usually a `vbox` * or `hbox` layout is used to arrange these children. */ export class SCompartmentImpl extends SShapeElementImpl implements Fadeable { static readonly DEFAULT_FEATURES = [boundsFeature, layoutContainerFeature, layoutableChildFeature, fadeFeature]; override children: SChildElementImpl[]; layout?: string; override layoutOptions?: {[key: string]: string | number | boolean}; opacity = 1; } /** * A specialized model index that tracks outgoing and incoming edges. */ export class SGraphIndex extends ModelIndexImpl { private outgoing: Map<string, SEdgeImpl[]> = new Map; private incoming: Map<string, SEdgeImpl[]> = new Map; override add(element: SModelElementImpl): void { super.add(element); if (element instanceof SEdgeImpl) { // Register the edge in the outgoing map if (element.sourceId) { const sourceArr = this.outgoing.get(element.sourceId); if (sourceArr === undefined) this.outgoing.set(element.sourceId, [element]); else sourceArr.push(element); } // Register the edge in the incoming map if (element.targetId) { const targetArr = this.incoming.get(element.targetId); if (targetArr === undefined) this.incoming.set(element.targetId, [element]); else targetArr.push(element); } } } override remove(element: SModelElementImpl): void { super.remove(element); if (element instanceof SEdgeImpl) { // Remove the edge from the outgoing map const sourceArr = this.outgoing.get(element.sourceId); if (sourceArr !== undefined) { const index = sourceArr.indexOf(element); if (index >= 0) { if (sourceArr.length === 1) this.outgoing.delete(element.sourceId); else sourceArr.splice(index, 1); } } // Remove the edge from the incoming map const targetArr = this.incoming.get(element.targetId); if (targetArr !== undefined) { const index = targetArr.indexOf(element); if (index >= 0) { if (targetArr.length === 1) this.incoming.delete(element.targetId); else targetArr.splice(index, 1); } } } } override getAttachedElements(element: SModelElementImpl): FluentIterable<SModelElementImpl> { return new FluentIterableImpl( () => ({ outgoing: this.outgoing.get(element.id), incoming: this.incoming.get(element.id), nextOutgoingIndex: 0, nextIncomingIndex: 0 }), (state) => { let index = state.nextOutgoingIndex; if (state.outgoing !== undefined && index < state.outgoing.length) { state.nextOutgoingIndex = index + 1; return { done: false, value: state.outgoing[index] }; } index = state.nextIncomingIndex; if (state.incoming !== undefined) { // Filter out self-loops: edges that are both outgoing and incoming while (index < state.incoming.length) { const edge = state.incoming[index]; if (edge.sourceId !== edge.targetId) { state.nextIncomingIndex = index + 1; return { done: false, value: edge }; } index++; } } return { done: true, value: undefined as any }; } ); } getIncomingEdges(element: SConnectableElementImpl): FluentIterable<SEdgeImpl> { return this.incoming.get(element.id) || []; } getOutgoingEdges(element: SConnectableElementImpl): FluentIterable<SEdgeImpl> { return this.outgoing.get(element.id) || []; } }