@kieler/klighd-core
Version:
Core KLighD diagram visualization with Sprotty
351 lines • 16 kB
JavaScript
"use strict";
/*
* KIELER - Kiel Integrated Environment for Layout Eclipse RichClient
*
* http://rtsys.informatik.uni-kiel.de/kieler
*
* Copyright 2021-2024 by
* + Kiel University
* + Department of Computer Science
* + Real-Time and Embedded Systems Group
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Region = exports.DepthMap = exports.isDetailWithChildren = exports.DetailLevel = void 0;
const constraint_classes_1 = require("@kieler/klighd-interactive/lib/constraint-classes");
const render_options_registry_1 = require("./options/render-options-registry");
const skgraph_models_1 = require("./skgraph-models");
/**
* The possible detail level of a KNode as determined by the DepthMap
*/
var DetailLevel;
(function (DetailLevel) {
DetailLevel[DetailLevel["FullDetails"] = 2] = "FullDetails";
DetailLevel[DetailLevel["MinimalDetails"] = 1] = "MinimalDetails";
DetailLevel[DetailLevel["OutOfBounds"] = 0] = "OutOfBounds";
})(DetailLevel || (exports.DetailLevel = DetailLevel = {}));
/**
* Type predicate to determine whether a DetailLevel is a DetailWithChildren level
*/
function isDetailWithChildren(detail) {
return detail === DetailLevel.FullDetails;
}
exports.isDetailWithChildren = isDetailWithChildren;
/**
* Divides Model KNodes into regions. On these detail level actions
* are defined via the detailLevel. Also holds additional information to determine
* the appropriate detail level, visibility and title for regions.
*/
class DepthMap {
/**
* @param rootElement The root element of the model.
*/
constructor(rootElement) {
this.rootElement = rootElement;
this.rootRegions = [];
this.criticalRegions = new Set();
this.regionIndexMap = new Map();
}
reset(modelRoot) {
this.rootElement = modelRoot;
// rootRegions are reset below as we also want to remove the edges from the graph spanned by the regions
this.criticalRegions.clear();
this.viewport = undefined;
this.lastThreshold = undefined;
this.regionIndexMap.clear();
let currentRegions = this.rootRegions;
this.rootRegions = [];
let remainingRegions = [];
// Go through all regions and clear the references to other Regions and KNodes
while (currentRegions.length !== 0) {
for (const region of currentRegions) {
remainingRegions.concat(region.children);
region.children = [];
region.parent = undefined;
}
currentRegions = remainingRegions;
remainingRegions = [];
}
}
/**
* Returns the current DepthMap instance or undefined if its not initialized
* @returns DepthMap | undefined
*/
static getDM() {
return DepthMap.instance;
}
/**
* Returns the current DepthMap instance or returns a new one.
* @param rootElement The model root element.
*/
static init(rootElement) {
if (!DepthMap.instance) {
// Create new DepthMap, when there is none
DepthMap.instance = new DepthMap(rootElement);
}
else if (DepthMap.instance.rootElement !== rootElement) {
// Reset and reinitialize if the model changed
DepthMap.instance.reset(rootElement);
}
return DepthMap.instance;
}
/**
* It is generally advised to initialize the elements from root to leaf
*
* @param element The KGraphElement to initialize for DepthMap usage
*/
initKGraphElement(element, viewport, renderingOptions) {
var _a, _b;
let entry = this.regionIndexMap.get(element.id);
if (entry) {
// KNode already initialized
return entry;
}
const relativeThreshold = renderingOptions.getValueOrDefault(render_options_registry_1.FullDetailRelativeThreshold);
const scaleThreshold = renderingOptions.getValueOrDefault(render_options_registry_1.FullDetailScaleThreshold);
if (element.parent === element.root && element instanceof constraint_classes_1.KNode) {
const providedRegion = new Region(element);
providedRegion.absolutePosition = element.bounds;
entry = { providingRegion: providedRegion, containingRegion: undefined };
providedRegion.detail = this.computeDetailLevel(providedRegion, viewport, relativeThreshold, scaleThreshold);
this.rootRegions.push(providedRegion);
element.properties.absoluteScale = 1;
element.properties.absoluteX = element.bounds.x;
element.properties.absoluteY = element.bounds.y;
}
else {
// parent should always exist because we're traversing root to leaf
const parentEntry = this.initKGraphElement(element.parent, viewport, renderingOptions);
entry = {
containingRegion: (_a = parentEntry.providingRegion) !== null && _a !== void 0 ? _a : parentEntry.containingRegion,
providingRegion: undefined,
};
const kRendering = this.findRendering(element);
const current = element.parent;
// compute own absolute scale and absolute position based on parent position
const parentAbsoluteScale = element.parent.properties.absoluteScale;
const scaleFactor = (_b = element.parent.properties['org.eclipse.elk.topdown.scaleFactor']) !== null && _b !== void 0 ? _b : 1;
element.properties.absoluteScale = parentAbsoluteScale * scaleFactor;
if (element instanceof constraint_classes_1.KNode) {
element.properties.absoluteX =
current.properties.absoluteX +
element.bounds.x * element.properties.absoluteScale;
element.properties.absoluteY =
current.properties.absoluteY +
element.bounds.y * element.properties.absoluteScale;
}
if (element instanceof constraint_classes_1.KNode &&
kRendering &&
(0, skgraph_models_1.isContainerRendering)(kRendering) &&
kRendering.children.length !== 0) {
entry = { containingRegion: entry.containingRegion, providingRegion: new Region(element) };
entry.providingRegion.detail = this.computeDetailLevel(entry.providingRegion, viewport, relativeThreshold, scaleThreshold);
entry.providingRegion.parent = entry.containingRegion;
entry.containingRegion.children.push(entry.providingRegion);
entry.providingRegion.absolutePosition = {
x: element.properties.absoluteX,
y: element.properties.absoluteY,
};
}
}
this.regionIndexMap.set(element.id, entry);
return entry;
}
/**
* Finds the KRendering in the given graph element.
* @param element The graph element to look up the rendering for.
* @returns The KRendering.
*/
findRendering(element) {
for (const data of element.data) {
if (data !== null && (0, skgraph_models_1.isRendering)(data)) {
return data;
}
}
return undefined;
}
getContainingRegion(element, viewport, renderOptions) {
// initKGraphELement already checks if it is already initialized and if it is returns the existing value
return this.initKGraphElement(element, viewport, renderOptions).containingRegion;
}
getProvidingRegion(node, viewport, renderOptions) {
// initKGraphElement already checks if it is already initialized and if it is returns the existing value
return this.initKGraphElement(node, viewport, renderOptions).providingRegion;
}
/**
* Decides the appropriate detail level for regions based on their size in the viewport and applies that state.
*
* @param viewport The current viewport.
*/
updateDetailLevels(viewport, renderingOptions) {
const relativeThreshold = renderingOptions.getValueOrDefault(render_options_registry_1.FullDetailRelativeThreshold);
const scaleThreshold = renderingOptions.getValueOrDefault(render_options_registry_1.FullDetailScaleThreshold);
this.viewport = { zoom: viewport.zoom, scroll: viewport.scroll };
this.lastThreshold = relativeThreshold;
// Initialize detail level on first run.
if (this.criticalRegions.size === 0) {
for (const region of this.rootRegions) {
const vis = this.computeDetailLevel(region, viewport, relativeThreshold, scaleThreshold);
if (vis === DetailLevel.FullDetails) {
this.updateRegionDetailLevel(region, vis, viewport, relativeThreshold, scaleThreshold);
}
}
}
else {
this.checkCriticalRegions(viewport, relativeThreshold, scaleThreshold);
}
}
/**
* Set detail level for the given region and recursively determine and update the children's detail level
*
* @param region The root region
* @param viewport The current viewport
* @param relativeThreshold The detail level threshold
*/
updateRegionDetailLevel(region, vis, viewport, relativeThreshold, scaleThreshold) {
region.setDetailLevel(vis);
let isCritical = false;
region.children.forEach((childRegion) => {
const childVis = this.computeDetailLevel(childRegion, viewport, relativeThreshold, scaleThreshold);
if (childVis < vis) {
isCritical = true;
}
if (isDetailWithChildren(childVis)) {
this.updateRegionDetailLevel(childRegion, childVis, viewport, relativeThreshold, scaleThreshold);
}
else {
this.recursiveSetOOB(childRegion, childVis);
}
});
if (isCritical) {
this.criticalRegions.add(region);
}
}
recursiveSetOOB(region, vis) {
region.setDetailLevel(vis);
// region is not/no longer the parent of a detail level boundary as such it is not critical
this.criticalRegions.delete(region);
region.children.forEach((childRegion) => {
// bail early when child is less or equally detailed already
if (vis < childRegion.detail) {
this.recursiveSetOOB(childRegion, vis);
}
});
}
/**
* Looks for a change in detail level for all critical regions.
* Applies the level change and manages the critical regions.
*
* @param viewport The current viewport
* @param relativeThreshold The full detail threshold
*/
checkCriticalRegions(viewport, relativeThreshold, scaleThreshold) {
// All regions that are at a detail level boundary (child has lower detail level and parent is at a DetailWithChildren level).
let toBeProcessed = new Set(this.criticalRegions);
// The regions that have become critical and therefore need to be checked as well
let nextToBeProcessed = new Set();
while (toBeProcessed.size !== 0) {
for (const region of toBeProcessed) {
const vis = this.computeDetailLevel(region, viewport, relativeThreshold, scaleThreshold);
region.setDetailLevel(vis);
if (region.parent && vis !== region.parent.detail) {
nextToBeProcessed.add(region.parent);
this.criticalRegions.add(region.parent);
}
if (isDetailWithChildren(vis)) {
this.updateRegionDetailLevel(region, vis, viewport, relativeThreshold, scaleThreshold);
}
else {
this.recursiveSetOOB(region, vis);
}
}
toBeProcessed = nextToBeProcessed;
nextToBeProcessed = new Set();
}
}
/**
* Decides the appropriate detail level for a region
* based on their size in the viewport and visibility
*
* @param region The region in question
* @param viewport The current viewport
* @param relativeThreshold The full detail threshold
* @returns The appropriate detail level
*/
computeDetailLevel(region, viewport, relativeThreshold, scaleThreshold) {
if (!this.isInBounds(region, viewport)) {
return DetailLevel.OutOfBounds;
}
if (!region.parent) {
// Regions without parents should always be full detail if they are visible
return DetailLevel.FullDetails;
}
const viewportSize = this.scaleMeasureInViewport(region.boundingRectangle, viewport);
const scale = viewport.zoom * region.boundingRectangle.properties.absoluteScale;
// change to full detail when relative size threshold is reached or the scaling within the region is big enough to be readable.
if (viewportSize >= relativeThreshold || scale > scaleThreshold) {
return DetailLevel.FullDetails;
}
return DetailLevel.MinimalDetails;
}
/**
* Checks visibility of a region with position from browser coordinates in current viewport.
*
* @param region The region in question for visibility.
* @param viewport The current viewport.
* @returns Boolean value indicating the visibility of the region in the current viewport.
*/
isInBounds(region, viewport) {
if (region.absolutePosition) {
const { canvasBounds } = this.rootElement;
return (region.absolutePosition.x + region.boundingRectangle.bounds.width - viewport.scroll.x >= 0 &&
region.absolutePosition.x - viewport.scroll.x <= canvasBounds.width / viewport.zoom &&
region.absolutePosition.y + region.boundingRectangle.bounds.height - viewport.scroll.y >= 0 &&
region.absolutePosition.y - viewport.scroll.y <= canvasBounds.height / viewport.zoom);
}
// Better to assume it is visible, if information are not sufficient
return true;
}
/**
* Compares the size of a node to the viewport and returns the smallest fraction of either height or width.
*
* @param node The KNode in question
* @param viewport The current viewport
* @returns the relative size of the KNodes shortest dimension
*/
scaleMeasureInViewport(node, viewport) {
const horizontal = node.bounds.width / (node.root.canvasBounds.width / viewport.zoom);
const vertical = node.bounds.height / (node.root.canvasBounds.height / viewport.zoom);
const absoluteScale = node.properties.absoluteScale;
const scaleMeasure = Math.min(horizontal, vertical);
return scaleMeasure * absoluteScale;
}
}
exports.DepthMap = DepthMap;
/**
* Combines KNodes into regions. These correspond to child areas. A region can correspond to
* a region or a super state in the model. Also manages the boundaries, title candidates,
* tree structure of the model and application of detail level of its KNodes.
*/
class Region {
/** Constructor initializes element array for region. */
constructor(boundingRectangle) {
this.boundingRectangle = boundingRectangle;
this.children = [];
this.detail = DetailLevel.FullDetails;
}
/**
* Applies the detail level to all elements of a region.
* @param level the detail level to apply
*/
setDetailLevel(level) {
this.detail = level;
}
}
exports.Region = Region;
//# sourceMappingURL=depth-map.js.map