@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
472 lines (433 loc) • 19.5 kB
text/typescript
/********************************************************************************
* Copyright (c) 2019-2024 EclipseSource 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 {
BoundsAware,
EdgeRouterRegistry,
ElementAndBounds,
ElementAndRoutingPoints,
FluentIterable,
GChildElement,
GModelElement,
GModelElementSchema,
GParentElement,
GRoutableElement,
GRoutingHandle,
Locateable,
ModelIndexImpl,
Point,
RoutedPoint,
Selectable,
TypeGuard,
distinctAdd,
getAbsoluteBounds,
getZoom,
isBoundsAware,
isMoveable,
isSelectable,
isSelected,
remove
} from '@eclipse-glsp/sprotty';
import { ResizeHandleLocation } from '../features/change-bounds/model';
/**
* Helper type to represent a filter predicate for {@link GModelElement}s. This is used to retrieve
* elements from the {@link ModelIndexImpl} that also conform to a second type `T`. Its mainly used for
* retrieving elements that also implement a certain model features (e.g. selectable)
*/
export type ModelFilterPredicate<T> = (modelElement: GModelElement) => modelElement is GModelElement & T;
/**
* Retrieves all elements from the given {@link ModelIndexImpl} that match the given {@link ModelFilterPredicate}
* @param index The {@link ModelIndexImpl}.
* @param predicate The {@link ModelFilterPredicate} that should be used.
* @returns A {@link FluentIterable} of all indexed element that match the predicate
* (correctly casted to also include the additional type of the predicate).
*/
export function filter<T>(index: ModelIndexImpl, predicate: ModelFilterPredicate<T>): FluentIterable<GModelElement & T> {
return index.all().filter(predicate) as FluentIterable<GModelElement & T>;
}
/**
* Retrieves all elements from the given {@link ModelIndexImpl} that match the given {@link ModelFilterPredicate} and executes
* the given runnable for each element of the result set.
* @param index The {@link ModelIndexImpl}.
* @param predicate The {@link ModelFilterPredicate} that should be used.
* @param runnable The runnable that should be executed for each matching element.
*/
export function forEachElement<T>(
index: ModelIndexImpl,
predicate: ModelFilterPredicate<T>,
runnable: (modelElement: GModelElement & T) => void
): void {
filter(index, predicate).forEach(runnable);
}
/**
* Retrieves an array of all elements that match the given {@link ModelFilterPredicate} from the given {@link ModelIndexImpl}.
* @param index The {@link ModelIndexImpl}.
* @param predicate The {@link ModelFilterPredicate} that should be used.
* @returns An array of all indexed element that match the predicate
* (correctly casted to also include the additional type of the predicate).
*/
export function getMatchingElements<T>(index: ModelIndexImpl, predicate: ModelFilterPredicate<T>): (GModelElement & T)[] {
return Array.from(filter(index, predicate));
}
/**
* Invokes the given model index to retrieve the corresponding model elements for the given set of ids. Ids that
* have no corresponding element in the index will be ignored.
* @param index THe model index.
* @param elementsIDs The element ids.
* @param guard Optional typeguard. If defined only elements that match the guard will be returned.
* @returns An array of the model elements that correspond to the given ids and filter predicate.
*/
export function getElements<S extends GModelElement>(index: ModelIndexImpl, elementsIDs: string[], guard?: TypeGuard<S>): S[] {
// Internal filter function that filters out undefined model elements and runs an optional typeguard check.
const filterFn = (element?: GModelElement): element is S => {
if (element !== undefined) {
return guard ? guard(element) : true;
}
return false;
};
return elementsIDs.map(id => index.getById(id)).filter(filterFn);
}
/**
* Retrieves the amount of currently selected elements in the given {@link ModelIndexImpl}.
* @param index The {@link ModelIndexImpl}.
* @returns The amount of selected elements.
*/
export function getSelectedElementCount(index: ModelIndexImpl): number {
let selected = 0;
forEachElement(index, isSelected, element => selected++);
return selected;
}
/**
* Helper function to check wether an any element is selected in the given {@link ModelIndexImpl}.
* @param index The {@link ModelIndexImpl}.
* @returns `true` if at least one element is selected, `false` otherwise.
*/
export function hasSelectedElements(index: ModelIndexImpl): boolean {
return getSelectedElementCount(index) > 0;
}
/**
* Helper function to check wether an element is defined. Can be used as {@link ModelFilterPredicate}.
* @param element The element that should be checked.
* @returns the type predicate for `T`
*/
export function isNotUndefined<T>(element: T | undefined): element is T {
return element !== undefined;
}
/**
* Adds a set of css classes to the given {@link GModelElement}.
* @param element The element to which the css classes should be added.
* @param cssClasses The set of css classes as string array.
*/
export function addCssClasses(element: GModelElement, cssClasses: string[]): void;
export function addCssClasses(element: GModelElement, ...cssClasses: string[]): void;
export function addCssClasses(element: GModelElement, ...cssClasses: string[] | [string[]]): void {
const classes = Array.isArray(cssClasses[0]) ? cssClasses[0] : cssClasses;
const elementCssClasses: string[] = element.cssClasses ?? [];
distinctAdd(elementCssClasses, ...classes);
element.cssClasses = elementCssClasses;
}
/**
* Removes a set of css classes from the given {@link GModelElement}.
* @param element The element from which the css classes should be removed.
* @param cssClasses The set of css classes as string array.
*/
export function removeCssClasses(element: GModelElement, cssClasses: string[]): void;
export function removeCssClasses(element: GModelElement, ...cssClasses: string[]): void;
export function removeCssClasses(element: GModelElement, ...cssClasses: string[] | [string[]]): void {
if (!element.cssClasses || element.cssClasses.length === 0) {
return;
}
const classes = Array.isArray(cssClasses[0]) ? cssClasses[0] : cssClasses;
remove(element.cssClasses, ...classes);
}
/**
* Adds a css classs to a set of {@link GModelElement}s.
*
* @param elements The elements to which the css class should be added.
* @param cssClass The css class to add.
*/
export function addCssClassToElements(elements: GModelElement[], ...cssClasses: string[]): void {
for (const element of elements) {
addCssClasses(element, cssClasses);
}
}
/**
* Removes a css class from a set of {@link GModelElement}s.
* @param elements The elements from which the css class should be removed.
* @param cssClass The css class to remove.
*/
export function removeCssClassOfElements(elements: GModelElement[], ...cssClasses: string[]): void {
for (const element of elements) {
removeCssClasses(element, cssClasses);
}
}
/**
* Toggles a css class on a {@link GModelElement} based on the given toggle flag.
*/
export function toggleCssClass(element: GModelElement, cssClass: string, toggle: boolean): void {
return toggle ? addCssClasses(element, cssClass) : removeCssClasses(element, cssClass);
}
export function isNonRoutableSelectedMovableBoundsAware(element: GModelElement): element is SelectableBoundsAware {
return isNonRoutableSelectedBoundsAware(element) && isMoveable(element);
}
export function isNonRoutableMovableBoundsAware(element: GModelElement): element is BoundsAwareModelElement {
return isNonRoutableBoundsAware(element) && isMoveable(element);
}
/**
* A typeguard function to check wether a given {@link GModelElement} implements the {@link BoundsAware} model feature,
* the {@link Selectable} model feature and is actually selected. In addition, the element must not be a {@link GRoutableElement}.
* @param element The element to check.
* @returns A type predicate indicating wether the element is of type {@link SelectableBoundsAware}.
*/
export function isNonRoutableSelectedBoundsAware(element: GModelElement): element is SelectableBoundsAware {
return isNonRoutableBoundsAware(element) && isSelected(element);
}
/**
* A typeguard function to check wether a given {@link GModelElement} implements the {@link BoundsAware} model feature.
* In addition, the element must not be a {@link GRoutableElement}.
* @param element The element to check.
* @returns A type predicate indicating wether the element is of type {@link BoundsAwareModelElement}.
*/
export function isNonRoutableBoundsAware(element: GModelElement): element is BoundsAwareModelElement {
return isBoundsAware(element) && !isRoutable(element);
}
/**
* A type guard function to check wether a given {@link GModelElement} is a {@link GRoutableElement}.
* @param element The element to check.
* @returns A type predicate indicating wether the element is a {@link GRoutableElement}.
*/
export function isRoutable<T extends GModelElement>(element: T): element is T & GRoutableElement {
return element instanceof GRoutableElement && (element as any).routingPoints !== undefined;
}
/**
* A typeguard function to check wether a given {@link GModelElement} is a {@link SRoutingHandle}.
* @param element The element to check.
* @returns A type predicate indicating wether the element is a {@link SRoutingHandle}
*/
export function isRoutingHandle(element: GModelElement | undefined): element is GRoutingHandle {
return element !== undefined && element instanceof GRoutingHandle;
}
/**
* A typeguard function to check wether a given {@link GModelElement} implements the {@link Selectable} model feature and
* the {@link BoundsAware} model feature.
* @returns A type predicate indicating wether the element is of type {@link SelectableBoundsAware}.
*/
export function isSelectableAndBoundsAware(element: GModelElement): element is SelectableBoundsAware {
return isSelectable(element) && isBoundsAware(element);
}
/**
* Union type to describe {@link GModelElement}s that implement the {@link Selectable} feature.
*/
export type SelectableElement = GModelElement & Selectable;
/**
* Union type to describe {@link GModelElement}s that implement the {@link Selectable} and {@link BoundsAware} feature.
*/
export type SelectableBoundsAware = SelectableElement & BoundsAware;
/**
* Union type to describe {@link GModelElement}s that implement the {@link BoundsAware} feature.
*/
export type BoundsAwareModelElement = GModelElement & BoundsAware;
/**
* Union type to describe {@link GModelElement}s that implement the {@link Locateable} feature.
*/
export type MoveableElement = GModelElement & Locateable;
export interface Resizable extends BoundsAware, Selectable {}
export interface ResizableModelElement extends GParentElement, Resizable {
resizeLocations?: ResizeHandleLocation[];
}
/**
* Helper function to translate a given {@link GModelElement} into its corresponding {@link ElementAndBounds} representation.
* @param element The element to translate.
* @returns The corresponding {@link ElementAndBounds} for the given element.
*/
export function toElementAndBounds(element: BoundsAwareModelElement): ElementAndBounds {
return {
elementId: element.id,
newPosition: {
x: element.bounds.x,
y: element.bounds.y
},
newSize: {
width: element.bounds.width,
height: element.bounds.height
}
};
}
/**
* Helper function to translate a given {@link GRoutableElement} into its corresponding
* {@link ElementAndRoutingPoints} representation.
* @param element The element to translate.
* @returns The corresponding {@link ElementAndRoutingPoints} for the given element.
*/
export function toElementAndRoutingPoints(element: GRoutableElement): ElementAndRoutingPoints {
return {
elementId: element.id,
newRoutingPoints: element.routingPoints
};
}
/** All routing points. */
export const ALL_ROUTING_POINTS = undefined;
/** Pure routing point data kinds. */
export const ROUTING_POINT_KINDS = ['linear', 'bezier-junction'];
/** Pure route data kinds. */
export const ROUTE_KINDS = [...ROUTING_POINT_KINDS, 'source', 'target'];
/**
* Helper function to calculate the {@link ElementAndRoutingPoints} for a given {@link GRoutableElement}.
* If client layout is activated, i.e., the edge routing registry is given and has a router for the element, then the routing
* points from the calculated route are used, otherwise we use the already specified routing points of the {@link GRoutableElement}.
* @param element The element to translate.
* @param routerRegistry the edge router registry.
* @returns The corresponding {@link ElementAndRoutingPoints} for the given element.
*/
export function calcElementAndRoutingPoints(element: GRoutableElement, routerRegistry?: EdgeRouterRegistry): ElementAndRoutingPoints {
const newRoutingPoints = routerRegistry ? calcRoute(element, routerRegistry, ROUTING_POINT_KINDS) : element.routingPoints;
return { elementId: element.id, newRoutingPoints };
}
/**
* Helper function to calculate the route for a given {@link GRoutableElement}.
* If client layout is activated, i.e., the edge routing registry is given and has a router for the element, then the points
* from the calculated route are used, otherwise we use the already specified routing points of the {@link GRoutableElement}.
* @param element The element to translate.
* @param routerRegistry the edge router registry.
* @returns The corresponding route for the given element.
*/
export function calcElementAndRoute(element: GRoutableElement, routerRegistry?: EdgeRouterRegistry): ElementAndRoutingPoints {
let route: Point[] | undefined = routerRegistry ? calcRoute(element, routerRegistry, ROUTE_KINDS) : undefined;
if (!route) {
// add source and target to the routing points
route = [...element.routingPoints];
route.splice(0, 0, element.source?.position || Point.ORIGIN);
route.push(element.target?.position || Point.ORIGIN);
}
return { elementId: element.id, newRoutingPoints: route };
}
/**
* Helper function to calculate the route for a given {@link GRoutableElement} by filtering duplicate points.
* @param element The element to translate.
* @param routerRegistry the edge router registry.
* @param pointKinds the routing point kinds that should be considered.
* @param tolerance the tolerance applied to a point's coordinates to determine duplicates.
* @returns The corresponding route for the given element.
*/
export function calcRoute(
element: GRoutableElement,
routerRegistry: EdgeRouterRegistry,
pointKinds: string[] | undefined = ALL_ROUTING_POINTS,
tolerance = Number.EPSILON
): RoutedPoint[] | undefined {
const route = routerRegistry.get(element.routerKind).route(element);
const calculatedRoute: RoutedPoint[] = [];
for (const point of route) {
// only include points we are actually interested in
if (pointKinds && !pointKinds.includes(point.kind)) {
continue;
}
// check if we are a duplicate based on coordinates in the already calculated route
if (
ROUTING_POINT_KINDS.includes(point.kind) &&
calculatedRoute.find(calculatedPoint => Point.maxDistance(point, calculatedPoint) < tolerance)
) {
continue;
}
calculatedRoute.push(point);
}
return calculatedRoute;
}
/**
* Convenience function to retrieve the model element type from a given input. The input
* can either be a {@link GModelElement}, {@link GModelElementSchema} or a string.
* @param input The type input.
* @returns The corresponding model type as string.
*/
export function getElementTypeId(input: GModelElement | GModelElementSchema | string): string {
if (typeof input === 'string') {
return input;
} else {
return input.type;
}
}
export function findTopLevelElementByFeature<T>(
element: GModelElement,
predicate: (t: GModelElement) => t is GModelElement & T,
skip: (t: GModelElement) => boolean = _t => false
): (GModelElement & T) | undefined {
let match: (GModelElement & T) | undefined;
let current: GModelElement | undefined = element;
while (current !== undefined) {
if (!skip(current) && predicate(current)) {
match = current;
}
if (current instanceof GChildElement) {
current = current.parent;
} else {
current = undefined;
}
}
return match;
}
export function calculateDeltaBetweenPoints(target: Point, source: Point, element: GModelElement): Point {
const delta = Point.subtract(target, source);
const zoom = getZoom(element);
const adaptedDelta = Point.divideScalar(delta, zoom);
return adaptedDelta;
}
export function isVisibleOnCanvas(model: BoundsAwareModelElement): boolean {
const modelBounds = getAbsoluteBounds(model);
const canvasBounds = model.root.canvasBounds;
return (
modelBounds.x <= canvasBounds.width &&
modelBounds.x + modelBounds.width >= 0 &&
modelBounds.y <= canvasBounds.height &&
modelBounds.y + modelBounds.height >= 0
);
}
export function getDescendantIds(element?: GModelElement, skip?: (t: GModelElement) => boolean): string[] {
if (!element || skip?.(element)) {
return [];
}
const parent = element;
const ids = [parent.id];
if (parent instanceof GParentElement) {
ids.push(...parent.children.flatMap(child => getDescendantIds(child, skip)));
}
return ids;
}
/**
* Returns a filter function that checks if the given element is not a descendant of any of the given elements.
*
* @param elements The elements that the element should not be a descendant of.
* @returns the filter function
*/
export function isNotDescendantOfAnyElement<T extends GModelElement>(elements: FluentIterable<T>): (element: T) => boolean {
const elementsSet = new Set<GModelElement>(elements);
return (element: T): boolean => {
let parent: GModelElement = element;
while (parent instanceof GChildElement) {
parent = parent.parent;
if (elementsSet.has(parent)) {
return false;
}
}
return true;
};
}
/**
* Removes any descendants of the given elements from the given elements.
* @param elements The elements to filter.
* @returns the filtered elements
*/
export function removeDescendants<T extends GModelElement>(elements: FluentIterable<T>): FluentIterable<T> {
return elements.filter(isNotDescendantOfAnyElement(elements));
}