UNPKG

@kieler/klighd-core

Version:

Core KLighD diagram visualization with Sprotty

351 lines 16 kB
"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