From 09ab728bbb7bc866dbb06b0543bcf45145c284c8 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 7 Aug 2024 17:54:52 +0200 Subject: [PATCH 01/37] Added first implementations for PointMergeService. --- erdblick_app/app/cesium.ts | 4 + erdblick_app/app/pointmerge.service.ts | 133 +++++++++++++++++++++++++ erdblick_app/app/view.component.ts | 1 + libs/core/src/bindings.cpp | 21 ++-- 4 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 erdblick_app/app/pointmerge.service.ts diff --git a/erdblick_app/app/cesium.ts b/erdblick_app/app/cesium.ts index c649acce..a9e184cc 100644 --- a/erdblick_app/app/cesium.ts +++ b/erdblick_app/app/cesium.ts @@ -50,6 +50,10 @@ export type Viewer = Cesium.Viewer; export const Viewer = Cesium.Viewer; export type PrimitiveCollection = Cesium.PrimitiveCollection; export const PrimitiveCollection = Cesium.PrimitiveCollection; +export type PointPrimitiveCollection = Cesium.PointPrimitiveCollection; +export const PointPrimitiveCollection = Cesium.PointPrimitiveCollection; +export type LabelCollection = Cesium.LabelCollection; +export const LabelCollection = Cesium.LabelCollection; export type BillboardCollection = Cesium.BillboardCollection; export const BillboardCollection = Cesium.BillboardCollection; export type Billboard = Cesium.Billboard; diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts new file mode 100644 index 00000000..3cfac975 --- /dev/null +++ b/erdblick_app/app/pointmerge.service.ts @@ -0,0 +1,133 @@ +import {Injectable} from "@angular/core"; +import {PointPrimitiveCollection, LabelCollection, Viewer} from "./cesium"; +import {coreLib} from "./wasm"; + +type MapLayerStyleRule = string; +type PositionHash = string; +type Cartographic = {x: number, y: number, z: number}; + +/** + * Class which represents a set of merged point features for one location. + * Each merged point feature may be visualized as a label or a point. + * To this end, the visualization retains visualization parameters for + * calls to either/both Cesium PointPrimitiveCollection.add() and/or LabelCollection.add(). + */ +interface MergedPointVisualization { + position: Cartographic, + positionHash: PositionHash, + pointParameters?: Record|null, // Point Visualization Parameters for call to PointPrimitiveCollection.add(). + labelParameters?: Record|null, // Label Visualization Parameters for call to LabelCollection.add(). + featureIds: Array +} + +/** + * Container of MergedPointVisualizations, sitting at the corner point of + * four surrounding tiles. It covers a quarter of the area of each surrounding + * tile. The actual visualization is performed, once all contributions have been gathered. + * Note: A MergedPointsTile is always unique for its NW corner tile ID and its Map-Layer-Style-Rule ID. + */ +export class MergedPointsTile { + quadId: string = "" // NE-NW-SE-SW tile IDs + mapLayerStyleRuleId: MapLayerStyleRule = ""; + + missingTiles: Array = []; + referencingTiles: Array = []; + + pointPrimitives: PointPrimitiveCollection|null = null; + labelPrimitives: LabelCollection|null = null; + + features: Map = new Map; + + count(positionHash: PositionHash) { + return this.features.has(positionHash) ? this.features.get(positionHash)!.featureIds.length : 0; + } + + render(viewer: Viewer) { + if (this.pointPrimitives) { + console.error("MergedPointsTile.render() was called twice."); + } + + this.pointPrimitives = new PointPrimitiveCollection(); + this.labelPrimitives = new LabelCollection(); + + for (let [_, feature] of this.features) { + if (feature.pointParameters) { + this.pointPrimitives.add(feature.pointParameters); + feature.pointParameters = null; + } + if (feature.labelParameters) { + this.labelPrimitives.add(feature.labelParameters); + feature.labelParameters = null; + } + } + + if (this.pointPrimitives.length) { + viewer.scene.primitives.add(this.pointPrimitives) + } + if (this.labelPrimitives.length) { + viewer.scene.primitives.add(this.labelPrimitives) + } + } + + remove(viewer: Viewer) { + if (this.pointPrimitives && this.pointPrimitives.length) { + viewer.scene.primitives.add(this.pointPrimitives) + } + if (this.labelPrimitives && this.labelPrimitives.length) { + viewer.scene.primitives.add(this.labelPrimitives) + } + } +} + +/** + * Service which manages the CRUD cycle of MergedPointsTiles. + */ +@Injectable({providedIn: 'root'}) +export class PointMergeService +{ + mergedPointsTiles: Map> = new Map>(); + + /** + * Check if the corner tile at geoPos is interested in contributions from `tileId`. + * Returns true if respective corner has sourceTileId in is in missingTiles. + */ + wants(geoPos: Cartographic, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): boolean { + return this.get(geoPos, coreLib.getTileLevel(sourceTileId), mapLayerStyleRuleId).missingTiles.findIndex(v => v == sourceTileId) != -1; + } + + /** + * Count how many points have been merged for the given position and style rule so far. + */ + count(geoPos: Cartographic, hashPos: PositionHash, level: number, mapLayerStyleRuleId: MapLayerStyleRule): number { + return this.get(geoPos, level, mapLayerStyleRuleId).count(hashPos); + } + + /** + * Get or create a MergedPointsTile for a particular cartographic location. + * Calculates the tile ID of the given location. If the position + * is north if the tile center, the tile IDs y component is decremented (unless it is already 0). + * If the position is west of the tile center, the tile IDs x component is decremented (unless it is already 0). + */ + get(geoPos: Cartographic, level: number, mapLayerStyleRuleId: string): MergedPointsTile { + // TODO + } + + /** + * Insert (or update) a bunch of point visualizations. They will be dispatched into the + * MergedPointsTiles surrounding sourceTileId. Afterward, the sourceTileId is removed from + * the missingTiles of each. MergedPointsTiles with empty referencingTiles (requiring render) + * are yielded. The sourceTileId is also added to the MergedPointsTiles referencingTiles set. + */ + *insert(points: Array, sourceTileId: number, mapLayerStyleRuleId: MapLayerStyleRule): Iterator { + // TODO + } + + /** + * Remove a sourceTileId reference from each surrounding corner tile whose mapLayerStyleRuleId has a + * prefix-match with the mapLayerStyleId. Yields MergedPointsTiles which now have empty referencingTiles, + * and whose visualization (if existing) must therefore be removed from the scene. + */ + *remove(sourceTileId: number, mapLayerStyleId: string): Iterator { + // TODO + } +} diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index 67bff2ac..f4393574 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -59,6 +59,7 @@ export class ErdblickViewComponent implements AfterViewInit { private tileVisForPrimitive: Map; private openStreetMapLayer: ImageryLayer | null = null; private marker: Entity | null = null; + /** * Construct a Cesium View with a Model. * @param mapService The map model service providing access to data diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index 917ffa43..6a0055d5 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -105,10 +105,15 @@ double getTilePriorityById(Viewport const& vp, uint64_t tileId) { /** Get the center position for a mapget tile id in WGS84. */ mapget::Point getTilePosition(uint64_t tileIdValue) { - mapget::TileId tid(tileIdValue); - return tid.center(); + return mapget::TileId(tileIdValue).center(); +} + +/** Get the level for a mapget tile id. */ +uint16_t getTileLevel(uint64_t tileIdValue) { + return mapget::TileId(tileIdValue).z(); } +/** Get the tile ID for the given level and position. */ uint64_t getTileIdFromPosition(double longitude, double latitude, uint16_t level) { return mapget::TileId::fromWgs84(longitude, latitude, level).value_; } @@ -124,10 +129,13 @@ em::val getTileBox(uint64_t tileIdValue) { }); } -/** Get the neighbor for a mapget tile id. */ +/** + * Get the neighbor for a mapget tile id. Tile row will be clamped to [0, maxForLevel], + * so a positive/negative wraparound is not possible. The tile id column will wrap at the + * antimeridian. + */ uint64_t getTileNeighbor(uint64_t tileIdValue, int32_t offsetX, int32_t offsetY) { - mapget::TileId tid(tileIdValue); - return mapget::TileId(tid.x() + offsetX, tid.y() + offsetY, tid.z()).value_; + return mapget::TileId(tileIdValue).neighbor(offsetX, offsetY).value_; } /** Get the full string key of a map tile feature layer. */ @@ -422,9 +430,8 @@ EMSCRIPTEN_BINDINGS(erdblick) em::function("getTilePriorityById", &getTilePriorityById); em::function("getTilePosition", &getTilePosition); em::function("getTileIdFromPosition", &getTileIdFromPosition); - - ////////// Return coordinates for a rectangle representing the bounding box of the tile em::function("getTileBox", &getTileBox); + em::function("getTileLevel", &getTileLevel); ////////// Get/Parse full id of a TileFeatureLayer em::function("getTileFeatureLayerKey", &getTileFeatureLayerKey); From 34c36ae0fb46d96ed6e5ca9a68a3860c0fc9b7c0 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 14 Aug 2024 10:41:35 +0200 Subject: [PATCH 02/37] Continued implementation of PointMergeService. --- erdblick_app/app/pointmerge.service.ts | 110 +++++++++++++++++++-- erdblick_app/app/visualization.model.ts | 16 ++- libs/core/include/erdblick/visualization.h | 4 +- libs/core/src/visualization.cpp | 1 + 4 files changed, 120 insertions(+), 11 deletions(-) diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts index 3cfac975..aa09acdf 100644 --- a/erdblick_app/app/pointmerge.service.ts +++ b/erdblick_app/app/pointmerge.service.ts @@ -27,7 +27,7 @@ interface MergedPointVisualization { * Note: A MergedPointsTile is always unique for its NW corner tile ID and its Map-Layer-Style-Rule ID. */ export class MergedPointsTile { - quadId: string = "" // NE-NW-SE-SW tile IDs + tileId: bigint = 0n; // NW tile ID mapLayerStyleRuleId: MapLayerStyleRule = ""; missingTiles: Array = []; @@ -38,6 +38,20 @@ export class MergedPointsTile { features: Map = new Map; + add(point: MergedPointVisualization) { + let existingPoint = this.features.get(point.positionHash); + if (!existingPoint) { + this.features.set(point.positionHash, point); + } + else { + for (let fid in point.featureIds) { + existingPoint.featureIds.push(fid); + } + existingPoint.pointParameters = point.pointParameters; + existingPoint.labelParameters = point.labelParameters; + } + } + count(positionHash: PositionHash) { return this.features.has(positionHash) ? this.features.get(positionHash)!.featureIds.length : 0; } @@ -85,11 +99,12 @@ export class MergedPointsTile { @Injectable({providedIn: 'root'}) export class PointMergeService { - mergedPointsTiles: Map> = new Map>(); + mergedPointsTiles: Map> = new Map>(); /** * Check if the corner tile at geoPos is interested in contributions from `tileId`. - * Returns true if respective corner has sourceTileId in is in missingTiles. + * Returns true if respective corner has sourceTileId in its in missingTiles. + * __Note: This is called from WASM.__ */ wants(geoPos: Cartographic, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): boolean { return this.get(geoPos, coreLib.getTileLevel(sourceTileId), mapLayerStyleRuleId).missingTiles.findIndex(v => v == sourceTileId) != -1; @@ -108,8 +123,40 @@ export class PointMergeService * is north if the tile center, the tile IDs y component is decremented (unless it is already 0). * If the position is west of the tile center, the tile IDs x component is decremented (unless it is already 0). */ - get(geoPos: Cartographic, level: number, mapLayerStyleRuleId: string): MergedPointsTile { - // TODO + get(geoPos: Cartographic, level: number, mapLayerStyleRuleId: MapLayerStyleRule): MergedPointsTile { + // Calculate the correct corner tile ID. + let tileId = coreLib.getTileIdFromPosition(geoPos.x, geoPos.y, level); + let tilePos = coreLib.getTilePosition(tileId); + let offsetX = 0; + let offsetY = 0; + if (geoPos.x < tilePos.x) + offsetX = -1; + if (geoPos.y > tilePos.y) + offsetY = -1; + tileId = coreLib.getTileNeighbor(tileId, offsetX, offsetY); + + // Get or create the tile-map for the mapLayerStyleRuleId. + let styleRuleMap = this.mergedPointsTiles.get(mapLayerStyleRuleId); + if (!styleRuleMap) { + styleRuleMap = new Map(); + this.mergedPointsTiles.set(mapLayerStyleRuleId, styleRuleMap); + } + + // Get or create the entry for the tile in the map. + let result = styleRuleMap.get(tileId); + if (!result) { + result = new MergedPointsTile(); + result.tileId = tileId; + result.mapLayerStyleRuleId = mapLayerStyleRuleId; + result.missingTiles = [ + tileId, + coreLib.getTileNeighbor(tileId, 1, 0), + coreLib.getTileNeighbor(tileId, 0, 1), + coreLib.getTileNeighbor(tileId, 1, 1), + ] + styleRuleMap.set(tileId, result); + } + return result; } /** @@ -118,8 +165,42 @@ export class PointMergeService * the missingTiles of each. MergedPointsTiles with empty referencingTiles (requiring render) * are yielded. The sourceTileId is also added to the MergedPointsTiles referencingTiles set. */ - *insert(points: Array, sourceTileId: number, mapLayerStyleRuleId: MapLayerStyleRule): Iterator { - // TODO + *insert(points: Array, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): Iterator { + // Insert the points into the relevant corner tiles. + let level = coreLib.getTileLevel(sourceTileId); + for (let point of points) { + let mergedPointsTile = this.get(point.position, level, mapLayerStyleRuleId); + mergedPointsTile.add(point); + } + + // Remove the sourceTileId from the corner tile IDs. + let cornerTileIds = [ + sourceTileId, + coreLib.getTileNeighbor(sourceTileId, -1, 0), + coreLib.getTileNeighbor(sourceTileId, 0, -1), + coreLib.getTileNeighbor(sourceTileId, -1, -1), + ]; + let styleRuleMap = this.mergedPointsTiles.get(mapLayerStyleRuleId)!; + for (let cornerTileId of cornerTileIds) { + let cornerTile = styleRuleMap.get(cornerTileId); + if (cornerTile) { + let newMissingTiles = cornerTile.missingTiles.filter(val => val != sourceTileId); + + // Add the source tile ID to the referencing tiles, + // if it was removed from the missing tiles. This can only happen once. + // This way, we are prepared for the idea that a style sheet might + // re-insert some data. + if (newMissingTiles.length != cornerTile.missingTiles.length) { + cornerTile.referencingTiles.push(sourceTileId); + cornerTile.missingTiles = newMissingTiles; + } + + // Yield the corner tile as to-be-rendered, if it does not have any missing tiles. + if (!cornerTile.missingTiles.length) { + yield cornerTile; + } + } + } } /** @@ -127,7 +208,18 @@ export class PointMergeService * prefix-match with the mapLayerStyleId. Yields MergedPointsTiles which now have empty referencingTiles, * and whose visualization (if existing) must therefore be removed from the scene. */ - *remove(sourceTileId: number, mapLayerStyleId: string): Iterator { - // TODO + *remove(sourceTileId: bigint, mapLayerStyleId: string): Iterator { + for (let [mapLayerStyleRuleId, tiles] of this.mergedPointsTiles.entries()) { + if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { + for (let [tileId, tile] of tiles) { + // Yield the corner tile as to-be-rendered, if it does not have any referencing tiles. + tile.referencingTiles = tile.referencingTiles.filter(val => val == sourceTileId); + if (!tile.referencingTiles.length) { + yield tile; + tiles.delete(tileId); + } + } + } + } } } diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index eb08f02b..ac7b0304 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -10,6 +10,7 @@ import { HeightReference } from "./cesium"; import {FeatureLayerStyle, TileFeatureLayer} from "../../build/libs/core/erdblick-core"; +import {PointMergeService} from "./pointmerge.service"; export interface LocateResolution { tileId: string, @@ -98,10 +99,12 @@ export class TileVisualization { private deleted: boolean = false; private readonly auxTileFun: (key: string)=>FeatureTile|null; private readonly options: Record; + private readonly pointMergeService: PointMergeService; /** * Create a tile visualization. * @param tile {FeatureTile} The tile to visualize. + * @param pointMergeService Instance of the central PointMergeService, used to visualize merged point features. * @param auxTileFun Callback which may be called to resolve external references * for relation visualization. * @param style The style to use for visualization. @@ -115,7 +118,16 @@ export class TileVisualization { * @param boxGrid Sets a flag to wrap this tile visualization into a bounding box * @param options Option values for option variables defined by the style sheet. */ - constructor(tile: FeatureTile, auxTileFun: (key: string)=>FeatureTile|null, style: FeatureLayerStyle, highDetail: boolean, highlight: string = "", boxGrid?: boolean, options?: Record) { + constructor( + tile: FeatureTile, + pointMergeService: PointMergeService, + auxTileFun: (key: string) => FeatureTile | null, + style: FeatureLayerStyle, + highDetail: boolean, + highlight: string = "", + boxGrid?: boolean, + options?: Record) + { this.tile = tile; this.style = style as StyleWithIsDeleted; this.isHighDetail = highDetail; @@ -125,6 +137,7 @@ export class TileVisualization { this.auxTileFun = auxTileFun; this.showTileBorder = boxGrid === undefined ? false : boxGrid; this.options = options || {}; + this.pointMergeService = pointMergeService; } /** @@ -153,6 +166,7 @@ export class TileVisualization { let visualization = new coreLib.FeatureLayerVisualization( this.style, this.options, + this.pointMergeService, this.highlight!); visualization.addTileFeatureLayer(tileFeatureLayer); try { diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index 5cf01d9c..f593a15d 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -80,7 +80,8 @@ class FeatureLayerVisualization */ FeatureLayerVisualization( const FeatureLayerStyle& style, - NativeJsValue const& optionValues, + NativeJsValue const& rawoptionValues, + NativeJsValue const& rawFeatureMergeService, std::string highlightFeatureIndex = ""); /** @@ -231,6 +232,7 @@ class FeatureLayerVisualization CesiumPrimitive coloredGroundMeshes_; CesiumPointPrimitiveCollection coloredPoints_; CesiumPrimitiveLabelsCollection labelCollection_; + JsValue featureMergeService_; FeatureLayerStyle const& style_; mapget::TileFeatureLayer::Ptr tile_; diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index a4e1d1be..b509940c 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -21,6 +21,7 @@ uint32_t fvec4ToInt(glm::fvec4 const& v) { FeatureLayerVisualization::FeatureLayerVisualization( const FeatureLayerStyle& style, NativeJsValue const& rawOptionValues, + NativeJsValue const& rawFeatureMergeService, std::string highlightFeatureId) : coloredLines_(CesiumPrimitive::withPolylineColorAppearance(false)), coloredNontrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(false, false)), From 7b9c407316d782e9eec5f09cf053849d19ac5344 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 14 Aug 2024 15:39:27 +0200 Subject: [PATCH 03/37] Add pointMergeGridCellSize to style. --- libs/core/include/erdblick/rule.h | 2 ++ libs/core/include/erdblick/visualization.h | 2 +- libs/core/src/rule.cpp | 23 ++++++++++++++++------ libs/core/src/visualization.cpp | 3 ++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index a02fea36..82a44995 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -58,6 +58,7 @@ class FeatureStyleRule [[nodiscard]] float outlineWidth() const; [[nodiscard]] std::optional> const& nearFarScale() const; [[nodiscard]] glm::dvec3 const& offset() const; + [[nodiscard]] std::optional const& pointMergeGridCellSize() const; [[nodiscard]] std::optional const& relationType() const; [[nodiscard]] float relationLineHeightOffset() const; @@ -119,6 +120,7 @@ class FeatureStyleRule float outlineWidth_ = .0; std::optional> nearFarScale_; glm::dvec3 offset_{.0, .0, .0}; + std::optional pointMergeGridCellSize_; // Labels' rules std::string labelFont_ = "24px Helvetica"; diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index f593a15d..bf122573 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -80,7 +80,7 @@ class FeatureLayerVisualization */ FeatureLayerVisualization( const FeatureLayerStyle& style, - NativeJsValue const& rawoptionValues, + NativeJsValue const& rawOptionValues, NativeJsValue const& rawFeatureMergeService, std::string highlightFeatureIndex = ""); diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index 6113594a..23630451 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -12,13 +12,13 @@ std::optional parseArrowMode(std::string const& arrowSt if (arrowStr == "none") { return FeatureStyleRule::NoArrow; } - else if (arrowStr == "forward") { + if (arrowStr == "forward") { return FeatureStyleRule::ForwardArrow; } - else if (arrowStr == "backward") { + if (arrowStr == "backward") { return FeatureStyleRule::BackwardArrow; } - else if (arrowStr == "double") { + if (arrowStr == "double") { return FeatureStyleRule::DoubleArrow; } @@ -30,13 +30,13 @@ std::optional parseGeometryEnum(std::string const& enumStr) { if (enumStr == "point") { return mapget::GeomType::Points; } - else if (enumStr == "mesh") { + if (enumStr == "mesh") { return mapget::GeomType::Mesh; } - else if (enumStr == "line") { + if (enumStr == "line") { return mapget::GeomType::Line; } - else if (enumStr == "polygon") { + if (enumStr == "polygon") { return mapget::GeomType::Polygon; } @@ -168,6 +168,12 @@ void FeatureStyleRule::parse(const YAML::Node& yaml) offset_.y = yaml["offset"][1].as(); offset_.z = yaml["offset"][2].as(); } + if (yaml["point-merge-grid-cell"].IsDefined() && yaml["point-merge-grid-cell"].size() >= 3) { + pointMergeGridCellSize_ = glm::dvec3(); + pointMergeGridCellSize_->x = yaml["point-merge-grid-cell"][0].as(); + pointMergeGridCellSize_->y = yaml["point-merge-grid-cell"][1].as(); + pointMergeGridCellSize_->z = yaml["point-merge-grid-cell"][2].as(); + } ///////////////////////////////////// /// Line Style Fields @@ -650,6 +656,11 @@ glm::dvec3 const& FeatureStyleRule::offset() const return offset_; } +std::optional const& FeatureStyleRule::pointMergeGridCellSize() const +{ + return pointMergeGridCellSize_; +} + std::optional const& FeatureStyleRule::attributeType() const { return attributeType_; diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index b509940c..6bc34870 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -30,7 +30,8 @@ FeatureLayerVisualization::FeatureLayerVisualization( coloredGroundMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(true, true)), style_(style), highlightFeatureId_(std::move(highlightFeatureId)), - externalRelationReferences_(JsValue::List()) + externalRelationReferences_(JsValue::List()), + featureMergeService_(rawFeatureMergeService) { // Convert option values dict to simfil values. auto optionValues = JsValue(rawOptionValues); From fe06332cd4e13137f056e88d7ba782c2cb18e349 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Fri, 16 Aug 2024 18:50:20 +0200 Subject: [PATCH 04/37] Refactor feature primitive IDs, add hover highlights. --- README.md | 2 +- erdblick_app/app/feature.panel.component.ts | 2 +- erdblick_app/app/feature.search.component.ts | 2 +- erdblick_app/app/features.model.ts | 27 ++-- erdblick_app/app/jump.service.ts | 2 +- erdblick_app/app/map.service.ts | 129 +++++++++++++----- erdblick_app/app/view.component.ts | 105 +++----------- erdblick_app/app/visualization.model.ts | 23 ++-- .../erdblick/cesium-interface/labels.h | 2 +- .../erdblick/cesium-interface/points.h | 2 +- .../erdblick/cesium-interface/primitive.h | 8 +- libs/core/include/erdblick/parser.h | 1 + libs/core/include/erdblick/rule.h | 11 +- libs/core/include/erdblick/visualization.h | 17 +-- libs/core/src/bindings.cpp | 23 ++-- libs/core/src/cesium-interface/labels.cpp | 2 +- libs/core/src/cesium-interface/points.cpp | 2 +- libs/core/src/cesium-interface/primitive.cpp | 8 +- libs/core/src/rule.cpp | 13 +- libs/core/src/visualization.cpp | 41 +++--- 20 files changed, 216 insertions(+), 206 deletions(-) diff --git a/README.md b/README.md index ade0dee5..6fae4dd3 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Each rule within the YAML `rules` array can have the following fields: |-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|--------------------------------------| | `geometry` | List of geometry type(s) or single type the rule applies to. | At least one of `"point"`,`"mesh"`, `"line"`, `"polygon"`. | `["point", "mesh"]`, `line` | | `aspect` | Specifies the aspect to which the rule applies: `"feature"`, `"relation"`, or `"attribute"`. | String | `"feature"`, `"relation"` | -| `mode` | Specifies the mode: `"normal"` or `"highlight"`. | String | `"normal"`, `"highlight"` | +| `mode` | Specifies the highlight mode: `"none"` or `"hover"` or `"selection"`. | String | `"none"`, `"hover"` | | `type` | A regular expression to match against a feature type. | String | `"Lane\|Boundary"` | | `filter` | A [simfil](https://github.com/klebert-engineering/simfil) filter expression over the feature's JSON representation. | String | `*roadClass == 4` | | `selectable` | Indicates if the feature is selectable. | Boolean | `true`, `false` | diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index eb20623b..08bdf54a 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -365,7 +365,7 @@ export class FeaturePanelComponent implements OnInit { } if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { - this.jumpService.highlightFeature(this.inspectionService.selectedMapIdName, rowData["value"]).then(); + this.jumpService.selectFeature(this.inspectionService.selectedMapIdName, rowData["value"]).then(); } this.copyToClipboard(rowData["value"]); } diff --git a/erdblick_app/app/feature.search.component.ts b/erdblick_app/app/feature.search.component.ts index 58592de5..136f54be 100644 --- a/erdblick_app/app/feature.search.component.ts +++ b/erdblick_app/app/feature.search.component.ts @@ -107,7 +107,7 @@ export class FeatureSearchComponent { selectResult(event: any) { if (event.value.mapId && event.value.featureId) { - this.jumpService.highlightFeature(event.value.mapId, event.value.featureId).then(() => { + this.jumpService.selectFeature(event.value.mapId, event.value.featureId).then(() => { if (this.inspectionService.selectedFeature) { this.mapService.focusOnFeature(this.inspectionService.selectedFeature); } diff --git a/erdblick_app/app/features.model.ts b/erdblick_app/app/features.model.ts index f3cb9c18..7f50ea8c 100644 --- a/erdblick_app/app/features.model.ts +++ b/erdblick_app/app/features.model.ts @@ -133,25 +133,31 @@ export class FeatureTile { level() { return Number(this.tileId & BigInt(0xffff)); } + + has(featureId: string) { + return this.peek((tileFeatureLayer: TileFeatureLayer) => { + return tileFeatureLayer.find(featureId) !== null; + }); + } } /** - * Wrapper which combines a FeatureTile and the index of - * a feature within the tileset. Using the peek-function, it is - * possible to access the WASM feature view in a memory-safe way. + * Wrapper which combines a FeatureTile and feature id. + * Using the peek-function, it is possible to access the + * WASM feature view in a memory-safe way. */ export class FeatureWrapper { - public readonly index: number; + public readonly featureId: string; public featureTile: FeatureTile; /** * Construct a feature wrapper from a featureTile and a feature index * within that tile. - * @param index The index of the feature within the tile. + * @param featureId The feature-id of the feature. * @param featureTile {FeatureTile} The feature tile container. */ - constructor(index: number, featureTile: FeatureTile) { - this.index = index; + constructor(featureId: string, featureTile: FeatureTile) { + this.featureId = featureId; this.featureTile = featureTile; } @@ -165,7 +171,10 @@ export class FeatureWrapper { throw new Error(`Unable to access feature of deleted layer ${this.featureTile.id}!`); } return this.featureTile.peek((tileFeatureLayer: TileFeatureLayer) => { - let feature = tileFeatureLayer.at(this.index); + let feature = tileFeatureLayer.find(this.featureId); + if (feature.isNull()) { + return null; + } let result = null; if (callback) { result = callback(feature); @@ -179,6 +188,6 @@ export class FeatureWrapper { if (!other) { return false; } - return this.featureTile.id == other.featureTile.id && this.index == other.index; + return this.featureTile.id == other.featureTile.id && this.featureId == other.featureId; } } diff --git a/erdblick_app/app/jump.service.ts b/erdblick_app/app/jump.service.ts index d739b9bc..c29d325d 100644 --- a/erdblick_app/app/jump.service.ts +++ b/erdblick_app/app/jump.service.ts @@ -135,7 +135,7 @@ export class JumpTargetService { ]); } - async highlightFeature(mapId: string, featureId: string) { + async selectFeature(mapId: string, featureId: string) { let featureJumpTargets = this.mapService.tileParser?.filterFeatureJumpTargets(featureId) as Array; const validIndex = featureJumpTargets.findIndex(action => !action.error); if (validIndex == -1) { diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index 81610240..76901d51 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -5,17 +5,29 @@ import {coreLib, uint8ArrayToWasm} from "./wasm"; import {TileVisualization} from "./visualization.model"; import {BehaviorSubject, Subject} from "rxjs"; import {ErdblickStyle, StyleService} from "./style.service"; -import {FeatureLayerStyle, TileLayerParser, Feature} from '../../build/libs/core/erdblick-core'; +import {FeatureLayerStyle, TileLayerParser, Feature, HighlightMode} from '../../build/libs/core/erdblick-core'; import {ParametersService} from "./parameters.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; import {InfoMessageService} from "./info.service"; import {MAX_ZOOM_LEVEL} from "./feature.search.service"; +import {PointMergeService} from "./pointmerge.service"; +/** + * Combination of a tile id and a feature id, which may be resolved + * to a feature object. + */ +interface TileFeatureId { + featureId: string, + mapTileKey: string, +} + +/** Expected structure of a LayerInfoItem's coverage entry. */ export interface CoverageRectItem extends Object { min: number, max: number } +/** Expected structure of a list entry in the MapInfoItem's layer entry. */ export interface LayerInfoItem extends Object { canRead: boolean; canWrite: boolean; @@ -30,6 +42,7 @@ export interface LayerInfoItem extends Object { tileBorders: boolean; } +/** Expected structure of a list entry in the /sources endpoint. */ export interface MapInfoItem extends Object { extraJsonAttachment: Object; layers: Map; @@ -44,6 +57,7 @@ export interface MapInfoItem extends Object { const infoUrl = "/sources"; const tileUrl = "/tiles"; +/** Redefinition of coreLib.Viewport. TODO: Check if needed. */ type ViewportProperties = { orientation: number; camPosLon: number; @@ -78,13 +92,14 @@ export class MapService { private tileStreamParsingQueue: any[]; private tileVisualizationQueue: [string, TileVisualization][]; private selectionVisualizations: TileVisualization[]; + private hoverVisualizations: TileVisualization[]; tileParser: TileLayerParser|null = null; tileVisualizationTopic: Subject; tileVisualizationDestructionTopic: Subject; moveToWgs84PositionTopic: Subject<{x: number, y: number, z?: number}>; - allViewportTileIds: Map = new Map(); - selectionTopic: BehaviorSubject = new BehaviorSubject(null); + selectionTopic: BehaviorSubject> = new BehaviorSubject>([]); + hoverTopic: BehaviorSubject> = new BehaviorSubject>([]); selectionTileRequest: { remoteRequest: { mapId: string, @@ -100,7 +115,9 @@ export class MapService { constructor(public styleService: StyleService, public parameterService: ParametersService, private sidePanelService: SidePanelService, - private messageService: InfoMessageService) { + private messageService: InfoMessageService, + private pointMergeService: PointMergeService) + { this.loadedTileLayers = new Map(); this.visualizedTileLayers = new Map(); this.currentFetch = null; @@ -118,6 +135,7 @@ export class MapService { this.tileStreamParsingQueue = []; this.tileVisualizationQueue = []; this.selectionVisualizations = []; + this.hoverVisualizations = []; // Triggered when a tile layer is freshly rendered and should be added to the frontend. this.tileVisualizationTopic = new Subject(); // {FeatureTile} @@ -164,29 +182,11 @@ export class MapService { await this.reloadDataSources(); - this.selectionTopic.subscribe(selectedFeatureWrapper => { - this.selectionVisualizations.forEach(visu => this.tileVisualizationDestructionTopic.next(visu)); - this.selectionVisualizations = []; - - if (this.sidePanelService.panel != SidePanelState.FEATURESEARCH) { - this.sidePanelService.panel = SidePanelState.NONE; - } - if (!selectedFeatureWrapper) - return; - - // Apply additional highlight styles. - for (let [_, styleData] of this.styleService.styles) { - if (styleData.featureLayerStyle && styleData.params.visible) { - let visu = new TileVisualization( - selectedFeatureWrapper!.featureTile, - (tileKey: string)=>this.getFeatureTile(tileKey), - styleData.featureLayerStyle, - true, - selectedFeatureWrapper.peek((f: Feature) => f.id())); - this.tileVisualizationTopic.next(visu); - this.selectionVisualizations.push(visu); - } - } + this.selectionTopic.subscribe(selectedFeatureWrappers => { + this.visualizeHighlights(coreLib.HighlightMode.SELECTION_HIGHLIGHT, selectedFeatureWrappers); + }); + this.hoverTopic.subscribe(hoveredFeatureWrappers => { + this.visualizeHighlights(coreLib.HighlightMode.HOVER_HIGHLIGHT, hoveredFeatureWrappers); }); } @@ -591,10 +591,12 @@ export class MapService { const layerName = tileLayer.layerName; let visu = new TileVisualization( tileLayer, + this.pointMergeService, (tileKey: string)=>this.getFeatureTile(tileKey), wasmStyle, tileLayer.preventCulling || this.currentHighDetailTileIds.has(tileLayer.tileId), - "", + coreLib.HighlightMode.NO_HIGHLIGHT, + [], this.getMapLayerBorderState(mapName, layerName), (style as ErdblickStyle).params !== undefined ? (style as ErdblickStyle).params.options : {}); this.tileVisualizationQueue.push([styleId, visu]); @@ -651,18 +653,20 @@ export class MapService { } async selectFeature(tileKey: string, typeId: string, idParts: Array, focus: boolean=false) { - let tile = await this.loadTileForSelection(tileKey); - let feature = new FeatureWrapper( - tile.peek(layer => layer.findFeatureIndex(typeId, idParts)), - tile); - if (feature.index < 0) { - let [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); + const tile = await this.loadTileForSelection(tileKey); + // TODO: Doing the stringification here sucks a bit. + const featureId = `${typeId}.${idParts.filter((_, index) => index % 2 === 0).join('.')}`; + + // Ensure that the feature really exists in the tile. + if (!tile.has(featureId)) { + const [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); this.messageService.showError( - `The feature ${typeId+idParts.map((val, n)=>((n%2)==1?val:".")).join("")}`+ - `does not exist in the ${layerId} layer of tile ${tileId} of map ${mapId}.`); + `The feature ${featureId} does not exist in the ${layerId} layer of tile ${tileId} of map ${mapId}.`); return; } - this.selectionTopic.next(feature); + + const feature = new FeatureWrapper(featureId, tile); + this.selectionTopic.next([feature]); if (focus) { this.focusOnFeature(feature); } @@ -682,4 +686,55 @@ export class MapService { } this.zoomLevel.next(MAX_ZOOM_LEVEL); } + + resolveFeature(id: TileFeatureId) { + const tile = this.loadedTileLayers.get(id.mapTileKey); + if (!tile) { + return null; + } + return new FeatureWrapper(id.featureId, tile); + } + + private visualizeHighlights(mode: HighlightMode, featureWrappers: Array) { + let visualizationCollection = null; + switch (mode) { + case coreLib.HighlightMode.SELECTION_HIGHLIGHT: + if (this.sidePanelService.panel != SidePanelState.FEATURESEARCH) { + this.sidePanelService.panel = SidePanelState.NONE; + } + visualizationCollection = this.selectionVisualizations; + break; + case coreLib.HighlightMode.HOVER_HIGHLIGHT: + visualizationCollection = this.hoverVisualizations; break; + default: + console.error(`Bad visualization mode ${mode}!`); + return; + } + + if (!featureWrappers.length) { + return; + } + + while (visualizationCollection.length) { + this.tileVisualizationDestructionTopic.next(visualizationCollection.pop()); + } + + // Apply additional highlight styles. + const featureTile = featureWrappers[0].featureTile; + const featureIds = featureWrappers.map(fw => fw.featureId); + for (let [_, styleData] of this.styleService.styles) { + if (styleData.featureLayerStyle && styleData.params.visible) { + let visu = new TileVisualization( + featureTile, + this.pointMergeService, + (tileKey: string)=>this.getFeatureTile(tileKey), + styleData.featureLayerStyle, + true, + mode, + featureIds); + this.tileVisualizationTopic.next(visu); + visualizationCollection.push(visu); + } + } + } } diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index f4393574..54973f21 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -53,10 +53,7 @@ declare let window: DebugWindow; }) export class ErdblickViewComponent implements AfterViewInit { viewer!: Viewer; - private hoveredFeature: any = null; - private hoveredFeatureOrigColor: Color | null = null; private mouseHandler: ScreenSpaceEventHandler | null = null; - private tileVisForPrimitive: Map; private openStreetMapLayer: ImageryLayer | null = null; private marker: Entity | null = null; @@ -78,26 +75,15 @@ export class ErdblickViewComponent implements AfterViewInit { public keyboardService: KeyboardService, public coordinatesService: CoordinatesService) { - this.tileVisForPrimitive = new Map(); - this.mapService.tileVisualizationTopic.subscribe((tileVis: TileVisualization) => { tileVis.render(this.viewer).then(wasRendered => { if (wasRendered) { - tileVis.forEachPrimitive((primitive: any) => { - this.tileVisForPrimitive.set(primitive, tileVis); - }) this.viewer.scene.requestRender(); } }); }); this.mapService.tileVisualizationDestructionTopic.subscribe((tileVis: TileVisualization) => { - if (this.hoveredFeature && this.tileVisForPrimitive.get(this.hoveredFeature.primitive) === tileVis) { - this.setHoveredCesiumFeature(null); - } - tileVis.forEachPrimitive((primitive: any) => { - this.tileVisForPrimitive.delete(primitive); - }) tileVis.destroy(this.viewer); this.viewer.scene.requestRender(); }); @@ -275,40 +261,30 @@ export class ErdblickViewComponent implements AfterViewInit { this.keyboardService.registerShortcuts(['r', 'R'], this.resetOrientation.bind(this)); } - /** - * Check if two cesium features are equal. A cesium feature is a - * combination of a feature id and a primitive which contains it. - */ - private cesiumFeaturesAreEqual(f1: any, f2: any) { - return (!f1 && !f2) || (f1 && f2 && f1.id === f2.id && f1.primitive === f1.primitive); - } - /** Check if the given feature is known and can be selected. */ isKnownCesiumFeature(f: any) { - return f && f.id !== undefined && f.primitive !== undefined && ( - this.tileVisForPrimitive.has(f.primitive) || - this.tileVisForPrimitive.has(f.primitive._pointPrimitiveCollection)) + return f && f.id !== undefined && f.id.hasOwnProperty("type") } /** * Set or re-set the hovered feature. */ private setHoveredCesiumFeature(feature: any) { - if (this.cesiumFeaturesAreEqual(feature, this.hoveredFeature)) { + // Get the actual mapget feature for the picked Cesium feature. + let resolvedFeature = feature ? this.mapService.resolveFeature(feature.id) : null; + if (!resolvedFeature) { + this.mapService.hoverTopic.next([]); return; } - // Restore the previously hovered feature to its original color. - if (this.hoveredFeature && this.hoveredFeatureOrigColor) { - this.setFeatureColor(this.hoveredFeature, this.hoveredFeatureOrigColor); + + if (this.mapService.selectionTopic.getValue().some(f => resolvedFeature!.equals(f))) { + return; } - this.hoveredFeature = null; - let resolvedFeature = feature ? this.resolveFeature(feature.primitive, feature.id) : null; - if (resolvedFeature && !resolvedFeature?.equals(this.mapService.selectionTopic.getValue())) { - // Highlight the new hovered feature and remember its original color. - this.hoveredFeatureOrigColor = this.getFeatureColor(feature); - this.setFeatureColor(feature, Color.YELLOW); - this.hoveredFeature = feature; + if (this.mapService.hoverTopic.getValue().some(f => resolvedFeature!.equals(f))) { + return; } + + this.mapService.hoverTopic.next([resolvedFeature]); } /** @@ -316,67 +292,20 @@ export class ErdblickViewComponent implements AfterViewInit { */ private setPickedCesiumFeature(feature: any) { // Get the actual mapget feature for the picked Cesium feature. - let resolvedFeature = feature ? this.resolveFeature(feature.primitive, feature.id) : null; + let resolvedFeature = feature ? this.mapService.resolveFeature(feature.id) : null; if (!resolvedFeature) { - this.mapService.selectionTopic.next(null); + this.mapService.selectionTopic.next([]); return; } - if (resolvedFeature.equals(this.mapService.selectionTopic.getValue())) { + if (this.mapService.selectionTopic.getValue().some(f => resolvedFeature!.equals(f))) { return; } - - // Make sure that if the hovered feature is picked, we don't - // remember the hover color as the original color. - if (this.cesiumFeaturesAreEqual(feature, this.hoveredFeature)) { + if (this.mapService.hoverTopic.getValue().some(f => resolvedFeature!.equals(f))) { this.setHoveredCesiumFeature(null); } - this.mapService.selectionTopic.next(resolvedFeature); - } - /** Set the color of a cesium feature through its associated primitive. */ - private setFeatureColor(feature: any, color: Color) { - if (feature.primitive.color !== undefined) { - // Special treatment for point primitives. - feature.primitive.color = color; - this.viewer.scene.requestRender(); - return; - } - if (feature.primitive.isDestroyed()) { - return; - } - const attributes = feature.primitive.getGeometryInstanceAttributes(feature.id); - attributes.color = ColorGeometryInstanceAttribute.toValue(color); - this.viewer.scene.requestRender(); - } - - /** Read the color of a cesium feature through its associated primitive. */ - private getFeatureColor(feature: any): Color | null { - if (feature.primitive.color !== undefined) { - // Special treatment for point primitives. - return feature.primitive.color.clone(); - } - if (feature.primitive.isDestroyed()) { - return null; - } - const attributes = feature.primitive.getGeometryInstanceAttributes(feature.id); - if (attributes.color === undefined) { - return null; - } - return Color.fromBytes(...attributes.color); - } - - /** Get a mapget feature from a cesium feature. */ - private resolveFeature(primitive: any, index: number) { - let tileVis = this.tileVisForPrimitive.get(primitive); - if (!tileVis) { - tileVis = this.tileVisForPrimitive.get(primitive._pointPrimitiveCollection); - if (!tileVis) { - console.error("Failed find tileLayer for primitive!"); - return null; - } - } - return new FeatureWrapper(index, tileVis.tile); + this.mapService.selectionTopic.next([resolvedFeature]); } /** diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index ac7b0304..ac8dd5e0 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -9,7 +9,7 @@ import { CallbackProperty, HeightReference } from "./cesium"; -import {FeatureLayerStyle, TileFeatureLayer} from "../../build/libs/core/erdblick-core"; +import {FeatureLayerStyle, TileFeatureLayer, HighlightMode} from "../../build/libs/core/erdblick-core"; import {PointMergeService} from "./pointmerge.service"; export interface LocateResolution { @@ -95,7 +95,8 @@ export class TileVisualization { private hasHighDetailVisualization: boolean = false; private hasTileBorder: boolean = false; private renderingInProgress: boolean = false; - private readonly highlight: string; + private readonly highlightMode: HighlightMode; + private readonly featureIdSubset: string[]; private deleted: boolean = false; private readonly auxTileFun: (key: string)=>FeatureTile|null; private readonly options: Record; @@ -103,7 +104,7 @@ export class TileVisualization { /** * Create a tile visualization. - * @param tile {FeatureTile} The tile to visualize. + * @param tile The tile to visualize. * @param pointMergeService Instance of the central PointMergeService, used to visualize merged point features. * @param auxTileFun Callback which may be called to resolve external references * for relation visualization. @@ -112,9 +113,10 @@ export class TileVisualization { * a low-detail representation is indicated by `false`, and * will result in a dot representation. A high-detail representation * based on the style can be triggered using `true`. - * @param highlight Controls whether the visualization will run rules that - * have `mode: highlight` set, otherwise, only rules with the default - * `mode: normal` are executed. + * @param highlightMode Controls whether the visualization will run rules that + * have a specific highlight mode. + * @param featureIdSubset Subset of feature IDs for visualization. If not set, + * all features in the tile will be visualized. * @param boxGrid Sets a flag to wrap this tile visualization into a bounding box * @param options Option values for option variables defined by the style sheet. */ @@ -124,7 +126,8 @@ export class TileVisualization { auxTileFun: (key: string) => FeatureTile | null, style: FeatureLayerStyle, highDetail: boolean, - highlight: string = "", + highlightMode: HighlightMode = coreLib.HighlightMode.NO_HIGHLIGHT, + featureIdSubset?: string[], boxGrid?: boolean, options?: Record) { @@ -132,7 +135,8 @@ export class TileVisualization { this.style = style as StyleWithIsDeleted; this.isHighDetail = highDetail; this.renderingInProgress = false; - this.highlight = highlight; + this.highlightMode = highlightMode; + this.featureIdSubset = featureIdSubset || []; this.deleted = false; this.auxTileFun = auxTileFun; this.showTileBorder = boxGrid === undefined ? false : boxGrid; @@ -167,7 +171,8 @@ export class TileVisualization { this.style, this.options, this.pointMergeService, - this.highlight!); + this.highlightMode, + this.featureIdSubset); visualization.addTileFeatureLayer(tileFeatureLayer); try { visualization.run(); diff --git a/libs/core/include/erdblick/cesium-interface/labels.h b/libs/core/include/erdblick/cesium-interface/labels.h index add46baa..916f65d8 100644 --- a/libs/core/include/erdblick/cesium-interface/labels.h +++ b/libs/core/include/erdblick/cesium-interface/labels.h @@ -18,7 +18,7 @@ struct CesiumPrimitiveLabelsCollection { JsValue const &position, const std::string& labelText, FeatureStyleRule const &style, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun); /** diff --git a/libs/core/include/erdblick/cesium-interface/points.h b/libs/core/include/erdblick/cesium-interface/points.h index 07b387c2..f23de407 100644 --- a/libs/core/include/erdblick/cesium-interface/points.h +++ b/libs/core/include/erdblick/cesium-interface/points.h @@ -18,7 +18,7 @@ struct CesiumPointPrimitiveCollection void addPoint( const JsValue& position, FeatureStyleRule const& style, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun); /** diff --git a/libs/core/include/erdblick/cesium-interface/primitive.h b/libs/core/include/erdblick/cesium-interface/primitive.h index fc3fafde..99c24300 100644 --- a/libs/core/include/erdblick/cesium-interface/primitive.h +++ b/libs/core/include/erdblick/cesium-interface/primitive.h @@ -64,7 +64,7 @@ struct CesiumPrimitive void addPolyLine( JsValue const& vertices, FeatureStyleRule const& style, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun); /** @@ -77,7 +77,7 @@ struct CesiumPrimitive void addPolygon( JsValue const& vertices, FeatureStyleRule const& style, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun); /** @@ -91,7 +91,7 @@ struct CesiumPrimitive void addTriangles( JsValue const& float64Array, FeatureStyleRule const& style, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun); /** @@ -111,7 +111,7 @@ struct CesiumPrimitive */ void addGeometryInstance( const FeatureStyleRule& style, - uint32_t id, + std::string_view const& id, const JsValue& geom, BoundEvalFun const& evalFun); diff --git a/libs/core/include/erdblick/parser.h b/libs/core/include/erdblick/parser.h index 9e014538..aa953c3a 100644 --- a/libs/core/include/erdblick/parser.h +++ b/libs/core/include/erdblick/parser.h @@ -1,6 +1,7 @@ #pragma once #include "mapget/model/stream.h" +#include "mapget/model/featurelayer.h" #include "buffer.h" #include "cesium-interface/object.h" #include "mapget/model/featurelayer.h" diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index 82a44995..57838639 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -28,9 +28,10 @@ class FeatureStyleRule Attribute }; - enum Mode { - Normal, - Highlight + enum HighlightMode { + NoHighlight, + HoverHighlight, + SelectionHighlight }; enum Arrow { @@ -42,7 +43,7 @@ class FeatureStyleRule FeatureStyleRule const* match(mapget::Feature& feature) const; [[nodiscard]] Aspect aspect() const; - [[nodiscard]] Mode mode() const; + [[nodiscard]] HighlightMode mode() const; [[nodiscard]] bool selectable() const; [[nodiscard]] bool supports(mapget::GeomType const& g) const; @@ -101,7 +102,7 @@ class FeatureStyleRule } Aspect aspect_ = Feature; - Mode mode_ = Normal; + HighlightMode mode_ = NoHighlight; bool selectable_ = true; uint32_t geometryTypes_ = 0; // bitfield from GeomType enum std::optional type_; diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index bf122573..218bda73 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -18,7 +18,7 @@ class FeatureLayerVisualization; * Feature ID which is used when the rendered representation is not * supposed to be selectable. */ -static constexpr uint32_t UnselectableId = 0xffffffff; +static std::string UnselectableId; /** * Covers the state for the visualization of a single Relation-Style+Feature @@ -82,7 +82,8 @@ class FeatureLayerVisualization const FeatureLayerStyle& style, NativeJsValue const& rawOptionValues, NativeJsValue const& rawFeatureMergeService, - std::string highlightFeatureIndex = ""); + FeatureStyleRule::HighlightMode const& highlightMode = FeatureStyleRule::NoHighlight, + NativeJsValue const& rawFeatureIdSubset = {}); /** * Add a tile which is considered for visualization. All tiles added after @@ -131,7 +132,6 @@ class FeatureLayerVisualization */ void addFeature( mapget::model_ptr& feature, - uint32_t id, FeatureStyleRule const& rule); /** @@ -141,7 +141,7 @@ class FeatureLayerVisualization mapget::model_ptr const& feature, std::string_view const& layer, mapget::model_ptr const& attr, - uint32_t id, + std::string_view const& id, const FeatureStyleRule& rule, uint32_t& offsetFactor, glm::dvec3 const& offset); @@ -152,7 +152,7 @@ class FeatureLayerVisualization */ void addGeometry( mapget::model_ptr const& geom, - uint32_t id, + std::string_view id, FeatureStyleRule const& rule, BoundEvalFun const& evalFun, glm::dvec3 const& offset = {.0, .0, .0}); @@ -165,7 +165,7 @@ class FeatureLayerVisualization void addLine( mapget::Point const& wgsA, mapget::Point const& wgsB, - uint32_t id, + std::string_view const& id, FeatureStyleRule const& rule, BoundEvalFun const& evalFun, glm::dvec3 const& offset, @@ -177,7 +177,7 @@ class FeatureLayerVisualization void addPolyLine( std::vector const& vertsCartesian, const FeatureStyleRule& rule, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun); /** @@ -237,9 +237,10 @@ class FeatureLayerVisualization FeatureLayerStyle const& style_; mapget::TileFeatureLayer::Ptr tile_; std::vector> allTiles_; - std::string highlightFeatureId_; + std::set featureIdSubset_; std::shared_ptr internalStringPoolCopy_; std::map optionValues_; + FeatureStyleRule::HighlightMode highlightMode_; /// ===== Relation Processing Members ===== diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index 6a0055d5..683a0d18 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -282,6 +282,10 @@ EMSCRIPTEN_BINDINGS(erdblick) ////////// Feature using FeaturePtr = mapget::model_ptr; em::class_("Feature") + .function( + "isNull", + std::function( + [](FeaturePtr& self) { return !self; })) .function( "id", std::function( @@ -350,15 +354,12 @@ EMSCRIPTEN_BINDINGS(erdblick) return result; })) .function( - "at", + "find", std::function< - mapget::model_ptr(mapget::TileFeatureLayer const&, int i)>( - [](mapget::TileFeatureLayer const& self, int i) + mapget::model_ptr(mapget::TileFeatureLayer const&, std::string const& id)>( + [](mapget::TileFeatureLayer const& self, std::string const& id) { - if (i < 0 || i >= self.numRoots()) { - mapget::log().error("TileFeatureLayer::at(): Index {} is oob.", i); - } - return self.at(i); + return self.find(id); })) .function( "findFeatureIndex", @@ -374,9 +375,15 @@ EMSCRIPTEN_BINDINGS(erdblick) })); em::register_vector>("TileFeatureLayers"); + ////////// Highlight Modes + em::enum_("HighlightMode") + .value("NO_HIGHLIGHT", FeatureStyleRule::NoHighlight) + .value("HOVER_HIGHLIGHT", FeatureStyleRule::HoverHighlight) + .value("SELECTION_HIGHLIGHT", FeatureStyleRule::SelectionHighlight); + ////////// FeatureLayerVisualization em::class_("FeatureLayerVisualization") - .constructor() + .constructor() .function("addTileFeatureLayer", &FeatureLayerVisualization::addTileFeatureLayer) .function("run", &FeatureLayerVisualization::run) .function("primitiveCollection", &FeatureLayerVisualization::primitiveCollection) diff --git a/libs/core/src/cesium-interface/labels.cpp b/libs/core/src/cesium-interface/labels.cpp index a8f34dd8..c3223f7b 100644 --- a/libs/core/src/cesium-interface/labels.cpp +++ b/libs/core/src/cesium-interface/labels.cpp @@ -13,7 +13,7 @@ void CesiumPrimitiveLabelsCollection::addLabel( JsValue const &position, const std::string &labelText, FeatureStyleRule const &style, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun) { auto const &color = style.labelColor(); auto const &outlineColor = style.labelOutlineColor(); diff --git a/libs/core/src/cesium-interface/points.cpp b/libs/core/src/cesium-interface/points.cpp index a579e372..aecffaee 100644 --- a/libs/core/src/cesium-interface/points.cpp +++ b/libs/core/src/cesium-interface/points.cpp @@ -15,7 +15,7 @@ CesiumPointPrimitiveCollection::CesiumPointPrimitiveCollection() : void CesiumPointPrimitiveCollection::addPoint( const JsValue& position, FeatureStyleRule const& style, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun) { auto const color = style.color(evalFun); diff --git a/libs/core/src/cesium-interface/primitive.cpp b/libs/core/src/cesium-interface/primitive.cpp index 1a8f327d..ebbc93db 100644 --- a/libs/core/src/cesium-interface/primitive.cpp +++ b/libs/core/src/cesium-interface/primitive.cpp @@ -64,7 +64,7 @@ CesiumPrimitive CesiumPrimitive::withPerInstanceColorAppearance(bool flatAndSync void CesiumPrimitive::addPolyLine( JsValue const &vertices, FeatureStyleRule const &style, - uint32_t id, + std::string_view const& id, BoundEvalFun const &evalFun) { JsValue polyline; if (clampToGround_) { @@ -85,7 +85,7 @@ void CesiumPrimitive::addPolyLine( void CesiumPrimitive::addPolygon( const JsValue &vertices, const FeatureStyleRule &style, - uint32_t id, + std::string_view const& id, BoundEvalFun const &evalFun) { auto polygon = Cesium().PolygonGeometry.New({ {"polygonHierarchy", Cesium().PolygonHierarchy.New(*vertices)}, @@ -98,7 +98,7 @@ void CesiumPrimitive::addPolygon( void CesiumPrimitive::addTriangles( const JsValue &float64Array, const FeatureStyleRule &style, - uint32_t id, + std::string_view const& id, BoundEvalFun const &evalFun) { auto geometry = Cesium().Geometry.New({ {"attributes", JsValue::Dict({ @@ -115,7 +115,7 @@ void CesiumPrimitive::addTriangles( void CesiumPrimitive::addGeometryInstance( const FeatureStyleRule &style, - uint32_t id, + std::string_view const& id, const JsValue &geom, BoundEvalFun const &evalFun) { auto attributes = JsValue::Dict(); diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index 23630451..7bfa3fb5 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -100,11 +100,14 @@ void FeatureStyleRule::parse(const YAML::Node& yaml) if (yaml["mode"].IsDefined()) { // Parse the feature aspect that is covered by this rule. auto modeStr = yaml["mode"].as(); - if (modeStr == "normal") { - mode_ = Normal; + if (modeStr == "none") { + mode_ = NoHighlight; } - else if (modeStr == "highlight") { - mode_ = Highlight; + else if (modeStr == "hover") { + mode_ = HoverHighlight; + } + else if (modeStr == "selection") { + mode_ = SelectionHighlight; } else { std::cout << "Unsupported mode: " << modeStr << std::endl; @@ -532,7 +535,7 @@ bool FeatureStyleRule::selectable() const return selectable_; } -FeatureStyleRule::Mode FeatureStyleRule::mode() const +FeatureStyleRule::HighlightMode FeatureStyleRule::mode() const { return mode_; } diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index 6bc34870..0482061d 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -22,16 +22,17 @@ FeatureLayerVisualization::FeatureLayerVisualization( const FeatureLayerStyle& style, NativeJsValue const& rawOptionValues, NativeJsValue const& rawFeatureMergeService, - std::string highlightFeatureId) + FeatureStyleRule::HighlightMode const& highlightMode, + NativeJsValue const& rawFeatureIdSubset) : coloredLines_(CesiumPrimitive::withPolylineColorAppearance(false)), coloredNontrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(false, false)), coloredTrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(true)), coloredGroundLines_(CesiumPrimitive::withPolylineColorAppearance(true)), coloredGroundMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(true, true)), + featureMergeService_(rawFeatureMergeService), style_(style), - highlightFeatureId_(std::move(highlightFeatureId)), - externalRelationReferences_(JsValue::List()), - featureMergeService_(rawFeatureMergeService) + highlightMode_(highlightMode), + externalRelationReferences_(JsValue::List()) { // Convert option values dict to simfil values. auto optionValues = JsValue(rawOptionValues); @@ -46,6 +47,12 @@ FeatureLayerVisualization::FeatureLayerVisualization( }); optionValues_.emplace(option.id_, std::move(simfilValue)); } + + // Convert feature ID subset. + auto featureIdSubset = JsValue(rawFeatureIdSubset); + for (auto i = 0; i < featureIdSubset.size(); ++i) { + featureIdSubset_.insert(featureIdSubset.at(i).as()); + } } void FeatureLayerVisualization::addTileFeatureLayer( @@ -67,30 +74,22 @@ void FeatureLayerVisualization::addTileFeatureLayer( void FeatureLayerVisualization::run() { - uint32_t featureId = 0; - for (auto&& feature : *tile_) { - if (!highlightFeatureId_.empty()) { - if (feature->id()->toString() != highlightFeatureId_) { - ++featureId; + if (!featureIdSubset_.empty()) { + if (featureIdSubset_.find(feature->id()->toString()) == featureIdSubset_.end()) { continue; } } for (auto&& rule : style_.rules()) { - if (!highlightFeatureId_.empty()) { - if (rule.mode() != FeatureStyleRule::Highlight) - continue; - } - else if (rule.mode() != FeatureStyleRule::Normal) + if (rule.mode() != highlightMode_) continue; if (auto* matchingSubRule = rule.match(*feature)) { - addFeature(feature, featureId, *matchingSubRule); + addFeature(feature, *matchingSubRule); featuresAdded_ = true; } } - ++featureId; } } @@ -189,9 +188,9 @@ void FeatureLayerVisualization::processResolvedExternalReferences( void FeatureLayerVisualization::addFeature( model_ptr& feature, - uint32_t id, FeatureStyleRule const& rule) { + auto id = feature->id()->toString(); auto offset = localWgs84UnitCoordinateSystem(feature->firstGeometry()) * rule.offset(); switch(rule.aspect()) { @@ -249,7 +248,7 @@ void FeatureLayerVisualization::addFeature( void FeatureLayerVisualization::addGeometry( model_ptr const& geom, - uint32_t id, + std::string_view id, FeatureStyleRule const& rule, BoundEvalFun const& evalFun, glm::dvec3 const& offset) @@ -400,7 +399,7 @@ CesiumPrimitive& FeatureLayerVisualization::getPrimitiveForArrowMaterial( void erdblick::FeatureLayerVisualization::addLine( const Point& wgsA, const Point& wgsB, - uint32_t id, + std::string_view const& id, const erdblick::FeatureStyleRule& rule, BoundEvalFun const& evalFun, glm::dvec3 const& offset, @@ -431,7 +430,7 @@ void erdblick::FeatureLayerVisualization::addLine( void FeatureLayerVisualization::addPolyLine( std::vector const& vertsCartesian, const FeatureStyleRule& rule, - uint32_t id, + std::string_view const& id, BoundEvalFun const& evalFun) { if (vertsCartesian.size() < 2) @@ -490,7 +489,7 @@ void FeatureLayerVisualization::addAttribute( model_ptr const& feature, std::string_view const& layer, model_ptr const& attr, - uint32_t id, + std::string_view const& id, const FeatureStyleRule& rule, uint32_t& offsetFactor, glm::dvec3 const& offset) From 2458f9a1e78c1b47d2a47b960fc6df377beffc80 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Mon, 19 Aug 2024 16:02:58 +0200 Subject: [PATCH 05/37] FeatureLayerVisualization now creates new compound feature IDs. --- erdblick_app/app/features.model.ts | 8 +- erdblick_app/app/map.service.ts | 14 ++-- erdblick_app/app/view.component.ts | 2 +- erdblick_app/app/visualization.model.ts | 16 +--- .../erdblick/cesium-interface/labels.h | 8 +- .../erdblick/cesium-interface/points.h | 2 +- .../erdblick/cesium-interface/primitive.h | 8 +- libs/core/include/erdblick/visualization.h | 11 ++- libs/core/src/bindings.cpp | 2 +- libs/core/src/cesium-interface/labels.cpp | 12 +-- libs/core/src/cesium-interface/points.cpp | 4 +- libs/core/src/cesium-interface/primitive.cpp | 10 +-- libs/core/src/visualization.cpp | 74 ++++++++++++------- 13 files changed, 93 insertions(+), 78 deletions(-) diff --git a/erdblick_app/app/features.model.ts b/erdblick_app/app/features.model.ts index 7f50ea8c..2f081505 100644 --- a/erdblick_app/app/features.model.ts +++ b/erdblick_app/app/features.model.ts @@ -10,7 +10,7 @@ import {TileLayerParser, TileFeatureLayer} from '../../build/libs/core/erdblick- * WASM TileFeatureLayer, use the peek()-function. */ export class FeatureTile { - id: string; + mapTileKey: string; nodeId: string; mapName: string; layerName: string; @@ -31,7 +31,7 @@ export class FeatureTile { let mapTileMetadata = uint8ArrayToWasm((wasmBlob: any) => { return parser.readTileLayerMetadata(wasmBlob); }, tileFeatureLayerBlob); - this.id = mapTileMetadata.id; + this.mapTileKey = mapTileMetadata.id; this.nodeId = mapTileMetadata.nodeId; this.mapName = mapTileMetadata.mapName; this.layerName = mapTileMetadata.layerName; @@ -168,7 +168,7 @@ export class FeatureWrapper { */ peek(callback: any) { if (this.featureTile.disposed) { - throw new Error(`Unable to access feature of deleted layer ${this.featureTile.id}!`); + throw new Error(`Unable to access feature of deleted layer ${this.featureTile.mapTileKey}!`); } return this.featureTile.peek((tileFeatureLayer: TileFeatureLayer) => { let feature = tileFeatureLayer.find(this.featureId); @@ -188,6 +188,6 @@ export class FeatureWrapper { if (!other) { return false; } - return this.featureTile.id == other.featureTile.id && this.featureId == other.featureId; + return this.featureTile.mapTileKey == other.featureTile.mapTileKey && this.featureId == other.featureId; } } diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index 76901d51..a335cbf7 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -401,7 +401,7 @@ export class MapService { if (evictTileLayer(tileLayer)) { tileLayer.destroy(); } else { - newTileLayers.set(tileLayer.id, tileLayer); + newTileLayers.set(tileLayer.mapTileKey, tileLayer); } } this.loadedTileLayers = newTileLayers; @@ -527,7 +527,7 @@ export class MapService { let tileLayer = new FeatureTile(this.tileParser!, tileLayerBlob, preventCulling); // Consider, if this tile is a selection tile request. - if (this.selectionTileRequest && tileLayer.id == this.selectionTileRequest.tileKey) { + if (this.selectionTileRequest && tileLayer.mapTileKey == this.selectionTileRequest.tileKey) { this.selectionTileRequest.resolve!(tileLayer); this.selectionTileRequest = null; } @@ -540,10 +540,10 @@ export class MapService { // If this one replaces an older tile with the same key, // then first remove the older existing one. - if (this.loadedTileLayers.has(tileLayer.id)) { - this.removeTileLayer(this.loadedTileLayers.get(tileLayer.id)); + if (this.loadedTileLayers.has(tileLayer.mapTileKey)) { + this.removeTileLayer(this.loadedTileLayers.get(tileLayer.mapTileKey)); } - this.loadedTileLayers.set(tileLayer.id, tileLayer); + this.loadedTileLayers.set(tileLayer.mapTileKey, tileLayer); // Schedule the visualization of the newly added tile layer, // but don't do it synchronously to avoid stalling the main thread. @@ -562,7 +562,7 @@ export class MapService { tileLayer.destroy() for (const styleId of this.visualizedTileLayers.keys()) { const tileVisus = this.visualizedTileLayers.get(styleId)?.filter(tileVisu => { - if (tileVisu.tile.id === tileLayer.id) { + if (tileVisu.tile.mapTileKey === tileLayer.id) { this.tileVisualizationDestructionTopic.next(tileVisu); return false; } @@ -575,7 +575,7 @@ export class MapService { } } this.tileVisualizationQueue = this.tileVisualizationQueue.filter(([_, tileVisu]) => { - return tileVisu.tile.id !== tileLayer.id; + return tileVisu.tile.mapTileKey !== tileLayer.id; }); this.loadedTileLayers.delete(tileLayer.id); } diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index 54973f21..a5f6fed1 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -263,7 +263,7 @@ export class ErdblickViewComponent implements AfterViewInit { /** Check if the given feature is known and can be selected. */ isKnownCesiumFeature(f: any) { - return f && f.id !== undefined && f.id.hasOwnProperty("type") + return f && f.id !== undefined && f.id.hasOwnProperty("mapTileKey") } /** diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index ac8dd5e0..73921929 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -146,7 +146,7 @@ export class TileVisualization { /** * Actually create the visualization. - * @param viewer {Cesium.Viewer} The viewer to add the rendered entity to. + * @param viewer {Viewer} The viewer to add the rendered entity to. * @return True if anything was rendered, false otherwise. */ async render(viewer: Viewer) { @@ -168,6 +168,7 @@ export class TileVisualization { if (this.isHighDetailAndNotEmpty()) { returnValue = await this.tile.peekAsync(async (tileFeatureLayer: TileFeatureLayer) => { let visualization = new coreLib.FeatureLayerVisualization( + this.tile.mapTileKey, this.style, this.options, this.pointMergeService, @@ -256,7 +257,7 @@ export class TileVisualization { /** * Destroy any current visualization. - * @param viewer {Cesium.Viewer} The viewer to remove the rendered entity from. + * @param viewer {Viewer} The viewer to remove the rendered entity from. */ destroy(viewer: Viewer) { this.deleted = true; @@ -278,17 +279,6 @@ export class TileVisualization { this.hasTileBorder = false; } - /** - * Iterate over all Cesium primitives of this visualization. - */ - forEachPrimitive(callback: any) { - if (this.primitiveCollection) { - for (let i = 0; i < this.primitiveCollection.length; ++i) { - callback(this.primitiveCollection.get(i)); - } - } - } - /** * Check if the visualization is high-detail, and the * underlying data is not empty. diff --git a/libs/core/include/erdblick/cesium-interface/labels.h b/libs/core/include/erdblick/cesium-interface/labels.h index 916f65d8..4c829961 100644 --- a/libs/core/include/erdblick/cesium-interface/labels.h +++ b/libs/core/include/erdblick/cesium-interface/labels.h @@ -7,9 +7,9 @@ namespace erdblick { -struct CesiumPrimitiveLabelsCollection { - - CesiumPrimitiveLabelsCollection(); +struct CesiumLabelCollection +{ + CesiumLabelCollection(); /** * Add an individual label to the collection @@ -18,7 +18,7 @@ struct CesiumPrimitiveLabelsCollection { JsValue const &position, const std::string& labelText, FeatureStyleRule const &style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun); /** diff --git a/libs/core/include/erdblick/cesium-interface/points.h b/libs/core/include/erdblick/cesium-interface/points.h index f23de407..f15d270f 100644 --- a/libs/core/include/erdblick/cesium-interface/points.h +++ b/libs/core/include/erdblick/cesium-interface/points.h @@ -18,7 +18,7 @@ struct CesiumPointPrimitiveCollection void addPoint( const JsValue& position, FeatureStyleRule const& style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun); /** diff --git a/libs/core/include/erdblick/cesium-interface/primitive.h b/libs/core/include/erdblick/cesium-interface/primitive.h index 99c24300..7e69b465 100644 --- a/libs/core/include/erdblick/cesium-interface/primitive.h +++ b/libs/core/include/erdblick/cesium-interface/primitive.h @@ -64,7 +64,7 @@ struct CesiumPrimitive void addPolyLine( JsValue const& vertices, FeatureStyleRule const& style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun); /** @@ -77,7 +77,7 @@ struct CesiumPrimitive void addPolygon( JsValue const& vertices, FeatureStyleRule const& style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun); /** @@ -91,7 +91,7 @@ struct CesiumPrimitive void addTriangles( JsValue const& float64Array, FeatureStyleRule const& style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun); /** @@ -111,7 +111,7 @@ struct CesiumPrimitive */ void addGeometryInstance( const FeatureStyleRule& style, - std::string_view const& id, + JsValue const& id, const JsValue& geom, BoundEvalFun const& evalFun); diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index 218bda73..3c61af93 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -79,6 +79,7 @@ class FeatureLayerVisualization * Convert a TileFeatureLayer into Cesium primitives based on the provided style. */ FeatureLayerVisualization( + std::string const& mapTileKey, const FeatureLayerStyle& style, NativeJsValue const& rawOptionValues, NativeJsValue const& rawFeatureMergeService, @@ -177,7 +178,7 @@ class FeatureLayerVisualization void addPolyLine( std::vector const& vertsCartesian, const FeatureStyleRule& rule, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun); /** @@ -218,8 +219,14 @@ class FeatureLayerVisualization */ void addOptionsToSimfilContext(simfil::OverlayNode& context); + /** + * Create a feature primitive ID struct from the mapTileKey_ and the given feature ID. + */ + JsValue makeTileFeatureId(std::string_view const& featureId) const; + /// =========== Generic Members =========== + JsValue mapTileKey_; bool featuresAdded_ = false; CesiumPrimitive coloredLines_; std::map, CesiumPrimitive> dashLines_; @@ -231,7 +238,7 @@ class FeatureLayerVisualization std::map arrowGroundLines_; CesiumPrimitive coloredGroundMeshes_; CesiumPointPrimitiveCollection coloredPoints_; - CesiumPrimitiveLabelsCollection labelCollection_; + CesiumLabelCollection labelCollection_; JsValue featureMergeService_; FeatureLayerStyle const& style_; diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index 683a0d18..aeb165e2 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -383,7 +383,7 @@ EMSCRIPTEN_BINDINGS(erdblick) ////////// FeatureLayerVisualization em::class_("FeatureLayerVisualization") - .constructor() + .constructor() .function("addTileFeatureLayer", &FeatureLayerVisualization::addTileFeatureLayer) .function("run", &FeatureLayerVisualization::run) .function("primitiveCollection", &FeatureLayerVisualization::primitiveCollection) diff --git a/libs/core/src/cesium-interface/labels.cpp b/libs/core/src/cesium-interface/labels.cpp index c3223f7b..670db595 100644 --- a/libs/core/src/cesium-interface/labels.cpp +++ b/libs/core/src/cesium-interface/labels.cpp @@ -6,14 +6,14 @@ namespace erdblick { -CesiumPrimitiveLabelsCollection::CesiumPrimitiveLabelsCollection() : +CesiumLabelCollection::CesiumLabelCollection() : labelCollection_(Cesium().LabelCollection.New()) {} -void CesiumPrimitiveLabelsCollection::addLabel( +void CesiumLabelCollection::addLabel( JsValue const &position, const std::string &labelText, FeatureStyleRule const &style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun) { auto const &color = style.labelColor(); auto const &outlineColor = style.labelOutlineColor(); @@ -21,7 +21,7 @@ void CesiumPrimitiveLabelsCollection::addLabel( auto const &padding = style.labelBackgroundPadding(); auto labelProperties = JsValue::Dict({ - {"id", JsValue(id)}, + {"id", id}, {"position", position}, {"show", JsValue(true)}, {"text", JsValue(labelText)}, @@ -66,11 +66,11 @@ void CesiumPrimitiveLabelsCollection::addLabel( numLabelInstances_++; } -NativeJsValue CesiumPrimitiveLabelsCollection::toJsObject() const { +NativeJsValue CesiumLabelCollection::toJsObject() const { return *labelCollection_; } -bool CesiumPrimitiveLabelsCollection::empty() const { +bool CesiumLabelCollection::empty() const { return numLabelInstances_ == 0; } diff --git a/libs/core/src/cesium-interface/points.cpp b/libs/core/src/cesium-interface/points.cpp index aecffaee..cad7128a 100644 --- a/libs/core/src/cesium-interface/points.cpp +++ b/libs/core/src/cesium-interface/points.cpp @@ -15,7 +15,7 @@ CesiumPointPrimitiveCollection::CesiumPointPrimitiveCollection() : void CesiumPointPrimitiveCollection::addPoint( const JsValue& position, FeatureStyleRule const& style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const& evalFun) { auto const color = style.color(evalFun); @@ -25,7 +25,7 @@ void CesiumPointPrimitiveCollection::addPoint( {"position", position}, {"color", Cesium().Color.New(color.r, color.g, color.b, color.a)}, {"pixelSize", JsValue(style.width())}, - {"id", JsValue(id)}, + {"id", id}, {"outlineColor", Cesium().Color.New(oColor.r, oColor.g, oColor.b, oColor.a)}, {"outlineWidth", JsValue(style.outlineWidth())}, }); diff --git a/libs/core/src/cesium-interface/primitive.cpp b/libs/core/src/cesium-interface/primitive.cpp index ebbc93db..b59dea93 100644 --- a/libs/core/src/cesium-interface/primitive.cpp +++ b/libs/core/src/cesium-interface/primitive.cpp @@ -64,7 +64,7 @@ CesiumPrimitive CesiumPrimitive::withPerInstanceColorAppearance(bool flatAndSync void CesiumPrimitive::addPolyLine( JsValue const &vertices, FeatureStyleRule const &style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const &evalFun) { JsValue polyline; if (clampToGround_) { @@ -85,7 +85,7 @@ void CesiumPrimitive::addPolyLine( void CesiumPrimitive::addPolygon( const JsValue &vertices, const FeatureStyleRule &style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const &evalFun) { auto polygon = Cesium().PolygonGeometry.New({ {"polygonHierarchy", Cesium().PolygonHierarchy.New(*vertices)}, @@ -98,7 +98,7 @@ void CesiumPrimitive::addPolygon( void CesiumPrimitive::addTriangles( const JsValue &float64Array, const FeatureStyleRule &style, - std::string_view const& id, + JsValue const& id, BoundEvalFun const &evalFun) { auto geometry = Cesium().Geometry.New({ {"attributes", JsValue::Dict({ @@ -115,7 +115,7 @@ void CesiumPrimitive::addTriangles( void CesiumPrimitive::addGeometryInstance( const FeatureStyleRule &style, - std::string_view const& id, + JsValue const& id, const JsValue &geom, BoundEvalFun const &evalFun) { auto attributes = JsValue::Dict(); @@ -125,7 +125,7 @@ void CesiumPrimitive::addGeometryInstance( } auto geometryInstance = Cesium().GeometryInstance.New({ {"geometry", geom}, - {"id", JsValue(id)}, + {"id", id}, {"attributes", attributes} }); ++numGeometryInstances_; diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index 0482061d..d27a520d 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -19,12 +19,14 @@ uint32_t fvec4ToInt(glm::fvec4 const& v) { } FeatureLayerVisualization::FeatureLayerVisualization( + std::string const& mapTileKey, const FeatureLayerStyle& style, NativeJsValue const& rawOptionValues, NativeJsValue const& rawFeatureMergeService, FeatureStyleRule::HighlightMode const& highlightMode, NativeJsValue const& rawFeatureIdSubset) - : coloredLines_(CesiumPrimitive::withPolylineColorAppearance(false)), + : mapTileKey_(mapTileKey), + coloredLines_(CesiumPrimitive::withPolylineColorAppearance(false)), coloredNontrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(false, false)), coloredTrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(true)), coloredGroundLines_(CesiumPrimitive::withPolylineColorAppearance(true)), @@ -75,12 +77,6 @@ void FeatureLayerVisualization::addTileFeatureLayer( void FeatureLayerVisualization::run() { for (auto&& feature : *tile_) { - if (!featureIdSubset_.empty()) { - if (featureIdSubset_.find(feature->id()->toString()) == featureIdSubset_.end()) { - continue; - } - } - for (auto&& rule : style_.rules()) { if (rule.mode() != highlightMode_) continue; @@ -190,7 +186,13 @@ void FeatureLayerVisualization::addFeature( model_ptr& feature, FeatureStyleRule const& rule) { - auto id = feature->id()->toString(); + auto featureId = feature->id()->toString(); + if (!featureIdSubset_.empty()) { + if (featureIdSubset_.find(featureId) == featureIdSubset_.end()) { + return; + } + } + auto offset = localWgs84UnitCoordinateSystem(feature->firstGeometry()) * rule.offset(); switch(rule.aspect()) { @@ -201,10 +203,10 @@ void FeatureLayerVisualization::addFeature( auto boundEvalFun = [this, &evaluationContext](auto&& str){return evaluateExpression(str, evaluationContext);}; feature->geom()->forEachGeometry( - [this, id, &rule, &boundEvalFun, &offset](auto&& geom) + [this, featureId, &rule, &boundEvalFun, &offset](auto&& geom) { if (rule.supports(geom->geomType())) - addGeometry(geom, id, rule, boundEvalFun, offset); + addGeometry(geom, featureId, rule, boundEvalFun, offset); return true; }); break; @@ -233,7 +235,7 @@ void FeatureLayerVisualization::addFeature( feature, layerName, attr, - id, // TODO: Rethink, how an attribute link may be encoded in the id. + featureId, // TODO: Rethink, how an attribute link may be encoded in the id. rule, offsetFactor, offset); @@ -256,12 +258,16 @@ void FeatureLayerVisualization::addGeometry( if (!rule.selectable()) id = UnselectableId; + // Combine the ID with the mapTileKey to create an + // easy link from the geometry back to the feature. + auto tileFeatureId = makeTileFeatureId(id); + std::vector vertsCartesian; vertsCartesian.reserve(geom->numPoints()); geom->forEachPoint( [&vertsCartesian, &offset](auto&& vertex) { - vertsCartesian.emplace_back(wgsToCartesian(vertex, offset)); + vertsCartesian.emplace_back(wgsToCartesian(vertex, offset)); return true; }); @@ -270,23 +276,23 @@ void FeatureLayerVisualization::addGeometry( if (vertsCartesian.size() >= 3) { auto jsVerts = encodeVerticesAsList(vertsCartesian); if (rule.flat()) - coloredGroundMeshes_.addPolygon(jsVerts, rule, id, evalFun); + coloredGroundMeshes_.addPolygon(jsVerts, rule, tileFeatureId, evalFun); else - coloredNontrivialMeshes_.addPolygon(jsVerts, rule, id, evalFun); + coloredNontrivialMeshes_.addPolygon(jsVerts, rule, tileFeatureId, evalFun); } break; case GeomType::Line: - addPolyLine(vertsCartesian, rule, id, evalFun); + addPolyLine(vertsCartesian, rule, tileFeatureId, evalFun); break; case GeomType::Mesh: if (vertsCartesian.size() >= 3) { auto jsVerts = encodeVerticesAsFloat64Array(vertsCartesian); - coloredTrivialMeshes_.addTriangles(jsVerts, rule, id, evalFun); + coloredTrivialMeshes_.addTriangles(jsVerts, rule, tileFeatureId, evalFun); } break; case GeomType::Points: for (auto const& pt : vertsCartesian) { - coloredPoints_.addPoint(JsValue(pt), rule, id, evalFun); + coloredPoints_.addPoint(JsValue(pt), rule, tileFeatureId, evalFun); } break; } @@ -298,7 +304,7 @@ void FeatureLayerVisualization::addGeometry( JsValue(wgsToCartesian(geometryCenter(geom), offset)), text, rule, - id, + tileFeatureId, evalFun); } } @@ -408,10 +414,14 @@ void erdblick::FeatureLayerVisualization::addLine( auto cartA = wgsToCartesian(wgsA, offset); auto cartB = wgsToCartesian(wgsB, offset); + // Combine the ID with the mapTileKey to create an + // easy link from the geometry back to the feature. + auto tileFeatureId = makeTileFeatureId(id); + addPolyLine( {cartA, cartB}, rule, - id, + tileFeatureId, evalFun); if (rule.hasLabel()) { @@ -421,7 +431,7 @@ void erdblick::FeatureLayerVisualization::addLine( JsValue(mapget::Point(cartA + (cartB - cartA) * labelPositionHint)), text, rule, - id, + tileFeatureId, evalFun); } } @@ -430,7 +440,7 @@ void erdblick::FeatureLayerVisualization::addLine( void FeatureLayerVisualization::addPolyLine( std::vector const& vertsCartesian, const FeatureStyleRule& rule, - std::string_view const& id, + JsValue const& tileFeatureId, BoundEvalFun const& evalFun) { if (vertsCartesian.size() < 2) @@ -441,27 +451,27 @@ void FeatureLayerVisualization::addPolyLine( if (arrowType == FeatureStyleRule::DoubleArrow) { auto jsVertsPair = encodeVerticesAsReversedSplitList(vertsCartesian); auto& primitive = getPrimitiveForArrowMaterial(rule, evalFun); - primitive.addPolyLine(jsVertsPair.first, rule, id, evalFun); - primitive.addPolyLine(jsVertsPair.second, rule, id, evalFun); + primitive.addPolyLine(jsVertsPair.first, rule, tileFeatureId, evalFun); + primitive.addPolyLine(jsVertsPair.second, rule, tileFeatureId, evalFun); return; } auto jsVerts = encodeVerticesAsList(vertsCartesian); if (arrowType == FeatureStyleRule::ForwardArrow) { - getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, id, evalFun); + getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else if (arrowType == FeatureStyleRule::BackwardArrow) { jsVerts.call("reverse"); - getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, id, evalFun); + getPrimitiveForArrowMaterial(rule, evalFun).addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else if (rule.isDashed()) { - getPrimitiveForDashMaterial(rule, evalFun).addPolyLine(jsVerts, rule, id, evalFun); + getPrimitiveForDashMaterial(rule, evalFun).addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else if (rule.flat()) { - coloredGroundLines_.addPolyLine(jsVerts, rule, id, evalFun); + coloredGroundLines_.addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } else { - coloredLines_.addPolyLine(jsVerts, rule, id, evalFun); + coloredLines_.addPolyLine(jsVerts, rule, tileFeatureId, evalFun); } } @@ -556,6 +566,14 @@ void FeatureLayerVisualization::addOptionsToSimfilContext(simfil::OverlayNode& c } } +JsValue FeatureLayerVisualization::makeTileFeatureId(const std::string_view& featureId) const +{ + return JsValue::Dict({ + {"mapTileKey", mapTileKey_}, + {"featureId", JsValue(featureId)} + }); +} + RecursiveRelationVisualizationState::RecursiveRelationVisualizationState( const FeatureStyleRule& rule, mapget::model_ptr f, From 84147ef8d7e3eb7089926118725fc9cb252348be Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Tue, 20 Aug 2024 15:17:26 +0200 Subject: [PATCH 06/37] Apply options in feature filter expression. --- libs/core/include/erdblick/rule.h | 2 +- libs/core/include/erdblick/visualization.h | 1 + libs/core/src/rule.cpp | 6 +++--- libs/core/src/visualization.cpp | 21 +++++++++++---------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index 57838639..5fb8a405 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -41,7 +41,7 @@ class FeatureStyleRule DoubleArrow }; - FeatureStyleRule const* match(mapget::Feature& feature) const; + FeatureStyleRule const* match(mapget::Feature& feature, BoundEvalFun const& evalFun) const; [[nodiscard]] Aspect aspect() const; [[nodiscard]] HighlightMode mode() const; [[nodiscard]] bool selectable() const; diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index 3c61af93..95a03e5a 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -133,6 +133,7 @@ class FeatureLayerVisualization */ void addFeature( mapget::model_ptr& feature, + BoundEvalFun const& evalFun, FeatureStyleRule const& rule); /** diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index 7bfa3fb5..70e63635 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -373,7 +373,7 @@ void FeatureStyleRule::parse(const YAML::Node& yaml) } } -FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature) const +FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature, BoundEvalFun const& evalFun) const { // Filter by feature type regular expression. if (type_) { @@ -385,7 +385,7 @@ FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature) const // Filter by simfil expression. if (!filter_.empty()) { - if (!feature.evaluate(filter_).as()) { + if (!evalFun(filter_).as()) { return nullptr; } } @@ -393,7 +393,7 @@ FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature) const // Return matching sub-rule or this. if (!firstOfRules_.empty()) { for (auto const& rule : firstOfRules_) { - if (auto matchingRule = rule.match(feature)) { + if (auto matchingRule = rule.match(feature, evalFun)) { return matchingRule; } } diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index d27a520d..2ec3c0f9 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -77,12 +77,17 @@ void FeatureLayerVisualization::addTileFeatureLayer( void FeatureLayerVisualization::run() { for (auto&& feature : *tile_) { + auto const& constFeature = static_cast(*feature); + simfil::OverlayNode evaluationContext(simfil::Value::field(constFeature)); + addOptionsToSimfilContext(evaluationContext); + auto boundEvalFun = [this, &evaluationContext](auto&& str){return evaluateExpression(str, evaluationContext);}; + for (auto&& rule : style_.rules()) { if (rule.mode() != highlightMode_) continue; - if (auto* matchingSubRule = rule.match(*feature)) { - addFeature(feature, *matchingSubRule); + if (auto* matchingSubRule = rule.match(*feature, boundEvalFun)) { + addFeature(feature, boundEvalFun, *matchingSubRule); featuresAdded_ = true; } } @@ -184,6 +189,7 @@ void FeatureLayerVisualization::processResolvedExternalReferences( void FeatureLayerVisualization::addFeature( model_ptr& feature, + BoundEvalFun const& evalFun, FeatureStyleRule const& rule) { auto featureId = feature->id()->toString(); @@ -197,16 +203,11 @@ void FeatureLayerVisualization::addFeature( switch(rule.aspect()) { case FeatureStyleRule::Feature: { - auto const& constFeature = static_cast(*feature); - simfil::OverlayNode evaluationContext(simfil::Value::field(constFeature)); - addOptionsToSimfilContext(evaluationContext); - - auto boundEvalFun = [this, &evaluationContext](auto&& str){return evaluateExpression(str, evaluationContext);}; feature->geom()->forEachGeometry( - [this, featureId, &rule, &boundEvalFun, &offset](auto&& geom) + [this, featureId, &rule, &evalFun, &offset](auto&& geom) { if (rule.supports(geom->geomType())) - addGeometry(geom, featureId, rule, boundEvalFun, offset); + addGeometry(geom, featureId, rule, evalFun, offset); return true; }); break; @@ -570,7 +571,7 @@ JsValue FeatureLayerVisualization::makeTileFeatureId(const std::string_view& fea { return JsValue::Dict({ {"mapTileKey", mapTileKey_}, - {"featureId", JsValue(featureId)} + {"featureId", JsValue(std::string(featureId))} }); } From fa5937737ef2554dd1c1908759592344216c72dc Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Tue, 20 Aug 2024 16:01:41 +0200 Subject: [PATCH 07/37] Update default style. --- config/styles/default-style.yaml | 42 +++++++++++++++++++++++++++++--- erdblick_app/app/map.service.ts | 7 +++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/config/styles/default-style.yaml b/config/styles/default-style.yaml index 1fbe89be..5fa6c793 100644 --- a/config/styles/default-style.yaml +++ b/config/styles/default-style.yaml @@ -1,15 +1,51 @@ name: DefaultStyle version: 1.0 +options: + - label: Show Meshes/Polygons + id: showMesh + - label: Show Points + id: showPoint + - label: Show Lines + id: showLine + rules: + # Normal styles - geometry: ["mesh", "polygon"] + filter: "showMesh" color: teal opacity: 0.8 - - geometry: ["point", "line"] + offset: [0, 0, -0.5] + - geometry: ["point"] + filter: "showPoint" color: moccasin opacity: 1.0 width: 1.0 - - geometry: ["point", "line", "mesh", "polygon"] + - geometry: ["line"] + filter: "showLine" + color: moccasin + opacity: 1.0 + width: 5.0 + + # Hover/Selection styles + - geometry: ["mesh", "polygon"] + color: yellow + opacity: 1.0 + width: 4.0 + mode: hover + offset: [0, 0, -0.5] + - geometry: ["point", "line"] + color: yellow + opacity: 1.0 + width: 4.0 + mode: hover + - geometry: ["mesh", "polygon"] + color: red + opacity: 1.0 + width: 4.0 + mode: selection + offset: [0, 0, -0.5] + - geometry: ["point", "line"] color: red opacity: 1.0 width: 4.0 - mode: highlight + mode: selection diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index a335cbf7..8ca982fa 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -711,13 +711,12 @@ export class MapService { return; } - if (!featureWrappers.length) { - return; - } - while (visualizationCollection.length) { this.tileVisualizationDestructionTopic.next(visualizationCollection.pop()); } + if (!featureWrappers.length) { + return; + } // Apply additional highlight styles. const featureTile = featureWrappers[0].featureTile; From 9a56356e116912dd33dcf66302ba5b9e879f8d04 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 28 Aug 2024 16:09:14 +0200 Subject: [PATCH 08/37] Finished FeatureLayerVisualization changes for merged point features. --- erdblick_app/app/map.service.ts | 3 +- erdblick_app/app/pointmerge.service.ts | 12 +- .../erdblick/cesium-interface/labels.h | 10 + .../erdblick/cesium-interface/points.h | 11 +- libs/core/include/erdblick/rule.h | 9 +- libs/core/include/erdblick/style.h | 2 + libs/core/include/erdblick/visualization.h | 38 +++- libs/core/src/bindings.cpp | 1 + libs/core/src/cesium-interface/labels.cpp | 94 +++++---- libs/core/src/cesium-interface/points.cpp | 22 +- libs/core/src/rule.cpp | 8 +- libs/core/src/style.cpp | 9 + libs/core/src/visualization.cpp | 188 +++++++++++++++--- 13 files changed, 315 insertions(+), 92 deletions(-) diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index 8ca982fa..a90d60b0 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -700,7 +700,8 @@ export class MapService { switch (mode) { case coreLib.HighlightMode.SELECTION_HIGHLIGHT: if (this.sidePanelService.panel != SidePanelState.FEATURESEARCH) { - this.sidePanelService.panel = SidePanelState.NONE; + this.sidePanelService.panel = SidePanelState.NONE +; } visualizationCollection = this.selectionVisualizations; break; diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts index aa09acdf..8aff5ba9 100644 --- a/erdblick_app/app/pointmerge.service.ts +++ b/erdblick_app/app/pointmerge.service.ts @@ -45,10 +45,16 @@ export class MergedPointsTile { } else { for (let fid in point.featureIds) { - existingPoint.featureIds.push(fid); + if (existingPoint.featureIds.findIndex(v => v == fid) == -1) { + existingPoint.featureIds.push(fid); + } + } + if (point.pointParameters !== undefined) { + existingPoint.pointParameters = point.pointParameters; + } + if (point.labelParameters !== undefined) { + existingPoint.labelParameters = point.labelParameters; } - existingPoint.pointParameters = point.pointParameters; - existingPoint.labelParameters = point.labelParameters; } } diff --git a/libs/core/include/erdblick/cesium-interface/labels.h b/libs/core/include/erdblick/cesium-interface/labels.h index 4c829961..6d216768 100644 --- a/libs/core/include/erdblick/cesium-interface/labels.h +++ b/libs/core/include/erdblick/cesium-interface/labels.h @@ -11,6 +11,16 @@ struct CesiumLabelCollection { CesiumLabelCollection(); + /** + * Get the parameter object for a call to LabelCollection.add(). + */ + JsValue labelParams( + JsValue const &position, + const std::string& labelText, + FeatureStyleRule const &style, + JsValue const& id, + BoundEvalFun const& evalFun); + /** * Add an individual label to the collection */ diff --git a/libs/core/include/erdblick/cesium-interface/points.h b/libs/core/include/erdblick/cesium-interface/points.h index f15d270f..0173e451 100644 --- a/libs/core/include/erdblick/cesium-interface/points.h +++ b/libs/core/include/erdblick/cesium-interface/points.h @@ -13,7 +13,7 @@ struct CesiumPointPrimitiveCollection CesiumPointPrimitiveCollection(); /** - * Add an individual point to the collection + * Add an individual point to the collection. */ void addPoint( const JsValue& position, @@ -21,6 +21,15 @@ struct CesiumPointPrimitiveCollection JsValue const& id, BoundEvalFun const& evalFun); + /** + * Get the parameters for a PointPrimitiveCollection::add() call. + */ + JsValue pointParams( + const JsValue& position, + FeatureStyleRule const& style, + JsValue const& id, + BoundEvalFun const& evalFun); + /** * Construct a JS Primitive from the provided Geometry instances. */ diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index 5fb8a405..86e1388c 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -2,6 +2,7 @@ #include "mapget/model/feature.h" #include "simfil/model/nodes.h" +#include "simfil/overlay.h" #include "yaml-cpp/yaml.h" #include "color.h" @@ -12,9 +13,13 @@ namespace erdblick { /** - * Simfil expression evaluation lambda, bound to a particular model node. + * Simfil expression evaluation lambda, bound to a particular context model node. */ -using BoundEvalFun = std::function; +struct BoundEvalFun +{ + simfil::OverlayNode context_; + std::function eval_; +}; class FeatureStyleRule { diff --git a/libs/core/include/erdblick/style.h b/libs/core/include/erdblick/style.h index c1555ec4..cbab1da3 100644 --- a/libs/core/include/erdblick/style.h +++ b/libs/core/include/erdblick/style.h @@ -43,11 +43,13 @@ class FeatureLayerStyle [[nodiscard]] bool isValid() const; [[nodiscard]] const std::vector& rules() const; [[nodiscard]] const std::vector& options() const; + [[nodiscard]] std::string const& name() const; private: std::vector rules_; std::vector options_; bool valid_ = false; + std::string name_; }; } \ No newline at end of file diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index 95a03e5a..00de5bef 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -127,14 +127,21 @@ class FeatureLayerVisualization */ [[nodiscard]] NativeJsValue primitiveCollection() const; + /** + * Returns all merged point features as a dict form mapLayerStyleRuleId + * to MergedPointVisualization primitives. + */ + [[nodiscard]] NativeJsValue mergedPointFeatures() const; + private: /** * Add all geometry of some feature which is compatible with the given rule. */ void addFeature( mapget::model_ptr& feature, - BoundEvalFun const& evalFun, - FeatureStyleRule const& rule); + BoundEvalFun& evalFun, + FeatureStyleRule const& rule, + std::string const& mapLayerStyleRuleId); /** * Visualize an attribute. @@ -145,6 +152,7 @@ class FeatureLayerVisualization mapget::model_ptr const& attr, std::string_view const& id, const FeatureStyleRule& rule, + std::string const& mapLayerStyleRuleId, uint32_t& offsetFactor, glm::dvec3 const& offset); @@ -156,7 +164,8 @@ class FeatureLayerVisualization mapget::model_ptr const& geom, std::string_view id, FeatureStyleRule const& rule, - BoundEvalFun const& evalFun, + std::string const& mapLayerStyleRuleId, + BoundEvalFun& evalFun, glm::dvec3 const& offset = {.0, .0, .0}); /** @@ -169,7 +178,7 @@ class FeatureLayerVisualization mapget::Point const& wgsB, std::string_view const& id, FeatureStyleRule const& rule, - BoundEvalFun const& evalFun, + BoundEvalFun& evalFun, glm::dvec3 const& offset, double labelPositionHint=0.5); @@ -180,9 +189,21 @@ class FeatureLayerVisualization std::vector const& vertsCartesian, const FeatureStyleRule& rule, JsValue const& id, - BoundEvalFun const& evalFun); + BoundEvalFun& evalFun); - /** + /** + * Add a merged point feature. + */ + void addMergedPointGeometry( + const std::string_view& id, + const std::string& mapLayerStyleRuleId, + const std::optional& gridCellSize, + mapget::Point const& pointCartographic, + const char* geomField, + BoundEvalFun& evalFun, + std::function const& makeGeomParams); + + /** * Get some cartesian points as a list of Cesium Cartesian points. */ static JsValue encodeVerticesAsList(std::vector const& points); @@ -202,13 +223,13 @@ class FeatureLayerVisualization * Get an initialised primitive for a particular PolylineDashMaterialAppearance. */ CesiumPrimitive& - getPrimitiveForDashMaterial(const FeatureStyleRule& rule, BoundEvalFun const& evalFun); + getPrimitiveForDashMaterial(const FeatureStyleRule& rule, BoundEvalFun& evalFun); /** * Get an initialised primitive for a particular PolylineArrowMaterialAppearance. */ CesiumPrimitive& - getPrimitiveForArrowMaterial(const FeatureStyleRule& rule, BoundEvalFun const& evalFun); + getPrimitiveForArrowMaterial(const FeatureStyleRule& rule, BoundEvalFun& evalFun); /** * Simfil expression evaluation function for the tile which this visualization belongs to. @@ -240,6 +261,7 @@ class FeatureLayerVisualization CesiumPrimitive coloredGroundMeshes_; CesiumPointPrimitiveCollection coloredPoints_; CesiumLabelCollection labelCollection_; + std::map mergedPointsPerStyleRuleId_; JsValue featureMergeService_; FeatureLayerStyle const& style_; diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index aeb165e2..ded98ff4 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -387,6 +387,7 @@ EMSCRIPTEN_BINDINGS(erdblick) .function("addTileFeatureLayer", &FeatureLayerVisualization::addTileFeatureLayer) .function("run", &FeatureLayerVisualization::run) .function("primitiveCollection", &FeatureLayerVisualization::primitiveCollection) + .function("mergedPointFeatures", &FeatureLayerVisualization::mergedPointFeatures) .function("externalReferences", &FeatureLayerVisualization::externalReferences) .function("processResolvedExternalReferences", &FeatureLayerVisualization::processResolvedExternalReferences); diff --git a/libs/core/src/cesium-interface/labels.cpp b/libs/core/src/cesium-interface/labels.cpp index 670db595..216f7c11 100644 --- a/libs/core/src/cesium-interface/labels.cpp +++ b/libs/core/src/cesium-interface/labels.cpp @@ -9,60 +9,80 @@ namespace erdblick { CesiumLabelCollection::CesiumLabelCollection() : labelCollection_(Cesium().LabelCollection.New()) {} -void CesiumLabelCollection::addLabel( - JsValue const &position, - const std::string &labelText, - FeatureStyleRule const &style, - JsValue const& id, - BoundEvalFun const& evalFun) { +JsValue CesiumLabelCollection::labelParams( + const JsValue& position, + const std::string& labelText, + const FeatureStyleRule& style, + const JsValue& id, + const BoundEvalFun& evalFun) +{ auto const &color = style.labelColor(); auto const &outlineColor = style.labelOutlineColor(); auto const &bgColor = style.labelBackgroundColor(); auto const &padding = style.labelBackgroundPadding(); auto labelProperties = JsValue::Dict({ - {"id", id}, - {"position", position}, - {"show", JsValue(true)}, - {"text", JsValue(labelText)}, - {"font", JsValue(style.labelFont())}, - {"disableDepthTestDistance", JsValue(std::numeric_limits::infinity())}, - {"fillColor", Cesium().Color.New(color.r, color.g, color.b, color.a)}, - {"outlineColor", Cesium().Color.New(outlineColor.r, outlineColor.g, outlineColor.b, outlineColor.a)}, - {"outlineWidth", JsValue(style.labelOutlineWidth())}, - {"showBackground", JsValue(style.showBackground())}, - {"backgroundColor", Cesium().Color.New(bgColor.r, bgColor.g, bgColor.b, bgColor.a)}, - {"backgroundPadding", Cesium().Cartesian2.New(padding.first, padding.second)}, - {"style", Cesium().LabelStyle[style.labelStyle()]}, - {"horizontalOrigin", Cesium().HorizontalOrigin[style.labelHorizontalOrigin()]}, - {"verticalOrigin", Cesium().VerticalOrigin[style.labelVerticalOrigin()]}, - {"scale", JsValue(style.labelScale())} + {"id", id}, + {"position", position}, + {"show", JsValue(true)}, + {"text", JsValue(labelText)}, + {"font", JsValue(style.labelFont())}, + {"disableDepthTestDistance", JsValue(std::numeric_limits::infinity())}, + {"fillColor", Cesium().Color.New(color.r, color.g, color.b, color.a)}, + {"outlineColor", Cesium().Color.New(outlineColor.r, outlineColor.g, outlineColor.b, outlineColor.a)}, + {"outlineWidth", JsValue(style.labelOutlineWidth())}, + {"showBackground", JsValue(style.showBackground())}, + {"backgroundColor", Cesium().Color.New(bgColor.r, bgColor.g, bgColor.b, bgColor.a)}, + {"backgroundPadding", Cesium().Cartesian2.New(padding.first, padding.second)}, + {"style", Cesium().LabelStyle[style.labelStyle()]}, + {"horizontalOrigin", Cesium().HorizontalOrigin[style.labelHorizontalOrigin()]}, + {"verticalOrigin", Cesium().VerticalOrigin[style.labelVerticalOrigin()]}, + {"scale", JsValue(style.labelScale())} }); - if (auto const &sbd = style.scaleByDistance()) { - labelProperties.set("scaleByDistance", + if (auto const& sbd = style.scaleByDistance()) { + labelProperties.set( + "scaleByDistance", Cesium().NearFarScalar.New((*sbd)[0], (*sbd)[1], (*sbd)[2], (*sbd)[3])); - } else if (auto const &nfs = style.nearFarScale() ) { - labelProperties.set("scaleByDistance", + } + else if (auto const& nfs = style.nearFarScale()) { + labelProperties.set( + "scaleByDistance", Cesium().NearFarScalar.New((*nfs)[0], (*nfs)[1], (*nfs)[2], (*nfs)[3])); } - if (auto const &osbd = style.offsetScaleByDistance() ) { - labelProperties.set("pixelOffsetScaleByDistance", + if (auto const& osbd = style.offsetScaleByDistance()) { + labelProperties.set( + "pixelOffsetScaleByDistance", Cesium().NearFarScalar.New((*osbd)[0], (*osbd)[1], (*osbd)[2], (*osbd)[3])); } - if (auto const &pixelOffset = style.labelPixelOffset()) { - labelProperties.set("pixelOffset", - Cesium().Cartesian2.New(pixelOffset->first, pixelOffset->second)); + if (auto const& pixelOffset = style.labelPixelOffset()) { + labelProperties + .set("pixelOffset", Cesium().Cartesian2.New(pixelOffset->first, pixelOffset->second)); } - if (auto const &eyeOffset = style.labelEyeOffset()) { - labelProperties.set("eyeOffset", - Cesium().Cartesian3.New(std::get<0>(*eyeOffset),std::get<1>(*eyeOffset),std::get<2>(*eyeOffset))); + if (auto const& eyeOffset = style.labelEyeOffset()) { + labelProperties.set( + "eyeOffset", + Cesium() + .Cartesian3 + .New(std::get<0>(*eyeOffset), std::get<1>(*eyeOffset), std::get<2>(*eyeOffset))); } - if (auto const &tbd = style.translucencyByDistance()) { - labelProperties.set("translucencyByDistance", + if (auto const& tbd = style.translucencyByDistance()) { + labelProperties.set( + "translucencyByDistance", Cesium().NearFarScalar.New((*tbd)[0], (*tbd)[1], (*tbd)[2], (*tbd)[3])); } - labelCollection_.call("add", *labelProperties); + return labelProperties; +} + +void CesiumLabelCollection::addLabel( + JsValue const &position, + const std::string &labelText, + FeatureStyleRule const &style, + JsValue const& id, + BoundEvalFun const& evalFun) +{ + auto params = labelParams(position, labelText, style, id, evalFun); + labelCollection_.call("add", *params); numLabelInstances_++; } diff --git a/libs/core/src/cesium-interface/points.cpp b/libs/core/src/cesium-interface/points.cpp index cad7128a..3a3470a1 100644 --- a/libs/core/src/cesium-interface/points.cpp +++ b/libs/core/src/cesium-interface/points.cpp @@ -12,11 +12,11 @@ CesiumPointPrimitiveCollection::CesiumPointPrimitiveCollection() : pointPrimitiveCollection_(Cesium().PointPrimitiveCollection.New()) {} -void CesiumPointPrimitiveCollection::addPoint( +JsValue CesiumPointPrimitiveCollection::pointParams( const JsValue& position, - FeatureStyleRule const& style, - JsValue const& id, - BoundEvalFun const& evalFun) + const FeatureStyleRule& style, + const JsValue& id, + const BoundEvalFun& evalFun) { auto const color = style.color(evalFun); auto const& oColor = style.outlineColor(); @@ -36,7 +36,17 @@ void CesiumPointPrimitiveCollection::addPoint( Cesium().NearFarScalar.New((*nfs)[0], (*nfs)[1], (*nfs)[2], (*nfs)[3])); } - pointPrimitiveCollection_.call("add", *options); + return options; +} + +void CesiumPointPrimitiveCollection::addPoint( + const JsValue& position, + FeatureStyleRule const& style, + JsValue const& id, + BoundEvalFun const& evalFun) +{ + auto params = pointParams(position, style, id, evalFun); + pointPrimitiveCollection_.call("add", *params); ++numGeometryInstances_; } @@ -50,4 +60,4 @@ bool CesiumPointPrimitiveCollection::empty() const return numGeometryInstances_ == 0; } -} \ No newline at end of file +} diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index 70e63635..59aad48e 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -385,7 +385,7 @@ FeatureStyleRule const* FeatureStyleRule::match(mapget::Feature& feature, BoundE // Filter by simfil expression. if (!filter_.empty()) { - if (!evalFun(filter_).as()) { + if (!evalFun.eval_(filter_).as()) { return nullptr; } } @@ -411,7 +411,7 @@ bool FeatureStyleRule::supports(const mapget::GeomType& g) const glm::fvec4 FeatureStyleRule::color(BoundEvalFun const& evalFun) const { if (!colorExpression_.empty()) { - auto colorVal = evalFun(colorExpression_); + auto colorVal = evalFun.eval_(colorExpression_); if (colorVal.isa(simfil::ValueType::Int)) { auto colorInt = colorVal.as(); auto a = static_cast(colorInt & 0xff) / 255.; @@ -467,7 +467,7 @@ int FeatureStyleRule::dashPattern() const FeatureStyleRule::Arrow FeatureStyleRule::arrow(BoundEvalFun const& evalFun) const { if (!arrowExpression_.empty()) { - auto arrowVal = evalFun(arrowExpression_); + auto arrowVal = evalFun.eval_(arrowExpression_); if (arrowVal.isa(simfil::ValueType::String)) { auto arrowStr = arrowVal.as(); if (auto arrowMode = parseArrowMode(arrowStr)) @@ -608,7 +608,7 @@ std::string const& FeatureStyleRule::labelTextExpression() const std::string FeatureStyleRule::labelText(BoundEvalFun const& evalFun) const { if (!labelTextExpression_.empty()) { - auto resultVal = evalFun(labelTextExpression_); + auto resultVal = evalFun.eval_(labelTextExpression_); auto resultText = resultVal.toString(); if (!resultText.empty()) { return resultText; diff --git a/libs/core/src/style.cpp b/libs/core/src/style.cpp index 42ab0f44..ab882f9f 100644 --- a/libs/core/src/style.cpp +++ b/libs/core/src/style.cpp @@ -17,6 +17,11 @@ FeatureLayerStyle::FeatureLayerStyle(SharedUint8Array const& yamlArray) // Convert char vector to YAML node. auto styleYaml = YAML::Load(styleSpec); + if (auto name = styleYaml["name"]) { + if (name.IsScalar()) + name_ = name.Scalar(); + } + if (!styleYaml["rules"] || !(styleYaml["rules"].IsSequence())) { std::cout << "YAML stylesheet error: Spec does not contain any rules?" << std::endl; return; @@ -52,6 +57,10 @@ const std::vector& FeatureLayerStyle::options() const return options_; } +std::string const& FeatureLayerStyle::name() const { + return name_; +} + FeatureStyleOption::FeatureStyleOption(const YAML::Node& yaml) { if (auto node = yaml["label"]) { diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index 2ec3c0f9..a4b78037 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -80,16 +80,32 @@ void FeatureLayerVisualization::run() auto const& constFeature = static_cast(*feature); simfil::OverlayNode evaluationContext(simfil::Value::field(constFeature)); addOptionsToSimfilContext(evaluationContext); - auto boundEvalFun = [this, &evaluationContext](auto&& str){return evaluateExpression(str, evaluationContext);}; + auto boundEvalFun = BoundEvalFun{ + evaluationContext, + [this, &evaluationContext](auto&& str) + { + return evaluateExpression(str, evaluationContext); + }}; + uint32_t ruleIndex = 0; for (auto&& rule : style_.rules()) { - if (rule.mode() != highlightMode_) + if (rule.mode() != highlightMode_) { + ++ruleIndex; continue; + } + + auto mapLayerStyleRuleId = fmt::format( + "{}:{}:{}:{}", + tile_->mapId(), + tile_->layerInfo()->layerId_, + style_.name(), + ruleIndex); if (auto* matchingSubRule = rule.match(*feature, boundEvalFun)) { - addFeature(feature, boundEvalFun, *matchingSubRule); + addFeature(feature, boundEvalFun, *matchingSubRule, mapLayerStyleRuleId); featuresAdded_ = true; } + ++ruleIndex; } } } @@ -132,6 +148,15 @@ NativeJsValue FeatureLayerVisualization::primitiveCollection() const return *collection; } +NativeJsValue FeatureLayerVisualization::mergedPointFeatures() const +{ + auto result = JsValue::Dict(); + for (auto const& [mapLayerStyleRuleId, primitives] : mergedPointsPerStyleRuleId_) { + result.set(mapLayerStyleRuleId, primitives); + } + return *result; +} + NativeJsValue FeatureLayerVisualization::externalReferences() { return *externalRelationReferences_; @@ -189,8 +214,9 @@ void FeatureLayerVisualization::processResolvedExternalReferences( void FeatureLayerVisualization::addFeature( model_ptr& feature, - BoundEvalFun const& evalFun, - FeatureStyleRule const& rule) + BoundEvalFun& evalFun, + FeatureStyleRule const& rule, + std::string const& mapLayerStyleRuleId) { auto featureId = feature->id()->toString(); if (!featureIdSubset_.empty()) { @@ -204,10 +230,10 @@ void FeatureLayerVisualization::addFeature( switch(rule.aspect()) { case FeatureStyleRule::Feature: { feature->geom()->forEachGeometry( - [this, featureId, &rule, &evalFun, &offset](auto&& geom) + [this, featureId, &rule, &mapLayerStyleRuleId, &evalFun, &offset](auto&& geom) { if (rule.supports(geom->geomType())) - addGeometry(geom, featureId, rule, evalFun, offset); + addGeometry(geom, featureId, rule, mapLayerStyleRuleId, evalFun, offset); return true; }); break; @@ -236,8 +262,9 @@ void FeatureLayerVisualization::addFeature( feature, layerName, attr, - featureId, // TODO: Rethink, how an attribute link may be encoded in the id. + featureId, // TODO: Rethink, how an attribute link may be encoded in the id. rule, + mapLayerStyleRuleId, offsetFactor, offset); return true; @@ -253,7 +280,8 @@ void FeatureLayerVisualization::addGeometry( model_ptr const& geom, std::string_view id, FeatureStyleRule const& rule, - BoundEvalFun const& evalFun, + std::string const& mapLayerStyleRuleId, + BoundEvalFun& evalFun, glm::dvec3 const& offset) { if (!rule.selectable()) @@ -292,8 +320,28 @@ void FeatureLayerVisualization::addGeometry( } break; case GeomType::Points: + auto pointIndex = 0; for (auto const& pt : vertsCartesian) { - coloredPoints_.addPoint(JsValue(pt), rule, tileFeatureId, evalFun); + + // If a merge-grid cell size is set, then a merged feature representation was requested. + if (auto const& gridCellSize = rule.pointMergeGridCellSize()) { + addMergedPointGeometry( + id, + mapLayerStyleRuleId, + gridCellSize, + geom->pointAt(pointIndex), + "pointParameters", + evalFun, + [&](auto& augmentedEvalFun) + { + return coloredPoints_ + .pointParams(JsValue(pt), rule, tileFeatureId, augmentedEvalFun); + }); + } + else + coloredPoints_.addPoint(JsValue(pt), rule, tileFeatureId, evalFun); + + ++pointIndex; } break; } @@ -301,16 +349,90 @@ void FeatureLayerVisualization::addGeometry( if (rule.hasLabel()) { auto text = rule.labelText(evalFun); if (!text.empty()) { - labelCollection_.addLabel( - JsValue(wgsToCartesian(geometryCenter(geom), offset)), - text, - rule, - tileFeatureId, - evalFun); + auto wgsPos = geometryCenter(geom); + auto xyzPos = JsValue(wgsToCartesian(wgsPos, offset)); + + // If a merge-grid cell size is set, then a merged feature representation was requested. + if (auto const& gridCellSize = rule.pointMergeGridCellSize()) { + addMergedPointGeometry( + id, + mapLayerStyleRuleId, + gridCellSize, + wgsPos, + "pointParameters", + evalFun, + [&](auto& augmentedEvalFun) + { + return labelCollection_.labelParams( + xyzPos, + text, + rule, + tileFeatureId, + augmentedEvalFun); + }); + } + else + labelCollection_.addLabel( + xyzPos, + text, + rule, + tileFeatureId, + evalFun); } } } +void FeatureLayerVisualization::addMergedPointGeometry( + const std::string_view& id, + const std::string& mapLayerStyleRuleId, + const std::optional& gridCellSize, + mapget::Point const& pointCartographic, + const char* geomField, + BoundEvalFun& evalFun, + std::function const& makeGeomParams) +{ + // Check if the corner tile for the cartographic position is still accepting + // contributions from this tile. + if (!featureMergeService_.call("wants", pointCartographic, tile_->tileId().value_, mapLayerStyleRuleId)) + return; + + // Convert the cartographic point to an integer representation, based + // on the grid cell size set in the style sheet. + auto gridPosition = pointCartographic / *gridCellSize; + auto gridPositionHash = fmt::format( + "{}:{}:{}", + static_cast(glm::floor(gridPosition.x)), + static_cast(glm::floor(gridPosition.y)), + static_cast(glm::floor(gridPosition.z))); + + // Add the $mergeCount variable to the evaluation context. + // This variable indicates, how many features from other tiles have already been added + // for the given grid position. + evalFun.context_.set( + internalStringPoolCopy_->emplace("$mergeCount"), + simfil::Value(featureMergeService_.call( + "count", + pointCartographic, + gridPositionHash, + tile_->tileId().z(), + mapLayerStyleRuleId))); + + // Ensure that there is a list of merged points for this mapLayerStyleRuleId. + auto [pointsForStyleRuleIdIt, wasInserted] = + mergedPointsPerStyleRuleId_.emplace(mapLayerStyleRuleId, JsValue()); + if (wasInserted) { + pointsForStyleRuleIdIt->second = JsValue::List(); + } + + // Add a MergedPointVisualization to the list. + pointsForStyleRuleIdIt->second.push(JsValue::Dict({ + {"position", JsValue(pointCartographic)}, + {"positionHash", JsValue(gridPositionHash)}, + {geomField, JsValue(makeGeomParams(evalFun))}, + {"featureIds", JsValue::List({JsValue(std::string(id))})}, + })); +} + JsValue FeatureLayerVisualization::encodeVerticesAsList(std::vector const& pointsCartesian) { @@ -367,7 +489,7 @@ FeatureLayerVisualization::encodeVerticesAsFloat64Array(std::vector const& vertsCartesian, const FeatureStyleRule& rule, JsValue const& tileFeatureId, - BoundEvalFun const& evalFun) + BoundEvalFun& evalFun) { if (vertsCartesian.size() < 2) return; @@ -502,6 +624,7 @@ void FeatureLayerVisualization::addAttribute( model_ptr const& attr, std::string_view const& id, const FeatureStyleRule& rule, + std::string const& mapLayerStyleRuleId, uint32_t& offsetFactor, glm::dvec3 const& offset) { @@ -542,10 +665,12 @@ void FeatureLayerVisualization::addAttribute( simfil::Value(layer)); // Function which can evaluate a simfil expression in the attribute context. - auto boundEvalFun = [this, &attrEvaluationContext](auto&& str) - { - return evaluateExpression(str, attrEvaluationContext); - }; + auto boundEvalFun = BoundEvalFun{ + attrEvaluationContext, + [this, &attrEvaluationContext](auto&& str) + { + return evaluateExpression(str, attrEvaluationContext); + }}; // Bump visual offset factor for next visualized attribute. ++offsetFactor; @@ -556,6 +681,7 @@ void FeatureLayerVisualization::addAttribute( geom, id, rule, + mapLayerStyleRuleId, boundEvalFun, offset * static_cast(offsetFactor)); } @@ -703,10 +829,12 @@ void RecursiveRelationVisualizationState::render( simfil::Value(r.twoway_)); // Function which can evaluate a simfil expression in the relation context. - auto boundEvalFun = [this, &relationEvaluationContext](auto&& str) - { - return visu_.evaluateExpression(str, relationEvaluationContext); - }; + auto boundEvalFun = BoundEvalFun{ + relationEvaluationContext, + [this, &relationEvaluationContext](auto&& str) + { + return visu_.evaluateExpression(str, relationEvaluationContext); + }}; // Obtain source/target geometries. auto sourceGeom = r.relation_->hasSourceValidity() ? @@ -759,14 +887,14 @@ void RecursiveRelationVisualizationState::render( // Run source geometry visualization. if (sourceGeom && visualizedFeatures_.emplace(sourceId).second) { if (auto sourceRule = rule_.relationSourceStyle()) { - visu_.addGeometry(sourceGeom, UnselectableId, *sourceRule, boundEvalFun, offsetBase * sourceRule->offset()); + visu_.addGeometry(sourceGeom, UnselectableId, *sourceRule, "", boundEvalFun, offsetBase * sourceRule->offset()); } } // Run target geometry visualization. if (targetGeom && visualizedFeatures_.emplace(targetId).second) { if (auto targetRule = rule_.relationTargetStyle()) { - visu_.addGeometry(targetGeom, UnselectableId, *targetRule, boundEvalFun, offsetBase * targetRule->offset()); + visu_.addGeometry(targetGeom, UnselectableId, *targetRule, "", boundEvalFun, offsetBase * targetRule->offset()); } } From 019c6368cd58c6e918957a262f273c69a57e487e Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 29 Aug 2024 17:24:48 +0200 Subject: [PATCH 09/37] Fix picking for merged features. --- erdblick_app/app/map.service.ts | 2 +- erdblick_app/app/pointmerge.service.ts | 11 +++-- erdblick_app/app/view.component.ts | 54 +++++++++++++++++++------ erdblick_app/app/visualization.model.ts | 2 +- libs/core/src/visualization.cpp | 2 +- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index a90d60b0..e2c21077 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -16,7 +16,7 @@ import {PointMergeService} from "./pointmerge.service"; * Combination of a tile id and a feature id, which may be resolved * to a feature object. */ -interface TileFeatureId { +export interface TileFeatureId { featureId: string, mapTileKey: string, } diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts index 8aff5ba9..2f028fd5 100644 --- a/erdblick_app/app/pointmerge.service.ts +++ b/erdblick_app/app/pointmerge.service.ts @@ -1,6 +1,7 @@ import {Injectable} from "@angular/core"; import {PointPrimitiveCollection, LabelCollection, Viewer} from "./cesium"; import {coreLib} from "./wasm"; +import {TileFeatureId} from "./map.service"; type MapLayerStyleRule = string; type PositionHash = string; @@ -12,12 +13,12 @@ type Cartographic = {x: number, y: number, z: number}; * To this end, the visualization retains visualization parameters for * calls to either/both Cesium PointPrimitiveCollection.add() and/or LabelCollection.add(). */ -interface MergedPointVisualization { +export interface MergedPointVisualization { position: Cartographic, positionHash: PositionHash, pointParameters?: Record|null, // Point Visualization Parameters for call to PointPrimitiveCollection.add(). labelParameters?: Record|null, // Label Visualization Parameters for call to LabelCollection.add(). - featureIds: Array + featureIds: Array } /** @@ -44,8 +45,8 @@ export class MergedPointsTile { this.features.set(point.positionHash, point); } else { - for (let fid in point.featureIds) { - if (existingPoint.featureIds.findIndex(v => v == fid) == -1) { + for (let fid of point.featureIds) { + if (existingPoint.featureIds.findIndex(v => v.featureId == fid.featureId) == -1) { existingPoint.featureIds.push(fid); } } @@ -72,10 +73,12 @@ export class MergedPointsTile { for (let [_, feature] of this.features) { if (feature.pointParameters) { + feature.pointParameters["id"] = feature.featureIds; this.pointPrimitives.add(feature.pointParameters); feature.pointParameters = null; } if (feature.labelParameters) { + feature.labelParameters["id"] = feature.featureIds; this.labelPrimitives.add(feature.labelParameters); feature.labelParameters = null; } diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index a5f6fed1..e9894f6a 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -37,6 +37,13 @@ import {KeyboardService} from "./keyboard.service"; // Redeclare window with extended interface declare let window: DebugWindow; +/** + * Determine if two lists of feature wrappers have the same features. + */ +function featureSetsEqual(rhs: FeatureWrapper[], lhs: FeatureWrapper[]) { + return rhs.length === lhs.length && rhs.every(rf => lhs.some(lf => rf.equals(lf))); +} + @Component({ selector: 'erdblick-view', template: ` @@ -270,42 +277,63 @@ export class ErdblickViewComponent implements AfterViewInit { * Set or re-set the hovered feature. */ private setHoveredCesiumFeature(feature: any) { - // Get the actual mapget feature for the picked Cesium feature. - let resolvedFeature = feature ? this.mapService.resolveFeature(feature.id) : null; - if (!resolvedFeature) { - this.mapService.hoverTopic.next([]); + // Get the actual mapget features for the picked Cesium feature. + let resolvedFeatures = this.resolveMapgetFeatures(feature); + if (!resolvedFeatures.length) { + this.mapService.selectionTopic.next([]); return; } - if (this.mapService.selectionTopic.getValue().some(f => resolvedFeature!.equals(f))) { + if (featureSetsEqual(this.mapService.selectionTopic.getValue(), resolvedFeatures)) { return; } - if (this.mapService.hoverTopic.getValue().some(f => resolvedFeature!.equals(f))) { + if (featureSetsEqual(this.mapService.hoverTopic.getValue(), resolvedFeatures)) { return; } - this.mapService.hoverTopic.next([resolvedFeature]); + this.mapService.hoverTopic.next(resolvedFeatures); } /** * Set or re-set the picked feature. */ private setPickedCesiumFeature(feature: any) { - // Get the actual mapget feature for the picked Cesium feature. - let resolvedFeature = feature ? this.mapService.resolveFeature(feature.id) : null; - if (!resolvedFeature) { + // Get the actual mapget features for the picked Cesium feature. + let resolvedFeatures = this.resolveMapgetFeatures(feature); + if (!resolvedFeatures.length) { this.mapService.selectionTopic.next([]); return; } - if (this.mapService.selectionTopic.getValue().some(f => resolvedFeature!.equals(f))) { + if (featureSetsEqual(this.mapService.selectionTopic.getValue(), resolvedFeatures)) { return; } - if (this.mapService.hoverTopic.getValue().some(f => resolvedFeature!.equals(f))) { + if (featureSetsEqual(this.mapService.hoverTopic.getValue(), resolvedFeatures)) { this.setHoveredCesiumFeature(null); } - this.mapService.selectionTopic.next([resolvedFeature]); + this.mapService.selectionTopic.next(resolvedFeatures); + } + + /** + * Resolve a Cesium primitive feature ID to a list of mapget FeatureWrappers. + */ + private resolveMapgetFeatures(feature: any) { + let resolvedFeatures: FeatureWrapper[] = []; + if (Array.isArray(feature?.id)) { + for (const fid of feature.id) { + const resolvedFeature = this.mapService.resolveFeature(feature.id); + if (resolvedFeature) { + resolvedFeatures = [resolvedFeature]; + } + } + } else if (feature) { + const resolvedFeature = this.mapService.resolveFeature(feature.id); + if (resolvedFeature) { + resolvedFeatures = [resolvedFeature]; + } + } + return resolvedFeatures; } /** diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index 73921929..423c9a21 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -10,7 +10,7 @@ import { HeightReference } from "./cesium"; import {FeatureLayerStyle, TileFeatureLayer, HighlightMode} from "../../build/libs/core/erdblick-core"; -import {PointMergeService} from "./pointmerge.service"; +import {MergedPointVisualization, PointMergeService} from "./pointmerge.service"; export interface LocateResolution { tileId: string, diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index a4b78037..0b65788f 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -429,7 +429,7 @@ void FeatureLayerVisualization::addMergedPointGeometry( {"position", JsValue(pointCartographic)}, {"positionHash", JsValue(gridPositionHash)}, {geomField, JsValue(makeGeomParams(evalFun))}, - {"featureIds", JsValue::List({JsValue(std::string(id))})}, + {"featureIds", JsValue::List({makeTileFeatureId(id)})}, })); } From 424d3b69605e42ba7038c7440d38f198220e0bb8 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 29 Aug 2024 17:25:17 +0200 Subject: [PATCH 10/37] Fix merged feature tile contributions. --- erdblick_app/app/visualization.model.ts | 9 +++++++++ libs/core/src/bindings.cpp | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index 423c9a21..15084d91 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -233,7 +233,11 @@ export class TileVisualization { } }); } + this.primitiveCollection = visualization.primitiveCollection(); + for (const [mapLayerStyleRuleId, mergedPointVisualizations] of Object.entries(visualization.mergedPointFeatures())) { + this.pointMergeService.insert(mergedPointVisualizations as MergedPointVisualization[], this.tile.tileId, mapLayerStyleRuleId); + } visualization.delete(); return true; }); @@ -265,6 +269,11 @@ export class TileVisualization { return; } + // Remove point-merge contributions that were made by this map-layer+style visualization combo. + this.pointMergeService.remove( + this.tile.tileId, + `${this.tile.mapName}:${this.tile.layerName}:${this.style.name()}`); + if (this.primitiveCollection) { viewer.scene.primitives.remove(this.primitiveCollection); if (!this.primitiveCollection.isDestroyed()) diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index ded98ff4..d8e460a5 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -255,7 +255,8 @@ EMSCRIPTEN_BINDINGS(erdblick) ////////// FeatureLayerStyle em::register_vector("FeatureStyleOptions"); em::class_("FeatureLayerStyle").constructor() - .function("options", &FeatureLayerStyle::options, em::allow_raw_pointers()); + .function("options", &FeatureLayerStyle::options, em::allow_raw_pointers()) + .function("name", &FeatureLayerStyle::name); ////////// SourceDataAddressFormat em::enum_("SourceDataAddressFormat") From 337b284ae132d351e42c5160c48bab06865799fb Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Sat, 31 Aug 2024 16:15:59 +0200 Subject: [PATCH 11/37] Run ctest in CI. --- .github/workflows/build-release.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1820413b..8f2cb5a5 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -38,3 +38,24 @@ jobs: name: erdblick path: | static/* + + build-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install build dependencies + run: sudo apt-get install ninja-build + + - name: Compile + run: | + mkdir build + cd build + cmake -GNinja -DCMAKE_BUILD_TYPE=Debug .. + cmake --build . + + - name: Run Tests + run: | + cd build + ctest --verbose From e8674328fd12a73c2737a36ec0d116cceec833ed Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Sat, 31 Aug 2024 17:33:40 +0200 Subject: [PATCH 12/37] Essential bugfixes for merged feature inspection and presentation. --- erdblick_app/app/feature.panel.component.ts | 16 +-- erdblick_app/app/feature.search.component.ts | 4 +- .../app/inspection.panel.component.ts | 2 +- erdblick_app/app/inspection.service.ts | 52 ++++---- erdblick_app/app/map.service.ts | 6 +- erdblick_app/app/pointmerge.service.ts | 118 ++++++++++++++---- erdblick_app/app/view.component.ts | 36 ++---- erdblick_app/app/visualization.model.ts | 25 +++- .../erdblick/cesium-interface/object.h | 5 + libs/core/include/erdblick/rule.h | 7 +- libs/core/include/erdblick/visualization.h | 8 +- libs/core/src/cesium-interface/object.cpp | 9 ++ libs/core/src/rule.cpp | 7 +- libs/core/src/style.cpp | 3 +- libs/core/src/visualization.cpp | 86 ++++++++----- 15 files changed, 258 insertions(+), 126 deletions(-) diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index 08bdf54a..cb80bc7e 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -29,17 +29,17 @@ interface Column { class="pi pi-times clear-icon" style="cursor: pointer">
- + loupe
- +
@@ -346,7 +346,7 @@ export class FeaturePanelComponent implements OnInit { const tileId = sourceDataRef.tileId; const address = sourceDataRef.address; const mapId = this.inspectionService.selectedMapIdName; - const featureId = this.inspectionService.selectedFeatureIdName; + const featureId = this.inspectionService.selectedFeatureIdNames.join(", "); this.inspectionService.selectedSourceData.next({ tileId: Number(tileId), diff --git a/erdblick_app/app/feature.search.component.ts b/erdblick_app/app/feature.search.component.ts index 136f54be..bc06dc3a 100644 --- a/erdblick_app/app/feature.search.component.ts +++ b/erdblick_app/app/feature.search.component.ts @@ -108,8 +108,8 @@ export class FeatureSearchComponent { selectResult(event: any) { if (event.value.mapId && event.value.featureId) { this.jumpService.selectFeature(event.value.mapId, event.value.featureId).then(() => { - if (this.inspectionService.selectedFeature) { - this.mapService.focusOnFeature(this.inspectionService.selectedFeature); + if (this.inspectionService.selectedFeatures.length) { + this.mapService.focusOnFeature(this.inspectionService.selectedFeatures[0]); } }); } diff --git a/erdblick_app/app/inspection.panel.component.ts b/erdblick_app/app/inspection.panel.component.ts index 8fa3a7d8..86a3dbcc 100644 --- a/erdblick_app/app/inspection.panel.component.ts +++ b/erdblick_app/app/inspection.panel.component.ts @@ -70,7 +70,7 @@ export class InspectionPanelComponent // TODO: Create a new FeaturePanelComponent instance for each unique selected feature // then we can get rid of all the service's View Component logic/functions. // reset() Would then completely clear the tabs. - const featureId = this.inspectionService.selectedFeatureIdName; + const featureId = this.inspectionService.selectedFeatureIdNames.join(", "); this.tabs[0].title = featureId; const selectedSourceData = parameterService.getSelectedSourceData() diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index e9717515..7839fd0a 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -44,15 +44,15 @@ export class InspectionService { featureTree: BehaviorSubject = new BehaviorSubject(""); featureTreeFilterValue: string = ""; isInspectionPanelVisible: boolean = false; - selectedFeatureGeoJsonText: string = ""; - selectedFeatureInspectionModel: Array | null = null; - selectedFeatureIdName: string = ""; + selectedFeatureGeoJsonTexts: string[] = []; + selectedFeatureInspectionModel: InspectionModelData[] = []; + selectedFeatureIdNames: string[] = []; selectedMapIdName: string = ""; + selectedFeatures: FeatureWrapper[] = []; selectedFeatureGeometryType: any; selectedFeatureCenter: Cartesian3 | null = null; selectedFeatureOrigin: Cartesian3 | null = null; selectedFeatureBoundingRadius: number = 0; - selectedFeature: FeatureWrapper | null = null; originAndNormalForFeatureZoom: Subject<[Cartesian3, Cartesian3]> = new Subject(); selectedSourceData = new BehaviorSubject(null); @@ -67,40 +67,44 @@ export class InspectionService { this.keyboardService.registerShortcuts(["Ctrl+j", "Ctrl+J"], this.zoomToFeature.bind(this)); - this.mapService.selectionTopic.pipe(distinctUntilChanged()).subscribe(selectedFeature => { - if (!selectedFeature) { + this.mapService.selectionTopic.pipe(distinctUntilChanged()).subscribe(selectedFeatures => { + if (!selectedFeatures.length) { this.isInspectionPanelVisible = false; this.featureTreeFilterValue = ""; this.parametersService.unsetSelectedFeature(); return; } - this.selectedMapIdName = selectedFeature.featureTile.mapName; - selectedFeature.peek((feature: Feature) => { - this.selectedFeatureInspectionModel = feature.inspectionModel(); - this.selectedFeatureGeoJsonText = feature.geojson() as string; - this.selectedFeatureIdName = feature.id() as string; - const center = feature.center() as Cartesian3; + // TODO: Handle case where selected features are from different maps. + // Atm, multiple features can merely come from merged feature points. + this.selectedMapIdName = selectedFeatures[0].featureTile.mapName; + this.selectedFeatureInspectionModel = []; + this.selectedFeatureIdNames = []; + this.selectedFeatureGeoJsonTexts = []; + + selectedFeatures.forEach(selectedFeature => { + selectedFeature.peek((feature: Feature) => { + this.selectedFeatureInspectionModel.push(...feature.inspectionModel()); + this.selectedFeatureGeoJsonTexts.push(feature.geojson() as string); + this.selectedFeatureIdNames.push(feature.id() as string); + const center = feature.center() as Cartesian3; this.selectedFeatureCenter = center; this.selectedFeatureOrigin = Cartesian3.fromDegrees(center.x, center.y, center.z); let radiusPoint = feature.boundingRadiusEndPoint() as Cartesian3; radiusPoint = Cartesian3.fromDegrees(radiusPoint.x, radiusPoint.y, radiusPoint.z); this.selectedFeatureBoundingRadius = Cartesian3.distance(this.selectedFeatureOrigin, radiusPoint); - this.selectedFeatureGeometryType = feature.getGeometryType() as any; - this.isInspectionPanelVisible = true; - this.loadFeatureData(); + this.selectedFeatureGeometryType = feature.getGeometryType() as any;this.isInspectionPanelVisible = true; + this.loadFeatureData(); + }); }); - this.selectedFeature = selectedFeature; - this.parametersService.setSelectedFeature(this.selectedMapIdName, this.selectedFeatureIdName); + this.selectedFeatures = selectedFeatures; + this.parametersService.setSelectedFeature(this.selectedMapIdName, this.selectedFeatureIdNames[0]); }); this.parametersService.parameters.pipe(distinctUntilChanged()).subscribe(parameters => { if (parameters.selected.length == 2) { const [mapId, featureId] = parameters.selected; - if (mapId != this.selectedMapIdName || featureId != this.selectedFeatureIdName) { - this.jumpService.highlightFeature(mapId, featureId); - if (this.selectedFeature != null) { - this.mapService.focusOnFeature(this.selectedFeature); - } + if (!this.selectedFeatureIdNames.some(n => n == featureId)) { + this.jumpService.selectFeature(mapId, featureId); } } }); @@ -276,6 +280,10 @@ export class InspectionService { }); } + selectedFeatureGeoJsonCollection() { + return `{"type": "FeatureCollection", "features": [${this.selectedFeatureGeoJsonTexts.join(", ")}]}`; + } + protected readonly InspectionValueType = coreLib.ValueType; protected readonly GeometryType = coreLib.GeomType; } diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index e2c21077..996b7d9c 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -687,9 +687,9 @@ export class MapService { this.zoomLevel.next(MAX_ZOOM_LEVEL); } - resolveFeature(id: TileFeatureId) { - const tile = this.loadedTileLayers.get(id.mapTileKey); - if (!tile) { + resolveFeature(id: TileFeatureId|null) { + const tile = this.loadedTileLayers.get(id?.mapTileKey || ""); + if (!tile || !id?.featureId) { return null; } return new FeatureWrapper(id.featureId, tile); diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts index 2f028fd5..ef363bd0 100644 --- a/erdblick_app/app/pointmerge.service.ts +++ b/erdblick_app/app/pointmerge.service.ts @@ -64,8 +64,8 @@ export class MergedPointsTile { } render(viewer: Viewer) { - if (this.pointPrimitives) { - console.error("MergedPointsTile.render() was called twice."); + if (this.pointPrimitives || this.labelPrimitives) { + this.remove(viewer); } this.pointPrimitives = new PointPrimitiveCollection(); @@ -94,12 +94,29 @@ export class MergedPointsTile { remove(viewer: Viewer) { if (this.pointPrimitives && this.pointPrimitives.length) { - viewer.scene.primitives.add(this.pointPrimitives) + viewer.scene.primitives.remove(this.pointPrimitives) } if (this.labelPrimitives && this.labelPrimitives.length) { - viewer.scene.primitives.add(this.labelPrimitives) + viewer.scene.primitives.remove(this.labelPrimitives) } } + + /** Remove a missing tile and add it to the references. */ + notifyTileInserted(sourceTileId: bigint): boolean { + let newMissingTiles = this.missingTiles.filter(val => val != sourceTileId); + + // Add the source tile ID to the referencing tiles, + // if it was removed from the missing tiles. This can only happen once. + // This way, we are prepared for the idea that a style sheet might + // re-insert some data. + if (newMissingTiles.length != this.missingTiles.length) { + this.referencingTiles.push(sourceTileId); + this.missingTiles = newMissingTiles; + } + + // Yield the corner tile as to-be-rendered, if it does not have any missing tiles. + return !this.missingTiles.length; + } } /** @@ -109,6 +126,7 @@ export class MergedPointsTile { export class PointMergeService { mergedPointsTiles: Map> = new Map>(); + emptyTiles: Map> = new Map>(); /** * Check if the corner tile at geoPos is interested in contributions from `tileId`. @@ -116,14 +134,14 @@ export class PointMergeService * __Note: This is called from WASM.__ */ wants(geoPos: Cartographic, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): boolean { - return this.get(geoPos, coreLib.getTileLevel(sourceTileId), mapLayerStyleRuleId).missingTiles.findIndex(v => v == sourceTileId) != -1; + return this.getCornerTileByPosition(geoPos, coreLib.getTileLevel(sourceTileId), mapLayerStyleRuleId).missingTiles.findIndex(v => v == sourceTileId) != -1; } /** * Count how many points have been merged for the given position and style rule so far. */ count(geoPos: Cartographic, hashPos: PositionHash, level: number, mapLayerStyleRuleId: MapLayerStyleRule): number { - return this.get(geoPos, level, mapLayerStyleRuleId).count(hashPos); + return this.getCornerTileByPosition(geoPos, level, mapLayerStyleRuleId).count(hashPos); } /** @@ -132,7 +150,7 @@ export class PointMergeService * is north if the tile center, the tile IDs y component is decremented (unless it is already 0). * If the position is west of the tile center, the tile IDs x component is decremented (unless it is already 0). */ - get(geoPos: Cartographic, level: number, mapLayerStyleRuleId: MapLayerStyleRule): MergedPointsTile { + getCornerTileByPosition(geoPos: Cartographic, level: number, mapLayerStyleRuleId: MapLayerStyleRule): MergedPointsTile { // Calculate the correct corner tile ID. let tileId = coreLib.getTileIdFromPosition(geoPos.x, geoPos.y, level); let tilePos = coreLib.getTilePosition(tileId); @@ -143,7 +161,13 @@ export class PointMergeService if (geoPos.y > tilePos.y) offsetY = -1; tileId = coreLib.getTileNeighbor(tileId, offsetX, offsetY); + return this.getCornerTileById(tileId, mapLayerStyleRuleId); + } + /** + * Get (or create) a corner tile by its style-rule-id + tile-id combo. + */ + getCornerTileById(tileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): MergedPointsTile { // Get or create the tile-map for the mapLayerStyleRuleId. let styleRuleMap = this.mergedPointsTiles.get(mapLayerStyleRuleId); if (!styleRuleMap) { @@ -163,6 +187,7 @@ export class PointMergeService coreLib.getTileNeighbor(tileId, 0, 1), coreLib.getTileNeighbor(tileId, 1, 1), ] + result.missingTiles = result.missingTiles.filter(tid => !this.isEmptyTile(tid, mapLayerStyleRuleId)); styleRuleMap.set(tileId, result); } return result; @@ -174,11 +199,11 @@ export class PointMergeService * the missingTiles of each. MergedPointsTiles with empty referencingTiles (requiring render) * are yielded. The sourceTileId is also added to the MergedPointsTiles referencingTiles set. */ - *insert(points: Array, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): Iterator { + *insert(points: Array, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): Generator { // Insert the points into the relevant corner tiles. let level = coreLib.getTileLevel(sourceTileId); for (let point of points) { - let mergedPointsTile = this.get(point.position, level, mapLayerStyleRuleId); + let mergedPointsTile = this.getCornerTileByPosition(point.position, level, mapLayerStyleRuleId); mergedPointsTile.add(point); } @@ -189,27 +214,48 @@ export class PointMergeService coreLib.getTileNeighbor(sourceTileId, 0, -1), coreLib.getTileNeighbor(sourceTileId, -1, -1), ]; - let styleRuleMap = this.mergedPointsTiles.get(mapLayerStyleRuleId)!; for (let cornerTileId of cornerTileIds) { - let cornerTile = styleRuleMap.get(cornerTileId); - if (cornerTile) { - let newMissingTiles = cornerTile.missingTiles.filter(val => val != sourceTileId); - - // Add the source tile ID to the referencing tiles, - // if it was removed from the missing tiles. This can only happen once. - // This way, we are prepared for the idea that a style sheet might - // re-insert some data. - if (newMissingTiles.length != cornerTile.missingTiles.length) { - cornerTile.referencingTiles.push(sourceTileId); - cornerTile.missingTiles = newMissingTiles; - } + let cornerTile = this.getCornerTileById(cornerTileId, mapLayerStyleRuleId); + if (cornerTile.notifyTileInserted(sourceTileId)) { + yield cornerTile; + } + } + } + + /** + * Register a tile visualization as empty, meaning that no + * contributions to its corner tiles are to be expected. + */ + *insertEmpty(sourceTileId: bigint, mapLayerStyleId: string): Generator { + // Calculate corner tile IDs for sourceTileId. + let cornerTileIds = [ + sourceTileId, + coreLib.getTileNeighbor(sourceTileId, -1, 0), + coreLib.getTileNeighbor(sourceTileId, 0, -1), + coreLib.getTileNeighbor(sourceTileId, -1, -1), + ]; - // Yield the corner tile as to-be-rendered, if it does not have any missing tiles. - if (!cornerTile.missingTiles.length) { - yield cornerTile; + // Remove the tileId as a contributor from surrounding mergedPointsTiles. + for (let [mapLayerStyleRuleId, cornerTiles] of this.mergedPointsTiles) { + if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { + for (const cornerTileId of cornerTileIds) { + let cornerTile = cornerTiles.get(cornerTileId); + if (cornerTile) { + if (cornerTile.notifyTileInserted(sourceTileId)) { + yield cornerTile; + } + } } } } + + // Register the tile as empty. + let emptyTileSet = this.emptyTiles.get(mapLayerStyleId); + if (!emptyTileSet) { + emptyTileSet = new Set(); + this.emptyTiles.set(mapLayerStyleId, emptyTileSet); + } + emptyTileSet.add(sourceTileId); } /** @@ -217,12 +263,12 @@ export class PointMergeService * prefix-match with the mapLayerStyleId. Yields MergedPointsTiles which now have empty referencingTiles, * and whose visualization (if existing) must therefore be removed from the scene. */ - *remove(sourceTileId: bigint, mapLayerStyleId: string): Iterator { + *remove(sourceTileId: bigint, mapLayerStyleId: string): Generator { for (let [mapLayerStyleRuleId, tiles] of this.mergedPointsTiles.entries()) { if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { for (let [tileId, tile] of tiles) { // Yield the corner tile as to-be-rendered, if it does not have any referencing tiles. - tile.referencingTiles = tile.referencingTiles.filter(val => val == sourceTileId); + tile.referencingTiles = tile.referencingTiles.filter(val => val != sourceTileId); if (!tile.referencingTiles.length) { yield tile; tiles.delete(tileId); @@ -230,5 +276,23 @@ export class PointMergeService } } } + + let emptyTileSet = this.emptyTiles.get(mapLayerStyleId); + if (emptyTileSet && emptyTileSet.has(sourceTileId)) { + emptyTileSet.delete(sourceTileId); + } + } + + /** + * Check if the tile for the given mapLayerStyle is already registered as empty, + * and therefore no contributions can be expected from it. + */ + private isEmptyTile(tid: bigint, mapLayerStyleRuleId: MapLayerStyleRule): boolean { + for (let [mapLayerStyleId, tileIdSet] of this.emptyTiles) { + if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { + return tileIdSet.has(tid); + } + } + return false; } } diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index e9894f6a..62c7314e 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -161,11 +161,7 @@ export class ErdblickViewComponent implements AfterViewInit { }); } } - if (this.isKnownCesiumFeature(feature)) { - this.setPickedCesiumFeature(feature); - } else { - this.setPickedCesiumFeature(null); - } + this.setPickedCesiumFeature(feature); }, ScreenSpaceEventType.LEFT_CLICK); // Add a handler for hover (i.e., MOUSE_MOVE) functionality. @@ -176,11 +172,7 @@ export class ErdblickViewComponent implements AfterViewInit { this.coordinatesService.mouseMoveCoordinates.next(Cartographic.fromCartesian(coordinates)) } let feature = this.viewer.scene.pick(position); - if (this.isKnownCesiumFeature(feature)) { - this.setHoveredCesiumFeature(feature); - } else { - this.setHoveredCesiumFeature(null); - } + this.setHoveredCesiumFeature(feature); }, ScreenSpaceEventType.MOUSE_MOVE); // Add a handler for camera movement. @@ -268,11 +260,6 @@ export class ErdblickViewComponent implements AfterViewInit { this.keyboardService.registerShortcuts(['r', 'R'], this.resetOrientation.bind(this)); } - /** Check if the given feature is known and can be selected. */ - isKnownCesiumFeature(f: any) { - return f && f.id !== undefined && f.id.hasOwnProperty("mapTileKey") - } - /** * Set or re-set the hovered feature. */ @@ -280,7 +267,7 @@ export class ErdblickViewComponent implements AfterViewInit { // Get the actual mapget features for the picked Cesium feature. let resolvedFeatures = this.resolveMapgetFeatures(feature); if (!resolvedFeatures.length) { - this.mapService.selectionTopic.next([]); + this.mapService.hoverTopic.next([]); return; } @@ -320,17 +307,16 @@ export class ErdblickViewComponent implements AfterViewInit { */ private resolveMapgetFeatures(feature: any) { let resolvedFeatures: FeatureWrapper[] = []; - if (Array.isArray(feature?.id)) { - for (const fid of feature.id) { - const resolvedFeature = this.mapService.resolveFeature(feature.id); - if (resolvedFeature) { - resolvedFeatures = [resolvedFeature]; - } + for (const fid of Array.isArray(feature?.id) ? feature.id : [feature?.id]) { + if (fid == "hover-highlight") { + return this.mapService.hoverTopic.getValue(); + } + if (fid == "selection-highlight") { + return this.mapService.selectionTopic.getValue(); } - } else if (feature) { - const resolvedFeature = this.mapService.resolveFeature(feature.id); + const resolvedFeature = this.mapService.resolveFeature(fid); if (resolvedFeature) { - resolvedFeatures = [resolvedFeature]; + resolvedFeatures.push(resolvedFeature); } } return resolvedFeatures; diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index 15084d91..5889030f 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -236,7 +236,9 @@ export class TileVisualization { this.primitiveCollection = visualization.primitiveCollection(); for (const [mapLayerStyleRuleId, mergedPointVisualizations] of Object.entries(visualization.mergedPointFeatures())) { - this.pointMergeService.insert(mergedPointVisualizations as MergedPointVisualization[], this.tile.tileId, mapLayerStyleRuleId); + for (let finishedCornerTile of this.pointMergeService.insert(mergedPointVisualizations as MergedPointVisualization[], this.tile.tileId, mapLayerStyleRuleId)) { + finishedCornerTile.render(viewer); + } } visualization.delete(); return true; @@ -246,6 +248,11 @@ export class TileVisualization { } this.hasHighDetailVisualization = true; } + else if (this.tile.numFeatures <= 0) { + for (let finishedCornerTile of this.pointMergeService.insertEmpty(this.tile.tileId, this.mapLayerStyleId())) { + finishedCornerTile.render(viewer); + } + } if (this.showTileBorder) { // Else: Low-detail bounding box representation @@ -270,9 +277,12 @@ export class TileVisualization { } // Remove point-merge contributions that were made by this map-layer+style visualization combo. - this.pointMergeService.remove( + let removedCornerTiles = this.pointMergeService.remove( this.tile.tileId, - `${this.tile.mapName}:${this.tile.layerName}:${this.style.name()}`); + this.mapLayerStyleId()); + for (let removedCornerTile of removedCornerTiles) { + removedCornerTile.remove(viewer); + } if (this.primitiveCollection) { viewer.scene.primitives.remove(this.primitiveCollection); @@ -307,4 +317,13 @@ export class TileVisualization { (this.showTileBorder != this.hasTileBorder) ); } + + /** + * Combination of map name, layer name, style name and highlight mode which + * (in combination with the tile id) uniquely identifies that rendered contents + * if this TileVisualization as expected by the surrounding MergedPointsTiles. + */ + private mapLayerStyleId() { + return `${this.tile.mapName}:${this.tile.layerName}:${this.style.name()}:${this.highlightMode.value}`; + } } diff --git a/libs/core/include/erdblick/cesium-interface/object.h b/libs/core/include/erdblick/cesium-interface/object.h index 75c28cdf..aa314d8c 100644 --- a/libs/core/include/erdblick/cesium-interface/object.h +++ b/libs/core/include/erdblick/cesium-interface/object.h @@ -67,6 +67,11 @@ struct JsValue */ static JsValue Float64Array(std::vector const& coordinates); + /** + * Construct an undefined value. + */ + static JsValue Undefined(); + /** Construct a JsValue from a variant with specific alternatives. */ template static JsValue fromVariant(T const& variant) { diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index 86e1388c..19fe31b6 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -24,7 +24,7 @@ struct BoundEvalFun class FeatureStyleRule { public: - explicit FeatureStyleRule(YAML::Node const& yaml); + explicit FeatureStyleRule(YAML::Node const& yaml, uint32_t index=0); FeatureStyleRule(FeatureStyleRule const& other, bool resetNonInheritableAttrs=false); enum Aspect { @@ -99,6 +99,8 @@ class FeatureStyleRule [[nodiscard]] std::optional> const& scaleByDistance() const; [[nodiscard]] std::optional> const& offsetScaleByDistance() const; + [[nodiscard]] uint32_t const& index() const; + private: void parse(YAML::Node const& yaml); @@ -162,6 +164,9 @@ class FeatureStyleRule std::optional attributeValidityGeometry_; std::vector firstOfRules_; + + // Index of the rule within the style sheet + int32_t index_ = 0; }; } diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index 00de5bef..a593f2e1 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -246,6 +246,12 @@ class FeatureLayerVisualization */ JsValue makeTileFeatureId(std::string_view const& featureId) const; + /** + * Get a unique identifier for the map+layer+style+rule-id+highlight-mode. + * In combination with a tile id, this uniquely identifiers a merged corner tile. + */ + std::string getMapLayerStyleRuleId(const uint32_t& ruleIndex) const; + /// =========== Generic Members =========== JsValue mapTileKey_; @@ -261,7 +267,7 @@ class FeatureLayerVisualization CesiumPrimitive coloredGroundMeshes_; CesiumPointPrimitiveCollection coloredPoints_; CesiumLabelCollection labelCollection_; - std::map mergedPointsPerStyleRuleId_; + std::map>> mergedPointsPerStyleRuleId_; JsValue featureMergeService_; FeatureLayerStyle const& style_; diff --git a/libs/core/src/cesium-interface/object.cpp b/libs/core/src/cesium-interface/object.cpp index 84f7c70b..ec01d15b 100644 --- a/libs/core/src/cesium-interface/object.cpp +++ b/libs/core/src/cesium-interface/object.cpp @@ -69,6 +69,15 @@ JsValue JsValue::Float64Array(const std::vector& coordinates) #endif } +JsValue JsValue::Undefined() +{ +#ifdef EMSCRIPTEN + return JsValue(emscripten::val::undefined()); +#else + return JsValue(""); +#endif +} + JsValue JsValue::operator[](std::string const& propertyName) { #ifdef EMSCRIPTEN diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index 59aad48e..bde51f12 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -45,7 +45,7 @@ std::optional parseGeometryEnum(std::string const& enumStr) { } } -FeatureStyleRule::FeatureStyleRule(YAML::Node const& yaml) +FeatureStyleRule::FeatureStyleRule(YAML::Node const& yaml, uint32_t index) : index_(index) { parse(yaml); } @@ -679,4 +679,9 @@ std::optional const& FeatureStyleRule::attributeValidityGeometry() const return attributeValidityGeometry_; } +uint32_t const& FeatureStyleRule::index() const +{ + return index_; +} + } \ No newline at end of file diff --git a/libs/core/src/style.cpp b/libs/core/src/style.cpp index ab882f9f..55911f07 100644 --- a/libs/core/src/style.cpp +++ b/libs/core/src/style.cpp @@ -27,9 +27,10 @@ FeatureLayerStyle::FeatureLayerStyle(SharedUint8Array const& yamlArray) return; } + uint32_t ruleIndex = 0; for (auto const& rule : styleYaml["rules"]) { // Create FeatureStyleRule object. - rules_.emplace_back(rule); + rules_.emplace_back(rule, ruleIndex++); } for (auto const& option : styleYaml["options"]) { diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index 0b65788f..efaf2ccb 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -63,8 +63,19 @@ void FeatureLayerVisualization::addTileFeatureLayer( if (!tile_) { tile_ = tile; internalStringPoolCopy_ = std::make_shared(*tile->strings()); + + // Pre-create empty merged point feature visualization lists. + for (auto&& rule : style_.rules()) { + if (rule.mode() != highlightMode_ || !rule.pointMergeGridCellSize()) { + continue; + } + mergedPointsPerStyleRuleId_.emplace( + getMapLayerStyleRuleId(rule.index()), + std::map>()); + } } + // Ensure that the added aux tile and the primary tile use the same // field name encoding. So we transcode the aux tile into the same dict. // However, the transcoding process changes the dictionary, as it might @@ -87,29 +98,30 @@ void FeatureLayerVisualization::run() return evaluateExpression(str, evaluationContext); }}; - uint32_t ruleIndex = 0; for (auto&& rule : style_.rules()) { if (rule.mode() != highlightMode_) { - ++ruleIndex; continue; } - - auto mapLayerStyleRuleId = fmt::format( - "{}:{}:{}:{}", - tile_->mapId(), - tile_->layerInfo()->layerId_, - style_.name(), - ruleIndex); - + auto mapLayerStyleRuleId = getMapLayerStyleRuleId(rule.index()); if (auto* matchingSubRule = rule.match(*feature, boundEvalFun)) { addFeature(feature, boundEvalFun, *matchingSubRule, mapLayerStyleRuleId); featuresAdded_ = true; } - ++ruleIndex; } } } +std::string FeatureLayerVisualization::getMapLayerStyleRuleId(uint32_t const& ruleIndex) const +{ + return fmt::format( + "{}:{}:{}:{}:{}", + tile_->mapId(), + tile_->layerInfo()->layerId_, + style_.name(), + static_cast(highlightMode_), + ruleIndex); +} + NativeJsValue FeatureLayerVisualization::primitiveCollection() const { if (!featuresAdded_) @@ -152,7 +164,13 @@ NativeJsValue FeatureLayerVisualization::mergedPointFeatures() const { auto result = JsValue::Dict(); for (auto const& [mapLayerStyleRuleId, primitives] : mergedPointsPerStyleRuleId_) { - result.set(mapLayerStyleRuleId, primitives); + auto pointList = JsValue::List(); + for (auto const& [_, points] : primitives) { + for (auto const& pt : points) { + pointList.push(pt); + } + } + result.set(mapLayerStyleRuleId, pointList); } return *result; } @@ -284,12 +302,22 @@ void FeatureLayerVisualization::addGeometry( BoundEvalFun& evalFun, glm::dvec3 const& offset) { - if (!rule.selectable()) - id = UnselectableId; - // Combine the ID with the mapTileKey to create an // easy link from the geometry back to the feature. - auto tileFeatureId = makeTileFeatureId(id); + auto tileFeatureId = JsValue::Undefined(); + if (rule.selectable()) { + switch (highlightMode_) { + case FeatureStyleRule::NoHighlight: + tileFeatureId = makeTileFeatureId(id); + break; + case FeatureStyleRule::HoverHighlight: + tileFeatureId = JsValue("hover-highlight"); + break; + case FeatureStyleRule::SelectionHighlight: + tileFeatureId = JsValue("selection-highlight"); + break; + } + } std::vector vertsCartesian; vertsCartesian.reserve(geom->numPoints()); @@ -407,25 +435,21 @@ void FeatureLayerVisualization::addMergedPointGeometry( // Add the $mergeCount variable to the evaluation context. // This variable indicates, how many features from other tiles have already been added - // for the given grid position. + // for the given grid position. We must sum both existing points in the point merge service + // from other tiles, and existing points from this tile. + auto& mergedPointVec = mergedPointsPerStyleRuleId_[mapLayerStyleRuleId][gridPositionHash]; + auto mergedPointCount = featureMergeService_.call( + "count", + pointCartographic, + gridPositionHash, + tile_->tileId().z(), + mapLayerStyleRuleId) + static_cast(mergedPointVec.size()); evalFun.context_.set( internalStringPoolCopy_->emplace("$mergeCount"), - simfil::Value(featureMergeService_.call( - "count", - pointCartographic, - gridPositionHash, - tile_->tileId().z(), - mapLayerStyleRuleId))); - - // Ensure that there is a list of merged points for this mapLayerStyleRuleId. - auto [pointsForStyleRuleIdIt, wasInserted] = - mergedPointsPerStyleRuleId_.emplace(mapLayerStyleRuleId, JsValue()); - if (wasInserted) { - pointsForStyleRuleIdIt->second = JsValue::List(); - } + simfil::Value(mergedPointCount)); // Add a MergedPointVisualization to the list. - pointsForStyleRuleIdIt->second.push(JsValue::Dict({ + mergedPointVec.push_back(JsValue::Dict({ {"position", JsValue(pointCartographic)}, {"positionHash", JsValue(gridPositionHash)}, {geomField, JsValue(makeGeomParams(evalFun))}, From 8623f59a388a803969d3c6d95df2456d821b5427 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Sat, 31 Aug 2024 23:27:59 +0200 Subject: [PATCH 13/37] Fix FeatureTile.has function. --- erdblick_app/app/features.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erdblick_app/app/features.model.ts b/erdblick_app/app/features.model.ts index 2f081505..c995eaa1 100644 --- a/erdblick_app/app/features.model.ts +++ b/erdblick_app/app/features.model.ts @@ -136,7 +136,7 @@ export class FeatureTile { has(featureId: string) { return this.peek((tileFeatureLayer: TileFeatureLayer) => { - return tileFeatureLayer.find(featureId) !== null; + return !tileFeatureLayer.find(featureId).isNull(); }); } } From 12096f63a468500ca101655bda05a41727aa5aa0 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Sat, 31 Aug 2024 23:28:34 +0200 Subject: [PATCH 14/37] Fix multi-selection panel header. --- erdblick_app/app/inspection.panel.component.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/erdblick_app/app/inspection.panel.component.ts b/erdblick_app/app/inspection.panel.component.ts index 86a3dbcc..3031de77 100644 --- a/erdblick_app/app/inspection.panel.component.ts +++ b/erdblick_app/app/inspection.panel.component.ts @@ -67,14 +67,19 @@ export class InspectionPanelComponent this.inspectionService.featureTree.pipe(distinctUntilChanged()).subscribe((tree: string) => { this.reset(); - // TODO: Create a new FeaturePanelComponent instance for each unique selected feature - // then we can get rid of all the service's View Component logic/functions. + // TODO: Create a new FeaturePanelComponent instance for each unique feature selection. + // Then we can get rid of all the service's View Component logic/functions. // reset() Would then completely clear the tabs. - const featureId = this.inspectionService.selectedFeatureIdNames.join(", "); - this.tabs[0].title = featureId; + const featureIds = this.inspectionService.selectedFeatures.map(f=>f.featureId).join(", "); + if (this.inspectionService.selectedFeatures.length == 1) { + this.tabs[0].title = featureIds; + } + else { + this.tabs[0].title = `Selected ${this.inspectionService.selectedFeatures.length} Features`; + } const selectedSourceData = parameterService.getSelectedSourceData() - if (selectedSourceData?.featureId === featureId) + if (selectedSourceData?.featureIds === featureIds) this.inspectionService.selectedSourceData.next(selectedSourceData); else this.inspectionService.selectedSourceData.next(null); From b9b627faa6c43633b2b11c0eb9ff94c7c9cd6c8d Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Sat, 31 Aug 2024 23:30:05 +0200 Subject: [PATCH 15/37] Fix jump-to-feature-reference. --- erdblick_app/app/feature.panel.component.ts | 13 +++--- erdblick_app/app/inspection.service.ts | 49 ++++++++++++--------- erdblick_app/app/map.service.ts | 2 +- erdblick_app/app/parameters.service.ts | 4 +- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index cb80bc7e..0750e250 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -345,15 +345,15 @@ export class FeaturePanelComponent implements OnInit { const layerId = sourceDataRef.layerId; const tileId = sourceDataRef.tileId; const address = sourceDataRef.address; - const mapId = this.inspectionService.selectedMapIdName; - const featureId = this.inspectionService.selectedFeatureIdNames.join(", "); + const mapId = this.inspectionService.selectedFeatures[0].featureTile.mapName; + const featureIds = this.inspectionService.selectedFeatures.map(f=>f.featureId).join(", "); this.inspectionService.selectedSourceData.next({ tileId: Number(tileId), layerId: String(layerId), mapId: String(mapId), address: BigInt(address), - featureId: featureId, + featureIds: featureIds, }) } @@ -365,9 +365,12 @@ export class FeaturePanelComponent implements OnInit { } if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { - this.jumpService.selectFeature(this.inspectionService.selectedMapIdName, rowData["value"]).then(); + // TODO: Support features from varying maps here. + this.jumpService.selectFeature( + this.inspectionService.selectedFeatures[0].featureTile.mapName, + rowData["value"]).then(); } - this.copyToClipboard(rowData["value"]); + // this.copyToClipboard(rowData["value"]); } highlightFeature(rowData: any) { diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index 7839fd0a..a6ade616 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -29,13 +29,27 @@ export interface SelectedSourceData { tileId: number, layerId: string, address: bigint, - featureId: string, + featureIds: string, } export function selectedSourceDataEqualTo(a: SelectedSourceData | null, b: SelectedSourceData | null) { if (!a || !b) return false; - return (a === b || (a.mapId === b.mapId && a.tileId === b.tileId && a.layerId === b.layerId && a.address === b.address && a.featureId === b.featureId)); + return (a === b || (a.mapId === b.mapId && a.tileId === b.tileId && a.layerId === b.layerId && a.address === b.address && a.featureIds === b.featureIds)); +} + +export function selectedFeaturesEqualTo(a: FeatureWrapper[] | null, b: FeatureWrapper[] | null) { + if (!a || !b) + return false; + if (a.length !== b.length) { + return false + } + for (let i = 0; i < a.length; ++i) { + if (!a[i].equals(b[i])) { + return false; + } + } + return true; } @Injectable({providedIn: 'root'}) @@ -46,8 +60,6 @@ export class InspectionService { isInspectionPanelVisible: boolean = false; selectedFeatureGeoJsonTexts: string[] = []; selectedFeatureInspectionModel: InspectionModelData[] = []; - selectedFeatureIdNames: string[] = []; - selectedMapIdName: string = ""; selectedFeatures: FeatureWrapper[] = []; selectedFeatureGeometryType: any; selectedFeatureCenter: Cartesian3 | null = null; @@ -67,43 +79,40 @@ export class InspectionService { this.keyboardService.registerShortcuts(["Ctrl+j", "Ctrl+J"], this.zoomToFeature.bind(this)); - this.mapService.selectionTopic.pipe(distinctUntilChanged()).subscribe(selectedFeatures => { - if (!selectedFeatures.length) { + this.mapService.selectionTopic.pipe(distinctUntilChanged(selectedFeaturesEqualTo)).subscribe(selectedFeatures => { + if (!selectedFeatures?.length) { this.isInspectionPanelVisible = false; this.featureTreeFilterValue = ""; this.parametersService.unsetSelectedFeature(); + this.selectedFeatures = []; return; } - // TODO: Handle case where selected features are from different maps. - // Atm, multiple features can merely come from merged feature points. - this.selectedMapIdName = selectedFeatures[0].featureTile.mapName; this.selectedFeatureInspectionModel = []; - this.selectedFeatureIdNames = []; this.selectedFeatureGeoJsonTexts = []; + this.selectedFeatures = selectedFeatures; selectedFeatures.forEach(selectedFeature => { selectedFeature.peek((feature: Feature) => { this.selectedFeatureInspectionModel.push(...feature.inspectionModel()); this.selectedFeatureGeoJsonTexts.push(feature.geojson() as string); - this.selectedFeatureIdNames.push(feature.id() as string); + this.isInspectionPanelVisible = true; const center = feature.center() as Cartesian3; - this.selectedFeatureCenter = center; - this.selectedFeatureOrigin = Cartesian3.fromDegrees(center.x, center.y, center.z); - let radiusPoint = feature.boundingRadiusEndPoint() as Cartesian3; - radiusPoint = Cartesian3.fromDegrees(radiusPoint.x, radiusPoint.y, radiusPoint.z); - this.selectedFeatureBoundingRadius = Cartesian3.distance(this.selectedFeatureOrigin, radiusPoint); - this.selectedFeatureGeometryType = feature.getGeometryType() as any;this.isInspectionPanelVisible = true; + this.selectedFeatureCenter = center; + this.selectedFeatureOrigin = Cartesian3.fromDegrees(center.x, center.y, center.z); + let radiusPoint = feature.boundingRadiusEndPoint() as Cartesian3; + radiusPoint = Cartesian3.fromDegrees(radiusPoint.x, radiusPoint.y, radiusPoint.z); + this.selectedFeatureBoundingRadius = Cartesian3.distance(this.selectedFeatureOrigin, radiusPoint); + this.selectedFeatureGeometryType = feature.getGeometryType() as any;this.isInspectionPanelVisible = true; this.loadFeatureData(); }); }); - this.selectedFeatures = selectedFeatures; - this.parametersService.setSelectedFeature(this.selectedMapIdName, this.selectedFeatureIdNames[0]); + this.parametersService.setSelectedFeature(this.selectedFeatures[0].featureTile.mapName, this.selectedFeatures[0].featureId); }); this.parametersService.parameters.pipe(distinctUntilChanged()).subscribe(parameters => { if (parameters.selected.length == 2) { const [mapId, featureId] = parameters.selected; - if (!this.selectedFeatureIdNames.some(n => n == featureId)) { + if (!this.selectedFeatures.some(f => f.featureId == featureId)) { this.jumpService.selectFeature(mapId, featureId); } } diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index 996b7d9c..c6fd81a3 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -655,7 +655,7 @@ export class MapService { async selectFeature(tileKey: string, typeId: string, idParts: Array, focus: boolean=false) { const tile = await this.loadTileForSelection(tileKey); // TODO: Doing the stringification here sucks a bit. - const featureId = `${typeId}.${idParts.filter((_, index) => index % 2 === 0).join('.')}`; + const featureId = `${typeId}.${idParts.filter((_, index) => index % 2 === 1).join('.')}`; // Ensure that the feature really exists in the tile. if (!tile.has(featureId)) { diff --git a/erdblick_app/app/parameters.service.ts b/erdblick_app/app/parameters.service.ts index fe663319..781bb323 100644 --- a/erdblick_app/app/parameters.service.ts +++ b/erdblick_app/app/parameters.service.ts @@ -206,7 +206,7 @@ export class ParametersService { selection.layerId, selection.mapId, selection.address.toString(), - selection.featureId, + selection.featureIds, ]; this.parameters.next(this.p()); } @@ -226,7 +226,7 @@ export class ParametersService { layerId: sd[1], mapId: sd[2], address: BigInt(sd[3] || '0'), - featureId: sd[4], + featureIds: sd[4], }; } From 73110bb4366bc2d023afe49f7c5315a6c2a416e7 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Sat, 31 Aug 2024 23:30:49 +0200 Subject: [PATCH 16/37] Simplify PointMergeService. --- erdblick_app/app/pointmerge.service.ts | 101 ++------------------- erdblick_app/app/search.panel.component.ts | 3 + erdblick_app/app/visualization.model.ts | 5 - libs/core/src/visualization.cpp | 5 - 4 files changed, 13 insertions(+), 101 deletions(-) diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts index ef363bd0..27283753 100644 --- a/erdblick_app/app/pointmerge.service.ts +++ b/erdblick_app/app/pointmerge.service.ts @@ -24,14 +24,13 @@ export interface MergedPointVisualization { /** * Container of MergedPointVisualizations, sitting at the corner point of * four surrounding tiles. It covers a quarter of the area of each surrounding - * tile. The actual visualization is performed, once all contributions have been gathered. - * Note: A MergedPointsTile is always unique for its NW corner tile ID and its Map-Layer-Style-Rule ID. + * tile. Note: A MergedPointsTile is always unique for its NW corner tile ID + * and its Map-Layer-Style-Rule ID. */ export class MergedPointsTile { tileId: bigint = 0n; // NW tile ID mapLayerStyleRuleId: MapLayerStyleRule = ""; - missingTiles: Array = []; referencingTiles: Array = []; pointPrimitives: PointPrimitiveCollection|null = null; @@ -101,21 +100,13 @@ export class MergedPointsTile { } } - /** Remove a missing tile and add it to the references. */ - notifyTileInserted(sourceTileId: bigint): boolean { - let newMissingTiles = this.missingTiles.filter(val => val != sourceTileId); - - // Add the source tile ID to the referencing tiles, - // if it was removed from the missing tiles. This can only happen once. - // This way, we are prepared for the idea that a style sheet might - // re-insert some data. - if (newMissingTiles.length != this.missingTiles.length) { + /** + * Add a neighboring tile which keeps this corner tile alive + */ + addReference(sourceTileId: bigint) { + if (this.referencingTiles.findIndex(v => v == sourceTileId) == -1) { this.referencingTiles.push(sourceTileId); - this.missingTiles = newMissingTiles; } - - // Yield the corner tile as to-be-rendered, if it does not have any missing tiles. - return !this.missingTiles.length; } } @@ -126,16 +117,6 @@ export class MergedPointsTile { export class PointMergeService { mergedPointsTiles: Map> = new Map>(); - emptyTiles: Map> = new Map>(); - - /** - * Check if the corner tile at geoPos is interested in contributions from `tileId`. - * Returns true if respective corner has sourceTileId in its in missingTiles. - * __Note: This is called from WASM.__ - */ - wants(geoPos: Cartographic, sourceTileId: bigint, mapLayerStyleRuleId: MapLayerStyleRule): boolean { - return this.getCornerTileByPosition(geoPos, coreLib.getTileLevel(sourceTileId), mapLayerStyleRuleId).missingTiles.findIndex(v => v == sourceTileId) != -1; - } /** * Count how many points have been merged for the given position and style rule so far. @@ -181,13 +162,6 @@ export class PointMergeService result = new MergedPointsTile(); result.tileId = tileId; result.mapLayerStyleRuleId = mapLayerStyleRuleId; - result.missingTiles = [ - tileId, - coreLib.getTileNeighbor(tileId, 1, 0), - coreLib.getTileNeighbor(tileId, 0, 1), - coreLib.getTileNeighbor(tileId, 1, 1), - ] - result.missingTiles = result.missingTiles.filter(tid => !this.isEmptyTile(tid, mapLayerStyleRuleId)); styleRuleMap.set(tileId, result); } return result; @@ -216,46 +190,9 @@ export class PointMergeService ]; for (let cornerTileId of cornerTileIds) { let cornerTile = this.getCornerTileById(cornerTileId, mapLayerStyleRuleId); - if (cornerTile.notifyTileInserted(sourceTileId)) { - yield cornerTile; - } - } - } - - /** - * Register a tile visualization as empty, meaning that no - * contributions to its corner tiles are to be expected. - */ - *insertEmpty(sourceTileId: bigint, mapLayerStyleId: string): Generator { - // Calculate corner tile IDs for sourceTileId. - let cornerTileIds = [ - sourceTileId, - coreLib.getTileNeighbor(sourceTileId, -1, 0), - coreLib.getTileNeighbor(sourceTileId, 0, -1), - coreLib.getTileNeighbor(sourceTileId, -1, -1), - ]; - - // Remove the tileId as a contributor from surrounding mergedPointsTiles. - for (let [mapLayerStyleRuleId, cornerTiles] of this.mergedPointsTiles) { - if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { - for (const cornerTileId of cornerTileIds) { - let cornerTile = cornerTiles.get(cornerTileId); - if (cornerTile) { - if (cornerTile.notifyTileInserted(sourceTileId)) { - yield cornerTile; - } - } - } - } - } - - // Register the tile as empty. - let emptyTileSet = this.emptyTiles.get(mapLayerStyleId); - if (!emptyTileSet) { - emptyTileSet = new Set(); - this.emptyTiles.set(mapLayerStyleId, emptyTileSet); + cornerTile.addReference(sourceTileId); + yield cornerTile; } - emptyTileSet.add(sourceTileId); } /** @@ -267,7 +204,7 @@ export class PointMergeService for (let [mapLayerStyleRuleId, tiles] of this.mergedPointsTiles.entries()) { if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { for (let [tileId, tile] of tiles) { - // Yield the corner tile as to-be-rendered, if it does not have any referencing tiles. + // Yield the corner tile as to-be-deleted, if it does not have any referencing tiles. tile.referencingTiles = tile.referencingTiles.filter(val => val != sourceTileId); if (!tile.referencingTiles.length) { yield tile; @@ -276,23 +213,5 @@ export class PointMergeService } } } - - let emptyTileSet = this.emptyTiles.get(mapLayerStyleId); - if (emptyTileSet && emptyTileSet.has(sourceTileId)) { - emptyTileSet.delete(sourceTileId); - } - } - - /** - * Check if the tile for the given mapLayerStyle is already registered as empty, - * and therefore no contributions can be expected from it. - */ - private isEmptyTile(tid: bigint, mapLayerStyleRuleId: MapLayerStyleRule): boolean { - for (let [mapLayerStyleId, tileIdSet] of this.emptyTiles) { - if (mapLayerStyleRuleId.startsWith(mapLayerStyleId)) { - return tileIdSet.has(tid); - } - } - return false; } } diff --git a/erdblick_app/app/search.panel.component.ts b/erdblick_app/app/search.panel.component.ts index c4893d8c..a4b2d36d 100644 --- a/erdblick_app/app/search.panel.component.ts +++ b/erdblick_app/app/search.panel.component.ts @@ -218,6 +218,9 @@ export class SearchPanelComponent implements AfterViewInit { ]; }); + // TODO: Get rid of map selection, as soon as we support + // multi-selection from different maps. Then we can + // just search all maps simultaneously. jumpToTargetService.mapSelectionSubject.subscribe(maps => { this.mapSelection = maps; this.mapSelectionVisible = true; diff --git a/erdblick_app/app/visualization.model.ts b/erdblick_app/app/visualization.model.ts index 5889030f..3aa8eb45 100644 --- a/erdblick_app/app/visualization.model.ts +++ b/erdblick_app/app/visualization.model.ts @@ -248,11 +248,6 @@ export class TileVisualization { } this.hasHighDetailVisualization = true; } - else if (this.tile.numFeatures <= 0) { - for (let finishedCornerTile of this.pointMergeService.insertEmpty(this.tile.tileId, this.mapLayerStyleId())) { - finishedCornerTile.render(viewer); - } - } if (this.showTileBorder) { // Else: Low-detail bounding box representation diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index efaf2ccb..9b103fc0 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -419,11 +419,6 @@ void FeatureLayerVisualization::addMergedPointGeometry( BoundEvalFun& evalFun, std::function const& makeGeomParams) { - // Check if the corner tile for the cartographic position is still accepting - // contributions from this tile. - if (!featureMergeService_.call("wants", pointCartographic, tile_->tileId().value_, mapLayerStyleRuleId)) - return; - // Convert the cartographic point to an integer representation, based // on the grid cell size set in the style sheet. auto gridPosition = pointCartographic / *gridCellSize; From d85bc74d2e90865ba838a8da4efa9823c61aac32 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 2 Sep 2024 09:44:28 +0200 Subject: [PATCH 17/37] cmake: Conditionally forward toolchain file --- cmake/cesium.cmake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmake/cesium.cmake b/cmake/cesium.cmake index 651d4fd2..563d32c5 100644 --- a/cmake/cesium.cmake +++ b/cmake/cesium.cmake @@ -49,6 +49,10 @@ foreach (lib ${CESIUM_LIBS}) endforeach() message(STATUS "cesium byproducts: ${CESIUM_BYPRODUCTS}") +if (CMAKE_TOOLCHAIN_FILE) + list(cesium_extra_args "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") +endif() + ExternalProject_Add(cesiumnative SOURCE_DIR ${cesiumnative_src_SOURCE_DIR} CMAKE_ARGS @@ -58,8 +62,8 @@ ExternalProject_Add(cesiumnative -DCESIUM_TRACING_ENABLED=OFF -DDRACO_JS_GLUE=OFF -DBUILD_SHARED_LIBS=OFF - -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ${cesium_extra_args} BUILD_BYPRODUCTS ${CESIUM_BYPRODUCTS} INSTALL_COMMAND "" From e0b8d92fba2d9697c3dfd9a52c713c8923de349a Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 2 Sep 2024 09:50:19 +0200 Subject: [PATCH 18/37] Fix CMake list usage --- cmake/cesium.cmake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmake/cesium.cmake b/cmake/cesium.cmake index 563d32c5..ad799e7c 100644 --- a/cmake/cesium.cmake +++ b/cmake/cesium.cmake @@ -49,8 +49,9 @@ foreach (lib ${CESIUM_LIBS}) endforeach() message(STATUS "cesium byproducts: ${CESIUM_BYPRODUCTS}") +set(CESIUM_EXTRA_ARGS) if (CMAKE_TOOLCHAIN_FILE) - list(cesium_extra_args "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") + list(APPEND CESIUM_EXTRA_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") endif() ExternalProject_Add(cesiumnative @@ -63,7 +64,7 @@ ExternalProject_Add(cesiumnative -DDRACO_JS_GLUE=OFF -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - ${cesium_extra_args} + ${CESIUM_EXTRA_ARGS} BUILD_BYPRODUCTS ${CESIUM_BYPRODUCTS} INSTALL_COMMAND "" From fc97c8d06c605774f311550348f54b29d64007ea Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 2 Sep 2024 09:52:22 +0200 Subject: [PATCH 19/37] Remove emsdk include from non-wasm header --- libs/core/include/erdblick/sourcedata.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/core/include/erdblick/sourcedata.hpp b/libs/core/include/erdblick/sourcedata.hpp index a955fddb..08d46e65 100644 --- a/libs/core/include/erdblick/sourcedata.hpp +++ b/libs/core/include/erdblick/sourcedata.hpp @@ -1,5 +1,3 @@ -#include - #include "mapget/model/sourcedatalayer.h" #include "cesium-interface/object.h" From 18587606367ed1dee47425cfe331326921ea5387 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 2 Sep 2024 10:00:56 +0200 Subject: [PATCH 20/37] cmake: Run build-ui for emsdk builds only --- CMakeLists.txt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 773c2de3..5a4b320e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,17 +33,15 @@ FetchContent_MakeAvailable(yaml-cpp) include(cmake/cesium.cmake) # Erdblick Core Library - add_subdirectory(libs/core) if(NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") add_subdirectory(test) +else() + # Angular Build + add_custom_target(erdblick-ui ALL + COMMAND bash "${CMAKE_SOURCE_DIR}/build-ui.bash" "${CMAKE_SOURCE_DIR}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + DEPENDS erdblick-core) endif() -# Angular Build - -add_custom_target(erdblick-ui ALL - COMMAND bash "${CMAKE_SOURCE_DIR}/build-ui.bash" "${CMAKE_SOURCE_DIR}" - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" - DEPENDS erdblick-core -) From a1c5376f0f5652fe5dad8262372fb61e65378abd Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 2 Sep 2024 10:11:17 +0200 Subject: [PATCH 21/37] cmake: Fail on no-tests --- .github/workflows/build-release.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 8f2cb5a5..4915e1ff 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -50,12 +50,9 @@ jobs: - name: Compile run: | - mkdir build - cd build - cmake -GNinja -DCMAKE_BUILD_TYPE=Debug .. - cmake --build . + cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -B build + cmake --build build - name: Run Tests run: | - cd build - ctest --verbose + ctest --verbose --no-tests=error --test-dir=build From 5f73f2142d559d422f9a550964b66c2cdb8e96e5 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Mon, 2 Sep 2024 10:13:28 +0200 Subject: [PATCH 22/37] cmake: Enable testing --- .github/workflows/build-release.yml | 2 +- test/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 4915e1ff..457aec78 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -55,4 +55,4 @@ jobs: - name: Run Tests run: | - ctest --verbose --no-tests=error --test-dir=build + ctest --verbose --no-tests=error --test-dir build diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d2f7e752..aa8d5d9a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,5 @@ project(test.erdblick) +enable_testing() if (NOT TARGET Catch2) FetchContent_Declare(Catch2 From 8303e21eb7ae842ff3ebfe28dcafcd9640f9956a Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 30 Aug 2024 14:42:40 +0200 Subject: [PATCH 23/37] Feature Inspector: Always expand section nodes --- erdblick_app/app/feature.panel.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index 0750e250..47b359a7 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -230,8 +230,9 @@ export class FeaturePanelComponent implements OnInit { expandTreeNodes(nodes: TreeTableNode[], parent: any = null): void { nodes.forEach(node => { const isTopLevelNode = parent === null; + const isSection = node.data["type"] === this.InspectionValueType.SECTION.value; const hasSingleChild = node.children && node.children.length === 1; - node.expanded = isTopLevelNode || hasSingleChild; + node.expanded = isTopLevelNode || isSection || hasSingleChild; if (node.children) { this.expandTreeNodes(node.children, node); From c1e4d6e32f524f3e267353cb87d55d49105b78db Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 30 Aug 2024 15:55:46 +0200 Subject: [PATCH 24/37] Feature Inspector: Show feature section source ref --- erdblick_app/app/feature.panel.component.ts | 2 +- erdblick_app/app/inspection.service.ts | 3 +++ libs/core/src/inspection.cpp | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index 47b359a7..f636d00e 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -230,7 +230,7 @@ export class FeaturePanelComponent implements OnInit { expandTreeNodes(nodes: TreeTableNode[], parent: any = null): void { nodes.forEach(node => { const isTopLevelNode = parent === null; - const isSection = node.data["type"] === this.InspectionValueType.SECTION.value; + const isSection = node.data && node.data["type"] === this.InspectionValueType.SECTION.value; const hasSingleChild = node.children && node.children.length === 1; node.expanded = isTopLevelNode || isSection || hasSingleChild; diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index a6ade616..8ebc302a 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -181,6 +181,9 @@ export class InspectionService { if (section.hasOwnProperty("info")) { node.data["info"] = section.info; } + if (section.hasOwnProperty("sourceDataReferences")) { + node.data["sourceDataReferences"] = section.sourceDataReferences; + } node.children = convertToTreeTableNodes(section.children); treeNodes.push(node); } diff --git a/libs/core/src/inspection.cpp b/libs/core/src/inspection.cpp index 0bf82571..0e598b86 100644 --- a/libs/core/src/inspection.cpp +++ b/libs/core/src/inspection.cpp @@ -46,6 +46,10 @@ JsValue InspectionConverter::convert(model_ptr const& featurePtr) stringPool_ = featurePtr->model().strings(); featureId_ = featurePtr->id()->toString(); + // Top-Level Feature Item + auto featureScope = push("Feature", "", ValueType::Section); + featureScope->value_ = JsValue(featurePtr->id()->toString()); + convertSourceDataReferences(featurePtr->sourceDataReferences(), *featureScope); // Identifiers section. { auto scope = push(convertStringView("Identifiers"), "", ValueType::Section); From 317c7dd8abb26eca2583e18369f481df7ebd483d Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 30 Aug 2024 16:17:31 +0200 Subject: [PATCH 25/37] Inspector Panel: Reset source data selection --- erdblick_app/app/inspection.panel.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erdblick_app/app/inspection.panel.component.ts b/erdblick_app/app/inspection.panel.component.ts index 3031de77..d73c8fe6 100644 --- a/erdblick_app/app/inspection.panel.component.ts +++ b/erdblick_app/app/inspection.panel.component.ts @@ -86,8 +86,10 @@ export class InspectionPanelComponent }); this.inspectionService.selectedSourceData.pipe(distinctUntilChanged(selectedSourceDataEqualTo)).subscribe(selection => { - if (selection) + if (selection) { + this.reset(); this.pushSourceDataInspector(selection); + } }) } From fa086783cf7e09169ecea1c8557ba3540b47d7d2 Mon Sep 17 00:00:00 2001 From: Johannes Wolf Date: Fri, 30 Aug 2024 16:21:08 +0200 Subject: [PATCH 26/37] Inspector Panel: Fix source data click-through --- erdblick_app/app/feature.panel.component.ts | 7 ++++--- libs/core/src/inspection.cpp | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index f636d00e..7b1371f5 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -79,8 +79,7 @@ interface Column { /> const& featurePtr) auto featureScope = push("Feature", "", ValueType::Section); featureScope->value_ = JsValue(featurePtr->id()->toString()); convertSourceDataReferences(featurePtr->sourceDataReferences(), *featureScope); + // Identifiers section. { auto scope = push(convertStringView("Identifiers"), "", ValueType::Section); From 52f82bd1550a8be472ac170e04612efe74b5b0c1 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Mon, 2 Sep 2024 14:12:00 +0200 Subject: [PATCH 27/37] Update Github actions. --- .github/workflows/build-release.yml | 20 -------------------- .github/workflows/build-test.yml | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/build-test.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 457aec78..99a0b923 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -2,8 +2,6 @@ name: build-release on: push: - branches: - - 'feature/renderer-wasm-demo' pull_request: workflow_dispatch: @@ -38,21 +36,3 @@ jobs: name: erdblick path: | static/* - - build-test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install build dependencies - run: sudo apt-get install ninja-build - - - name: Compile - run: | - cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -B build - cmake --build build - - - name: Run Tests - run: | - ctest --verbose --no-tests=error --test-dir build diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000..31a7bcb5 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,27 @@ +name: build-test + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install build dependencies + run: sudo apt-get install ninja-build + + - name: Compile + run: | + mkdir build-test && cd build-test + cmake -GNinja -DCMAKE_BUILD_TYPE=Debug .. + cmake --build . + + - name: Run Tests + run: | + cd build-test + ctest --verbose --no-tests=error From de497ec9f5b580cdcc31ade9f51a841bf8c8cf89 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Tue, 3 Sep 2024 18:26:54 +0200 Subject: [PATCH 28/37] Cesium Build fixes. --- cmake/cesium.cmake | 7 +++++-- cmake/draco.patch | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 cmake/draco.patch diff --git a/cmake/cesium.cmake b/cmake/cesium.cmake index ad799e7c..7609829d 100644 --- a/cmake/cesium.cmake +++ b/cmake/cesium.cmake @@ -15,7 +15,7 @@ set(CESIUM_LIBS CesiumGltf CesiumGltfWriter) -# Use fetch content for cloning the repository durring +# Use fetch content for cloning the repository during # configure phase. We do not call `FetchContent_MakeAvailable`, # but instead use `ExternalProject_Add` to compile Cesium in # isolation. @@ -23,7 +23,10 @@ FetchContent_Declare(cesiumnative_src GIT_REPOSITORY "https://github.com/Klebert-Engineering/cesium-native.git" GIT_TAG "main" GIT_SUBMODULES_RECURSE YES - GIT_PROGRESS YES) + GIT_PROGRESS YES + PATCH_COMMAND git reset --hard HEAD && git -C extern/draco reset --hard HEAD && git apply "${CMAKE_CURRENT_SOURCE_DIR}/cmake/draco.patch" + UPDATE_DISCONNECTED YES + UPDATE_COMMAND "") FetchContent_GetProperties(cesiumnative_src) if (NOT cesiumnative_src_POPULATED) diff --git a/cmake/draco.patch b/cmake/draco.patch new file mode 100644 index 00000000..06f61e00 --- /dev/null +++ b/cmake/draco.patch @@ -0,0 +1,47 @@ +index c928fdf..cf6a63b 100644 +--- a/extern/draco/src/draco/io/file_utils.h ++++ b/extern/draco/src/draco/io/file_utils.h +@@ -17,6 +17,7 @@ + + #include + #include ++#include + + namespace draco { + +diff --git a/CesiumAsync/include/CesiumAsync/CacheItem.h b/CesiumAsync/include/CesiumAsync/CacheItem.h +index 20d1ca80..bd47492b 100644 +--- a/CesiumAsync/include/CesiumAsync/CacheItem.h ++++ b/CesiumAsync/include/CesiumAsync/CacheItem.h +@@ -9,6 +9,7 @@ + #include + #include + #include ++#include + + namespace CesiumAsync { + +diff --git a/CesiumAsync/include/CesiumAsync/IAssetResponse.h b/CesiumAsync/include/CesiumAsync/IAssetResponse.h +index 10519057..0944b26b 100644 +--- a/CesiumAsync/include/CesiumAsync/IAssetResponse.h ++++ b/CesiumAsync/include/CesiumAsync/IAssetResponse.h +@@ -8,6 +8,7 @@ + #include + #include + #include ++#include + + namespace CesiumAsync { + +diff --git a/CesiumIonClient/src/fillWithRandomBytes.h b/CesiumIonClient/src/fillWithRandomBytes.h +index 55765c72..654d09df 100644 +--- a/CesiumIonClient/src/fillWithRandomBytes.h ++++ b/CesiumIonClient/src/fillWithRandomBytes.h +@@ -1,6 +1,7 @@ + #pragma once + + #include ++#include + + namespace CesiumIonClient { + From 86de59cd5639648af8a615ef958b2091aaf811fd Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 4 Sep 2024 09:06:50 +0200 Subject: [PATCH 29/37] Debug multi-selection refactoring. --- erdblick_app/app/feature.panel.component.ts | 84 ++++----- erdblick_app/app/feature.search.component.ts | 2 +- erdblick_app/app/features.model.ts | 9 + erdblick_app/app/inspection.service.ts | 14 +- erdblick_app/app/jump.service.ts | 20 ++- erdblick_app/app/map.service.ts | 163 ++++++++++++------ erdblick_app/app/parameters.service.ts | 67 +++++-- erdblick_app/app/pointmerge.service.ts | 2 +- .../app/sourcedata.panel.component.ts | 2 +- erdblick_app/app/view.component.ts | 87 ++-------- test/test-visualization.cpp | 2 +- 11 files changed, 247 insertions(+), 205 deletions(-) diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index 7b1371f5..1cfa7559 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -18,15 +18,15 @@ interface Column { selector: 'feature-panel', template: `
+ style="display: flex; align-content: center; justify-content: center; width: 100%; padding: 0.5em;">
+ class="pi pi-times clear-icon" style="cursor: pointer">
+ [pTooltip]="rowData['key'].toString()" tooltipPosition="left" + [tooltipOptions]="tooltipOptions"> {{ rowData['key'] }} + style="cursor: pointer">{{ rowData['key'] }} - + @@ -92,16 +93,16 @@ interface Column {
+ [pTooltip]="rowData['value'].toString()" tooltipPosition="left" + [tooltipOptions]="tooltipOptions">
+ (mouseover)="onValueHover($event, rowData)" + (mouseout)="onValueHoverExit($event, rowData)"> {{ rowData['value'] }} + [pTooltip]="rowData['info'].toString()" + tooltipPosition="left">
@@ -367,19 +368,24 @@ export class FeaturePanelComponent implements OnInit { } if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { - // TODO: Support features from varying maps here. - this.jumpService.selectFeature( - this.inspectionService.selectedFeatures[0].featureTile.mapName, - rowData["value"]).then(); + this.jumpService.highlightByJumpTargetFilter( + rowData["value"].mapTileKey, + rowData["value"].featureId).then(); } - // this.copyToClipboard(rowData["value"]); } - highlightFeature(rowData: any) { - return; + onValueHover(event: any, rowData: any) { + event.stopPropagation(); + if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { + this.jumpService.highlightByJumpTargetFilter( + rowData["value"].mapTileKey, + rowData["value"].featureId, + coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); + } } - stopHighlight(rowData: any) { + onValueHoverExit(event: any, rowData: any) { + event.stopPropagation(); return; } diff --git a/erdblick_app/app/feature.search.component.ts b/erdblick_app/app/feature.search.component.ts index bc06dc3a..56cbfa58 100644 --- a/erdblick_app/app/feature.search.component.ts +++ b/erdblick_app/app/feature.search.component.ts @@ -107,7 +107,7 @@ export class FeatureSearchComponent { selectResult(event: any) { if (event.value.mapId && event.value.featureId) { - this.jumpService.selectFeature(event.value.mapId, event.value.featureId).then(() => { + this.jumpService.highlightByJumpTargetFilter(event.value.mapId, event.value.featureId).then(() => { if (this.inspectionService.selectedFeatures.length) { this.mapService.focusOnFeature(this.inspectionService.selectedFeatures[0]); } diff --git a/erdblick_app/app/features.model.ts b/erdblick_app/app/features.model.ts index c995eaa1..44413509 100644 --- a/erdblick_app/app/features.model.ts +++ b/erdblick_app/app/features.model.ts @@ -2,6 +2,7 @@ import {uint8ArrayToWasm, uint8ArrayToWasmAsync} from "./wasm"; import {TileLayerParser, TileFeatureLayer} from '../../build/libs/core/erdblick-core'; +import {TileFeatureId} from "./parameters.service"; /** * JS interface of a WASM TileFeatureLayer. @@ -184,10 +185,18 @@ export class FeatureWrapper { }); } + /** Check if this wrapper wraps the same feature as another wrapper. */ equals(other: FeatureWrapper | null): boolean { if (!other) { return false; } return this.featureTile.mapTileKey == other.featureTile.mapTileKey && this.featureId == other.featureId; } + + key(): TileFeatureId { + return { + mapTileKey: this.featureTile.mapTileKey, + featureId: this.featureId + }; + } } diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index 8ebc302a..bcb6b577 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -4,7 +4,7 @@ import {BehaviorSubject, distinctUntilChanged, Subject, distinctUntilKeyChanged, import {MapService} from "./map.service"; import {Feature, TileSourceDataLayer} from "../../build/libs/core/erdblick-core"; import {FeatureWrapper} from "./features.model"; -import {ParametersService} from "./parameters.service"; +import {ParametersService, TileFeatureId} from "./parameters.service"; import {coreLib, uint8ArrayToWasm} from "./wasm"; import {JumpTargetService} from "./jump.service"; import {Cartesian3} from "./cesium"; @@ -83,7 +83,7 @@ export class InspectionService { if (!selectedFeatures?.length) { this.isInspectionPanelVisible = false; this.featureTreeFilterValue = ""; - this.parametersService.unsetSelectedFeature(); + this.parametersService.setSelectedFeatures([]); this.selectedFeatures = []; return; } @@ -106,16 +106,8 @@ export class InspectionService { this.loadFeatureData(); }); }); - this.parametersService.setSelectedFeature(this.selectedFeatures[0].featureTile.mapName, this.selectedFeatures[0].featureId); - }); - this.parametersService.parameters.pipe(distinctUntilChanged()).subscribe(parameters => { - if (parameters.selected.length == 2) { - const [mapId, featureId] = parameters.selected; - if (!this.selectedFeatures.some(f => f.featureId == featureId)) { - this.jumpService.selectFeature(mapId, featureId); - } - } + this.parametersService.setSelectedFeatures(this.selectedFeatures.map(f => f.key())); }); this.selectedSourceData.pipe(distinctUntilChanged(selectedSourceDataEqualTo)).subscribe(selection => { diff --git a/erdblick_app/app/jump.service.ts b/erdblick_app/app/jump.service.ts index c29d325d..d8ba8744 100644 --- a/erdblick_app/app/jump.service.ts +++ b/erdblick_app/app/jump.service.ts @@ -7,6 +7,7 @@ import {InfoMessageService} from "./info.service"; import {coreLib} from "./wasm"; import {FeatureSearchService} from "./feature.search.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; +import {HighlightMode} from "build/libs/core/erdblick-core"; export interface SearchTarget { icon: string; @@ -122,7 +123,7 @@ export class JumpTargetService { name: `Jump to ${fjt.name}`, label: label, enabled: !fjt.error, - execute: (_: string) => { this.jumpToFeature(fjt).then(); }, + execute: (_: string) => { this.highlightByJumpTarget(fjt).then(); }, validate: (_: string) => { return !fjt.error; }, } }); @@ -135,17 +136,18 @@ export class JumpTargetService { ]); } - async selectFeature(mapId: string, featureId: string) { + /** Select */ + async highlightByJumpTargetFilter(mapId: string, featureId: string, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { let featureJumpTargets = this.mapService.tileParser?.filterFeatureJumpTargets(featureId) as Array; const validIndex = featureJumpTargets.findIndex(action => !action.error); if (validIndex == -1) { console.error(`Error highlighting ${featureId}!`); return; } - await this.jumpToFeature(featureJumpTargets[validIndex], false, mapId); + await this.highlightByJumpTarget(featureJumpTargets[validIndex], false, mapId, mode); } - async jumpToFeature(action: FeatureJumpAction, moveCamera: boolean=true, mapId?:string|null) { + async highlightByJumpTarget(action: FeatureJumpAction, moveCamera: boolean=true, mapId?:string|null, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { // Select the map. if (!mapId) { if (action.maps.length > 1) { @@ -184,10 +186,10 @@ export class JumpTargetService { let selectThisFeature = extRefsResolved.responses[0][0]; // Set feature-to-select on MapService. - await this.mapService.selectFeature( - selectThisFeature.tileId, - selectThisFeature.typeId, - selectThisFeature.featureId, - moveCamera); + const featureId = `${selectThisFeature.typeId}.${selectThisFeature.featureId.filter((_, index) => index % 2 === 1).join('.')}`; + await this.mapService.highlightFeatures([{ + mapTileKey: selectThisFeature.tileId, + featureId: featureId + }], moveCamera, mode).then(); } } diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index c6fd81a3..6f4234c7 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -3,24 +3,15 @@ import {Fetch} from "./fetch.model"; import {FeatureTile, FeatureWrapper} from "./features.model"; import {coreLib, uint8ArrayToWasm} from "./wasm"; import {TileVisualization} from "./visualization.model"; -import {BehaviorSubject, Subject} from "rxjs"; +import {BehaviorSubject, distinctUntilChanged, Subject} from "rxjs"; import {ErdblickStyle, StyleService} from "./style.service"; import {FeatureLayerStyle, TileLayerParser, Feature, HighlightMode} from '../../build/libs/core/erdblick-core'; -import {ParametersService} from "./parameters.service"; +import {ParametersService, TileFeatureId} from "./parameters.service"; import {SidePanelService, SidePanelState} from "./sidepanel.service"; import {InfoMessageService} from "./info.service"; import {MAX_ZOOM_LEVEL} from "./feature.search.service"; import {PointMergeService} from "./pointmerge.service"; -/** - * Combination of a tile id and a feature id, which may be resolved - * to a feature object. - */ -export interface TileFeatureId { - featureId: string, - mapTileKey: string, -} - /** Expected structure of a LayerInfoItem's coverage entry. */ export interface CoverageRectItem extends Object { min: number, @@ -68,6 +59,13 @@ type ViewportProperties = { camPosLat: number }; +/** + * Determine if two lists of feature wrappers have the same features. + */ +function featureSetsEqual(rhs: FeatureWrapper[], lhs: FeatureWrapper[]) { + return rhs.length === lhs.length && rhs.every(rf => lhs.some(lf => rf.equals(lf))); +} + /** * Erdblick map service class. This class is responsible for keeping track * of the following objects: @@ -180,6 +178,10 @@ export class MapService { this.update(); }) + this.parameterService.parameters.pipe(distinctUntilChanged()).subscribe(parameters => { + this.highlightFeatures(parameters.selected).then(); + }); + await this.reloadDataSources(); this.selectionTopic.subscribe(selectedFeatureWrappers => { @@ -626,49 +628,110 @@ export class MapService { return this.loadedTileLayers.get(tileKey) || null; } - async loadTileForSelection(tileKey: string) { - if (this.loadedTileLayers.has(tileKey)) { - return this.loadedTileLayers.get(tileKey)!; - } + async loadTiles(tileKeys: Set): Promise> { + let result = new Map(); - let [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); - this.selectionTileRequest = { - remoteRequest: { - mapId: mapId, - layerId: layerId, - tileIds: [Number(tileId)], - }, - tileKey: tileKey, - resolve: null, - reject: null, - } + // TODO: Optimize this loop to make just a single update call. + for (let tileKey of tileKeys) { + if (!tileKey) { + continue; + } - let selectionTilePromise = new Promise((resolve, reject)=>{ - this.selectionTileRequest!.resolve = resolve; - this.selectionTileRequest!.reject = reject; - }) + let tile = this.loadedTileLayers.get(tileKey); + if (tile) { + result.set(tileKey, tile); + continue; + } - this.update(); - return selectionTilePromise; + let [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); + this.selectionTileRequest = { + remoteRequest: { + mapId: mapId, + layerId: layerId, + tileIds: [Number(tileId)], + }, + tileKey: tileKey, + resolve: null, + reject: null, + } + + let selectionTilePromise = new Promise((resolve, reject)=>{ + this.selectionTileRequest!.resolve = resolve; + this.selectionTileRequest!.reject = reject; + }) + + this.update(); + await selectionTilePromise; + } + + return result; } - async selectFeature(tileKey: string, typeId: string, idParts: Array, focus: boolean=false) { - const tile = await this.loadTileForSelection(tileKey); - // TODO: Doing the stringification here sucks a bit. - const featureId = `${typeId}.${idParts.filter((_, index) => index % 2 === 1).join('.')}`; + async highlightFeatures(tileFeatureIds: (TileFeatureId|null|string)[], focus: boolean=false, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { + if (mode == coreLib.HighlightMode.SELECTION_HIGHLIGHT) + console.trace(tileFeatureIds) + + // Load the tiles for the selection. + const tiles = await this.loadTiles( + new Set(tileFeatureIds.filter(s => typeof s !== "string").map(s => s?.mapTileKey || null))); // Ensure that the feature really exists in the tile. - if (!tile.has(featureId)) { - const [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(tileKey); - this.messageService.showError( - `The feature ${featureId} does not exist in the ${layerId} layer of tile ${tileId} of map ${mapId}.`); - return; + let features = new Array(); + for (let id of tileFeatureIds) { + if (typeof id == "string") { + // When clicking on geometry that respresents a highlight, + // this is reflected in the feature id. By processing this + // info here, a hover highlight can be turned into a selection. + if (id == "hover-highlight") { + features = this.hoverTopic.getValue(); + } + else if (id == "selection-highlight") { + features = this.selectionTopic.getValue(); + } + continue; + } + + const tile = tiles.get(id?.mapTileKey || ""); + if (!tile || !id?.featureId) { + continue; + } + if (!tile.has(id?.featureId || "")) { + const [mapId, layerId, tileId] = coreLib.parseTileFeatureLayerKey(id?.mapTileKey || ""); + this.messageService.showError( + `The feature ${id?.featureId} does not exist in the ${layerId} layer of tile ${tileId} of map ${mapId}.`); + continue; + } + + features.push(new FeatureWrapper(id!.featureId, tile)); + } + + if (mode == coreLib.HighlightMode.HOVER_HIGHLIGHT) { + if (features.length) { + if (featureSetsEqual(this.selectionTopic.getValue(), features)) { + return; + } + } + if (featureSetsEqual(this.hoverTopic.getValue(), features)) { + return; + } + this.hoverTopic.next(features); + } + else if (mode == coreLib.HighlightMode.SELECTION_HIGHLIGHT) { + if (featureSetsEqual(this.selectionTopic.getValue(), features)) { + return; + } + if (featureSetsEqual(this.hoverTopic.getValue(), features)) { + this.hoverTopic.next([]); + } + this.selectionTopic.next(features); + } + else { + console.error(`Unsupported highlight mode!`); } - const feature = new FeatureWrapper(featureId, tile); - this.selectionTopic.next([feature]); - if (focus) { - this.focusOnFeature(feature); + // TODO: Focus on bounding box of all features? + if (focus && features.length) { + this.focusOnFeature(features[0]); } } @@ -687,14 +750,6 @@ export class MapService { this.zoomLevel.next(MAX_ZOOM_LEVEL); } - resolveFeature(id: TileFeatureId|null) { - const tile = this.loadedTileLayers.get(id?.mapTileKey || ""); - if (!tile || !id?.featureId) { - return null; - } - return new FeatureWrapper(id.featureId, tile); - } - private visualizeHighlights(mode: HighlightMode, featureWrappers: Array) { let visualizationCollection = null; switch (mode) { @@ -719,7 +774,7 @@ export class MapService { return; } - // Apply additional highlight styles. + // Apply highlight styles. const featureTile = featureWrappers[0].featureTile; const featureIds = featureWrappers.map(fw => fw.featureId); for (let [_, styleData] of this.styleService.styles) { diff --git a/erdblick_app/app/parameters.service.ts b/erdblick_app/app/parameters.service.ts index 781bb323..1b6e9444 100644 --- a/erdblick_app/app/parameters.service.ts +++ b/erdblick_app/app/parameters.service.ts @@ -8,6 +8,15 @@ import {InspectionService, SelectedSourceData} from "./inspection.service"; export const MAX_NUM_TILES_TO_LOAD = 2048; export const MAX_NUM_TILES_TO_VISUALIZE = 512; +/** + * Combination of a tile id and a feature id, which may be resolved + * to a feature object. + */ +export interface TileFeatureId { + featureId: string, + mapTileKey: string, +} + export interface StyleParameters { visible: boolean, options: Record, @@ -18,7 +27,7 @@ interface ErdblickParameters extends Record { search: [number, string] | [], marker: boolean, markedPosition: Array, - selected: Array, + selected: TileFeatureId[], heading: number, pitch: number, roll: number, @@ -46,6 +55,22 @@ interface ParameterDescriptor { urlParam: boolean } +/** Function to create an object validator given a key-typeof-value dictionary. */ +function validateObject(fields: Record) { + return (o: object) => { + if (typeof o !== "object") { + return false; + } + for (let [key, value] of Object.entries(o)) { + let valueType = typeof value; + if (valueType !== fields[key]) { + return false; + } + } + return true; + }; +} + const erdblickParameters: Record = { search: { converter: val => JSON.parse(val), @@ -67,7 +92,7 @@ const erdblickParameters: Record = { }, selected: { converter: val => JSON.parse(val), - validator: val => Array.isArray(val) && val.every(item => typeof item === 'string'), + validator: val => Array.isArray(val) && val.every(validateObject({mapTileKey: "string", featureId: "string"})), default: [], urlParam: true }, @@ -127,8 +152,11 @@ const erdblickParameters: Record = { }, styles: { converter: val => JSON.parse(val), - validator: val => typeof val === "object" && Object.entries(val as Record).every(([_, v]) => typeof v["visible"] === "boolean" && typeof v["showOptions"] === "boolean" && typeof v["options"] === "object"), - default: new Map(), + validator: val => { + return typeof val === "object" && Object.entries(val as Record).every( + ([_, v]) => validateObject({visible: "boolean", showOptions: "boolean", options: "object"})(v)); + }, + default: {}, urlParam: true }, tilesLoadLimit: { @@ -248,18 +276,27 @@ export class ParametersService { this.parameters.next(this.p()); } - setSelectedFeature(mapId: string, featureId: string) { + setSelectedFeatures(newSelection: TileFeatureId[]) { const currentSelection = this.p().selected; - if (currentSelection && (currentSelection[0] != mapId || currentSelection[1] != featureId)) { - this.p().selected = [mapId, featureId]; - this._replaceUrl = false; - this.parameters.next(this.p()); + if (currentSelection.length == newSelection.length) { + let selectedFeaturesAreSame = true; + for (let i = 0; i < currentSelection.length; ++i) { + const a = currentSelection[i]; + const b = newSelection[i]; + if (a.featureId != b.featureId || a.mapTileKey != b.mapTileKey) { + selectedFeaturesAreSame = false; + break; + } + } + + if (selectedFeaturesAreSame) { + return false; + } } - } - unsetSelectedFeature() { - this.p().selected = []; + this.p().selected = newSelection; this.parameters.next(this.p()); + return true; } setMarkerState(enabled: boolean) { @@ -271,7 +308,7 @@ export class ParametersService { } } - setMarkerPosition(position: Cartographic | null) { + setMarkerPosition(position: Cartographic | null, delayUpdate: boolean=false) { if (position) { const longitude = CesiumMath.toDegrees(position.longitude); const latitude = CesiumMath.toDegrees(position.latitude); @@ -279,7 +316,9 @@ export class ParametersService { } else { this.p().markedPosition = []; } - this.parameters.next(this.p()); + if (!delayUpdate) { + this.parameters.next(this.p()); + } } mapLayerConfig(mapId: string, layerId: string, fallbackLevel: number): [boolean, number, boolean] { diff --git a/erdblick_app/app/pointmerge.service.ts b/erdblick_app/app/pointmerge.service.ts index 27283753..e84a44d1 100644 --- a/erdblick_app/app/pointmerge.service.ts +++ b/erdblick_app/app/pointmerge.service.ts @@ -1,7 +1,7 @@ import {Injectable} from "@angular/core"; import {PointPrimitiveCollection, LabelCollection, Viewer} from "./cesium"; import {coreLib} from "./wasm"; -import {TileFeatureId} from "./map.service"; +import {TileFeatureId} from "./parameters.service"; type MapLayerStyleRule = string; type PositionHash = string; diff --git a/erdblick_app/app/sourcedata.panel.component.ts b/erdblick_app/app/sourcedata.panel.component.ts index 3214d194..4cbabe3d 100644 --- a/erdblick_app/app/sourcedata.panel.component.ts +++ b/erdblick_app/app/sourcedata.panel.component.ts @@ -61,7 +61,7 @@ import {Menu} from "primeng/menu"; - + diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index 62c7314e..db2af622 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -33,17 +33,12 @@ import {distinctUntilChanged} from "rxjs"; import {SearchResultPosition} from "./featurefilter.worker"; import {InspectionService} from "./inspection.service"; import {KeyboardService} from "./keyboard.service"; +import {core} from "@angular/compiler"; +import {coreLib} from "./wasm"; // Redeclare window with extended interface declare let window: DebugWindow; -/** - * Determine if two lists of feature wrappers have the same features. - */ -function featureSetsEqual(rhs: FeatureWrapper[], lhs: FeatureWrapper[]) { - return rhs.length === lhs.length && rhs.every(rf => lhs.some(lf => rf.equals(lf))); -} - @Component({ selector: 'erdblick-view', template: ` @@ -139,9 +134,9 @@ export class ErdblickViewComponent implements AfterViewInit { this.mouseHandler.setInputAction((movement: any) => { const position = movement.position; const coordinates = this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); - if (coordinates !== undefined) { - this.coordinatesService.mouseClickCoordinates.next(Cartographic.fromCartesian(coordinates)); - } + // if (coordinates !== undefined) { + // this.coordinatesService.mouseClickCoordinates.next(Cartographic.fromCartesian(coordinates)); + // } let feature = this.viewer.scene.pick(position); if (defined(feature) && feature.primitive instanceof Billboard && feature.primitive.id.type === "SearchResult") { if (feature.primitive.id) { @@ -161,7 +156,10 @@ export class ErdblickViewComponent implements AfterViewInit { }); } } - this.setPickedCesiumFeature(feature); + this.mapService.highlightFeatures( + Array.isArray(feature?.id) ? feature.id : [feature?.id], + false, + coreLib.HighlightMode.SELECTION_HIGHLIGHT).then(); }, ScreenSpaceEventType.LEFT_CLICK); // Add a handler for hover (i.e., MOUSE_MOVE) functionality. @@ -172,7 +170,10 @@ export class ErdblickViewComponent implements AfterViewInit { this.coordinatesService.mouseMoveCoordinates.next(Cartographic.fromCartesian(coordinates)) } let feature = this.viewer.scene.pick(position); - this.setHoveredCesiumFeature(feature); + this.mapService.highlightFeatures( + Array.isArray(feature?.id) ? feature.id : [feature?.id], + false, + coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); }, ScreenSpaceEventType.MOUSE_MOVE); // Add a handler for camera movement. @@ -260,68 +261,6 @@ export class ErdblickViewComponent implements AfterViewInit { this.keyboardService.registerShortcuts(['r', 'R'], this.resetOrientation.bind(this)); } - /** - * Set or re-set the hovered feature. - */ - private setHoveredCesiumFeature(feature: any) { - // Get the actual mapget features for the picked Cesium feature. - let resolvedFeatures = this.resolveMapgetFeatures(feature); - if (!resolvedFeatures.length) { - this.mapService.hoverTopic.next([]); - return; - } - - if (featureSetsEqual(this.mapService.selectionTopic.getValue(), resolvedFeatures)) { - return; - } - if (featureSetsEqual(this.mapService.hoverTopic.getValue(), resolvedFeatures)) { - return; - } - - this.mapService.hoverTopic.next(resolvedFeatures); - } - - /** - * Set or re-set the picked feature. - */ - private setPickedCesiumFeature(feature: any) { - // Get the actual mapget features for the picked Cesium feature. - let resolvedFeatures = this.resolveMapgetFeatures(feature); - if (!resolvedFeatures.length) { - this.mapService.selectionTopic.next([]); - return; - } - - if (featureSetsEqual(this.mapService.selectionTopic.getValue(), resolvedFeatures)) { - return; - } - if (featureSetsEqual(this.mapService.hoverTopic.getValue(), resolvedFeatures)) { - this.setHoveredCesiumFeature(null); - } - - this.mapService.selectionTopic.next(resolvedFeatures); - } - - /** - * Resolve a Cesium primitive feature ID to a list of mapget FeatureWrappers. - */ - private resolveMapgetFeatures(feature: any) { - let resolvedFeatures: FeatureWrapper[] = []; - for (const fid of Array.isArray(feature?.id) ? feature.id : [feature?.id]) { - if (fid == "hover-highlight") { - return this.mapService.hoverTopic.getValue(); - } - if (fid == "selection-highlight") { - return this.mapService.selectionTopic.getValue(); - } - const resolvedFeature = this.mapService.resolveFeature(fid); - if (resolvedFeature) { - resolvedFeatures.push(resolvedFeature); - } - } - return resolvedFeatures; - } - /** * Update the visible viewport, and communicate it to the model. */ diff --git a/test/test-visualization.cpp b/test/test-visualization.cpp index f13a469e..53fb1599 100644 --- a/test/test-visualization.cpp +++ b/test/test-visualization.cpp @@ -14,7 +14,7 @@ TEST_CASE("FeatureLayerVisualization", "[erdblick.renderer]") TileLayerParser tlp; auto testLayer = TestDataProvider(tlp).getTestLayer(42., 11., 13); auto style = TestDataProvider::style(); - FeatureLayerVisualization visualization(style, {}); + FeatureLayerVisualization visualization("Features:Test:Test:0", style, {}, {}); visualization.addTileFeatureLayer(testLayer); visualization.run(); auto result = visualization.primitiveCollection(); From 3c8378c481229d9c9ad5514b4c7afe5e99eb74f6 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 4 Sep 2024 14:49:50 +0200 Subject: [PATCH 30/37] Additional test fixes. --- erdblick_app/app/inspection.service.ts | 2 +- libs/core/include/erdblick/inspection.h | 2 +- libs/core/include/erdblick/rule.h | 2 +- libs/core/include/erdblick/visualization.h | 2 +- libs/core/src/inspection.cpp | 23 ++++++++++++---------- libs/core/src/visualization.cpp | 2 +- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index bcb6b577..99879cb2 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -103,9 +103,9 @@ export class InspectionService { radiusPoint = Cartesian3.fromDegrees(radiusPoint.x, radiusPoint.y, radiusPoint.z); this.selectedFeatureBoundingRadius = Cartesian3.distance(this.selectedFeatureOrigin, radiusPoint); this.selectedFeatureGeometryType = feature.getGeometryType() as any;this.isInspectionPanelVisible = true; - this.loadFeatureData(); }); }); + this.loadFeatureData(); this.parametersService.setSelectedFeatures(this.selectedFeatures.map(f => f.key())); }); diff --git a/libs/core/include/erdblick/inspection.h b/libs/core/include/erdblick/inspection.h index 8e89b968..a7264c64 100644 --- a/libs/core/include/erdblick/inspection.h +++ b/libs/core/include/erdblick/inspection.h @@ -29,7 +29,7 @@ class InspectionConverter JsValue key_; JsValue value_; ValueType type_ = ValueType::Null; - std::string hoverId_; + std::string hoverId_; // For highlight attribs/relations on hovering. std::string info_; std::vector children_; JsValue direction_; diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index 19fe31b6..e3338d95 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -166,7 +166,7 @@ class FeatureStyleRule std::vector firstOfRules_; // Index of the rule within the style sheet - int32_t index_ = 0; + uint32_t index_ = 0; }; } diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h index a593f2e1..bfe5bcd4 100644 --- a/libs/core/include/erdblick/visualization.h +++ b/libs/core/include/erdblick/visualization.h @@ -250,7 +250,7 @@ class FeatureLayerVisualization * Get a unique identifier for the map+layer+style+rule-id+highlight-mode. * In combination with a tile id, this uniquely identifiers a merged corner tile. */ - std::string getMapLayerStyleRuleId(const uint32_t& ruleIndex) const; + std::string getMapLayerStyleRuleId(uint32_t ruleIndex) const; /// =========== Generic Members =========== diff --git a/libs/core/src/inspection.cpp b/libs/core/src/inspection.cpp index 3a8e6c8b..1f3fc996 100644 --- a/libs/core/src/inspection.cpp +++ b/libs/core/src/inspection.cpp @@ -61,15 +61,15 @@ JsValue InspectionConverter::convert(model_ptr const& featurePtr) push("layerId", "layerId", ValueType::String)->value_ = convertStringView(featurePtr->model().layerInfo()->layerId_); // TODO: Investigate and fix the issue for "index out of bounds" error. - // Affects boundaries and lane connectors -// if (auto prefix = featurePtr->model().getIdPrefix()) { -// for (auto const& [k, v] : prefix->fields()) { -// convertField(k, v); -// } -// } -// for (auto const& [k, v] : featurePtr->id()->fields()) { -// convertField(k, v); -// } + // Affects boundaries and lane connectors + // if (auto prefix = featurePtr->model().getIdPrefix()) { + // for (auto const& [k, v] : prefix->fields()) { + // convertField(k, v); + // } + // } + // for (auto const& [k, v] : featurePtr->id()->fields()) { + // convertField(k, v); + // } for (auto const& [key, value]: featurePtr->id()->keyValuePairs()) { auto &field = current_->children_.emplace_back(); @@ -241,7 +241,10 @@ void InspectionConverter::convertRelation(const model_ptr& r) } auto relGroupScope = push(relGroup); auto relScope = push(JsValue(relGroup->children_.size()), nextRelationIndex_, ValueType::FeatureId); - relScope->value_ = JsValue(r->target()->toString()); + relScope->value_ = JsValue::Dict({ + {"mapTileKey", JsValue(r->model().id().toString())}, + {"featureId", JsValue(r->target()->toString())}, + }); relScope->hoverId_ = featureId_+":relation#"+std::to_string(nextRelationIndex_); convertSourceDataReferences(r->sourceDataReferences(), *relScope); if (r->hasSourceValidity()) { diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index 9b103fc0..d4a7b2d8 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -111,7 +111,7 @@ void FeatureLayerVisualization::run() } } -std::string FeatureLayerVisualization::getMapLayerStyleRuleId(uint32_t const& ruleIndex) const +std::string FeatureLayerVisualization::getMapLayerStyleRuleId(uint32_t ruleIndex) const { return fmt::format( "{}:{}:{}:{}:{}", From 7827e1fb451f82bc8ddc6ed45e88781ca2dd06bd Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 4 Sep 2024 15:23:29 +0200 Subject: [PATCH 31/37] Update default style. --- cmake/cesium.cmake | 2 +- cmake/{draco.patch => cesium.patch} | 0 config/styles/default-style.yaml | 20 ++++++++++---------- 3 files changed, 11 insertions(+), 11 deletions(-) rename cmake/{draco.patch => cesium.patch} (100%) diff --git a/cmake/cesium.cmake b/cmake/cesium.cmake index 7609829d..59a948ed 100644 --- a/cmake/cesium.cmake +++ b/cmake/cesium.cmake @@ -24,7 +24,7 @@ FetchContent_Declare(cesiumnative_src GIT_TAG "main" GIT_SUBMODULES_RECURSE YES GIT_PROGRESS YES - PATCH_COMMAND git reset --hard HEAD && git -C extern/draco reset --hard HEAD && git apply "${CMAKE_CURRENT_SOURCE_DIR}/cmake/draco.patch" + PATCH_COMMAND git reset --hard HEAD && git -C extern/draco reset --hard HEAD && git apply "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cesium.patch" UPDATE_DISCONNECTED YES UPDATE_COMMAND "") diff --git a/cmake/draco.patch b/cmake/cesium.patch similarity index 100% rename from cmake/draco.patch rename to cmake/cesium.patch diff --git a/config/styles/default-style.yaml b/config/styles/default-style.yaml index 5fa6c793..78d30bc6 100644 --- a/config/styles/default-style.yaml +++ b/config/styles/default-style.yaml @@ -11,17 +11,19 @@ options: rules: # Normal styles - geometry: ["mesh", "polygon"] - filter: "showMesh" + filter: showMesh color: teal opacity: 0.8 offset: [0, 0, -0.5] - geometry: ["point"] - filter: "showPoint" - color: moccasin + point-merge-grid-cell: [0.000000084, 0.000000084, 0.01] + filter: showPoint + color-expression: "$mergeCount > 1 and 'red' or 'moccasin'" opacity: 1.0 - width: 1.0 + width: 10.0 + # label-text-expression: "$mergeCount" - geometry: ["line"] - filter: "showLine" + filter: showLine color: moccasin opacity: 1.0 width: 5.0 @@ -32,20 +34,18 @@ rules: opacity: 1.0 width: 4.0 mode: hover - offset: [0, 0, -0.5] - geometry: ["point", "line"] color: yellow opacity: 1.0 - width: 4.0 + width: 20.0 mode: hover - geometry: ["mesh", "polygon"] color: red opacity: 1.0 width: 4.0 mode: selection - offset: [0, 0, -0.5] - geometry: ["point", "line"] - color: red + color: yellow opacity: 1.0 - width: 4.0 + width: 20.0 mode: selection From 9961eff59214b50e115d9b8187512060ae6e02d4 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 4 Sep 2024 19:13:25 +0200 Subject: [PATCH 32/37] Improved (hover-)highlight/(re-)selection behavior. Still got some bugs. --- erdblick_app/app/feature.panel.component.ts | 12 +++++++----- erdblick_app/app/inspection.service.ts | 11 ++++++++--- erdblick_app/app/jump.service.ts | 1 - erdblick_app/app/map.service.ts | 18 +++++++++++------- erdblick_app/app/parameters.service.ts | 2 ++ erdblick_app/app/view.component.ts | 12 ++++++++---- libs/core/include/erdblick/inspection.h | 1 + libs/core/src/inspection.cpp | 6 ++---- 8 files changed, 39 insertions(+), 24 deletions(-) diff --git a/erdblick_app/app/feature.panel.component.ts b/erdblick_app/app/feature.panel.component.ts index 1cfa7559..918eb93a 100644 --- a/erdblick_app/app/feature.panel.component.ts +++ b/erdblick_app/app/feature.panel.component.ts @@ -369,8 +369,8 @@ export class FeaturePanelComponent implements OnInit { if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { this.jumpService.highlightByJumpTargetFilter( - rowData["value"].mapTileKey, - rowData["value"].featureId).then(); + rowData["mapId"], + rowData["value"]).then(); } } @@ -378,15 +378,17 @@ export class FeaturePanelComponent implements OnInit { event.stopPropagation(); if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { this.jumpService.highlightByJumpTargetFilter( - rowData["value"].mapTileKey, - rowData["value"].featureId, + rowData["mapId"], + rowData["value"], coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); } } onValueHoverExit(event: any, rowData: any) { event.stopPropagation(); - return; + if (rowData["type"] == this.InspectionValueType.FEATUREID.value) { + this.mapService.highlightFeatures([], false, coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); + } } getStyleClassByType(valueType: number): string { diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index 99879cb2..7529a5f6 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -11,6 +11,7 @@ import {Cartesian3} from "./cesium"; import {InfoMessageService} from "./info.service"; import {KeyboardService} from "./keyboard.service"; import {Fetch} from "./fetch.model"; +import {core} from "@angular/compiler"; interface InspectionModelData { @@ -20,6 +21,7 @@ interface InspectionModelData { info?: string; hoverId?: string geoJsonPath?: string; + mapId?: string; sourceDataReferences?: Array; children: Array; } @@ -124,9 +126,9 @@ export class InspectionService { for (const data of dataNodes) { const node: TreeTableNode = {}; let value = data.value; - if (data.type == this.InspectionValueType.NULL.value && data.children === undefined) { + if (data.type == coreLib.ValueType.NULL.value && data.children === undefined) { value = "NULL"; - } else if ((data.type & 128) == 128 && (data.type - 128) == 1) { + } else if ((data.type & coreLib.ValueType.ARRAY.value) && (data.type & coreLib.ValueType.NUMBER.value)) { for (let i = 0; i < value.length; i++) { if (!Number.isInteger(value[i])) { const strValue = String(value[i]) @@ -138,7 +140,7 @@ export class InspectionService { } } - if ((data.type & 128) == 128) { + if (data.type & coreLib.ValueType.ARRAY.value) { value = value.join(", "); } @@ -153,6 +155,9 @@ export class InspectionService { if (data.hasOwnProperty("hoverId")) { node.data["hoverId"] = data.hoverId; } + if (data.hasOwnProperty("mapId")) { + node.data["mapId"] = data.value["mapId"]; + } if (data.hasOwnProperty("geoJsonPath")) { node.data["geoJsonPath"] = data.geoJsonPath; } diff --git a/erdblick_app/app/jump.service.ts b/erdblick_app/app/jump.service.ts index d8ba8744..b8c48e85 100644 --- a/erdblick_app/app/jump.service.ts +++ b/erdblick_app/app/jump.service.ts @@ -136,7 +136,6 @@ export class JumpTargetService { ]); } - /** Select */ async highlightByJumpTargetFilter(mapId: string, featureId: string, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { let featureJumpTargets = this.mapService.tileParser?.filterFeatureJumpTargets(featureId) as Array; const validIndex = featureJumpTargets.findIndex(action => !action.error); diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index 6f4234c7..fddcad2b 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -395,7 +395,7 @@ export class MapService { // Evict present non-required tile layers. let newTileLayers = new Map(); let evictTileLayer = (tileLayer: FeatureTile) => { - return !tileLayer.preventCulling && (!this.currentVisibleTileIds.has(tileLayer.tileId) || + return !tileLayer.preventCulling && !this.selectionTopic.getValue().some(v => v.featureTile == tileLayer) && (!this.currentVisibleTileIds.has(tileLayer.tileId) || !this.getMapLayerVisibility(tileLayer.mapName, tileLayer.layerName) || tileLayer.level() != this.getMapLayerLevel(tileLayer.mapName, tileLayer.layerName)) } @@ -661,16 +661,14 @@ export class MapService { }) this.update(); - await selectionTilePromise; + tile = await selectionTilePromise; + result.set(tileKey, tile); } return result; } async highlightFeatures(tileFeatureIds: (TileFeatureId|null|string)[], focus: boolean=false, mode: HighlightMode=coreLib.HighlightMode.SELECTION_HIGHLIGHT) { - if (mode == coreLib.HighlightMode.SELECTION_HIGHLIGHT) - console.trace(tileFeatureIds) - // Load the tiles for the selection. const tiles = await this.loadTiles( new Set(tileFeatureIds.filter(s => typeof s !== "string").map(s => s?.mapTileKey || null))); @@ -679,7 +677,7 @@ export class MapService { let features = new Array(); for (let id of tileFeatureIds) { if (typeof id == "string") { - // When clicking on geometry that respresents a highlight, + // When clicking on geometry that represents a highlight, // this is reflected in the feature id. By processing this // info here, a hover highlight can be turned into a selection. if (id == "hover-highlight") { @@ -691,8 +689,13 @@ export class MapService { continue; } + if (!id?.featureId) { + continue; + } + const tile = tiles.get(id?.mapTileKey || ""); - if (!tile || !id?.featureId) { + if (!tile) { + console.error(`Could not load tile ${id?.mapTileKey} for highlighting!`); continue; } if (!tile.has(id?.featureId || "")) { @@ -705,6 +708,7 @@ export class MapService { features.push(new FeatureWrapper(id!.featureId, tile)); } + console.trace(`features: ${features}`) if (mode == coreLib.HighlightMode.HOVER_HIGHLIGHT) { if (features.length) { if (featureSetsEqual(this.selectionTopic.getValue(), features)) { diff --git a/erdblick_app/app/parameters.service.ts b/erdblick_app/app/parameters.service.ts index 1b6e9444..f2b76330 100644 --- a/erdblick_app/app/parameters.service.ts +++ b/erdblick_app/app/parameters.service.ts @@ -295,6 +295,7 @@ export class ParametersService { } this.p().selected = newSelection; + this._replaceUrl = false; this.parameters.next(this.p()); return true; } @@ -317,6 +318,7 @@ export class ParametersService { this.p().markedPosition = []; } if (!delayUpdate) { + this._replaceUrl = false; this.parameters.next(this.p()); } } diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index db2af622..3c1703ef 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -134,6 +134,8 @@ export class ErdblickViewComponent implements AfterViewInit { this.mouseHandler.setInputAction((movement: any) => { const position = movement.position; const coordinates = this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); + // TODO: FIXME before release: Reactivate. Currently, this leads to + // a parameter update race condition, which undoes the subsequent selection. // if (coordinates !== undefined) { // this.coordinatesService.mouseClickCoordinates.next(Cartographic.fromCartesian(coordinates)); // } @@ -170,10 +172,12 @@ export class ErdblickViewComponent implements AfterViewInit { this.coordinatesService.mouseMoveCoordinates.next(Cartographic.fromCartesian(coordinates)) } let feature = this.viewer.scene.pick(position); - this.mapService.highlightFeatures( - Array.isArray(feature?.id) ? feature.id : [feature?.id], - false, - coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); + // TODO: FIXME before release: Reactivate. + // console.log("Mouse Move (Canvas)"); + // this.mapService.highlightFeatures( + // Array.isArray(feature?.id) ? feature.id : [feature?.id], + // false, + // coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); }, ScreenSpaceEventType.MOUSE_MOVE); // Add a handler for camera movement. diff --git a/libs/core/include/erdblick/inspection.h b/libs/core/include/erdblick/inspection.h index a7264c64..96ea391b 100644 --- a/libs/core/include/erdblick/inspection.h +++ b/libs/core/include/erdblick/inspection.h @@ -28,6 +28,7 @@ class InspectionConverter { JsValue key_; JsValue value_; + JsValue mapId_; ValueType type_ = ValueType::Null; std::string hoverId_; // For highlight attribs/relations on hovering. std::string info_; diff --git a/libs/core/src/inspection.cpp b/libs/core/src/inspection.cpp index 1f3fc996..5f3d3bcb 100644 --- a/libs/core/src/inspection.cpp +++ b/libs/core/src/inspection.cpp @@ -241,10 +241,8 @@ void InspectionConverter::convertRelation(const model_ptr& r) } auto relGroupScope = push(relGroup); auto relScope = push(JsValue(relGroup->children_.size()), nextRelationIndex_, ValueType::FeatureId); - relScope->value_ = JsValue::Dict({ - {"mapTileKey", JsValue(r->model().id().toString())}, - {"featureId", JsValue(r->target()->toString())}, - }); + relScope->value_ = JsValue(r->target()->toString()); + relScope->mapId_ = JsValue(r->model().mapId()); relScope->hoverId_ = featureId_+":relation#"+std::to_string(nextRelationIndex_); convertSourceDataReferences(r->sourceDataReferences(), *relScope); if (r->hasSourceValidity()) { From 702cc243b552a206f212547eb0dd911a97c71369 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 5 Sep 2024 06:25:03 +0200 Subject: [PATCH 33/37] Run tests in test folder. --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 31a7bcb5..46955151 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -23,5 +23,5 @@ jobs: - name: Run Tests run: | - cd build-test + cd build-test/test ctest --verbose --no-tests=error From c10e6880022454ed4a18e3b4083cafb434eb814c Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 5 Sep 2024 18:52:02 +0200 Subject: [PATCH 34/37] Fix picking/highlighting bugs. --- erdblick_app/app/features.model.ts | 1 + erdblick_app/app/map.service.ts | 25 +++++++++++-------------- erdblick_app/app/style.service.ts | 1 - erdblick_app/app/view.component.ts | 28 ++++++++++++++++------------ 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/erdblick_app/app/features.model.ts b/erdblick_app/app/features.model.ts index 44413509..eac39680 100644 --- a/erdblick_app/app/features.model.ts +++ b/erdblick_app/app/features.model.ts @@ -193,6 +193,7 @@ export class FeatureWrapper { return this.featureTile.mapTileKey == other.featureTile.mapTileKey && this.featureId == other.featureId; } + /** Returns the cross-map-layer global ID for this feature. */ key(): TileFeatureId { return { mapTileKey: this.featureTile.mapTileKey, diff --git a/erdblick_app/app/map.service.ts b/erdblick_app/app/map.service.ts index fddcad2b..71fec9c4 100644 --- a/erdblick_app/app/map.service.ts +++ b/erdblick_app/app/map.service.ts @@ -83,7 +83,7 @@ export class MapService { public maps: BehaviorSubject> = new BehaviorSubject>(new Map()); public loadedTileLayers: Map; private visualizedTileLayers: Map; - private currentFetch: any; + private currentFetch: Fetch|null = null; private currentViewport: ViewportProperties; private currentVisibleTileIds: Set; private currentHighDetailTileIds: Set; @@ -395,7 +395,7 @@ export class MapService { // Evict present non-required tile layers. let newTileLayers = new Map(); let evictTileLayer = (tileLayer: FeatureTile) => { - return !tileLayer.preventCulling && !this.selectionTopic.getValue().some(v => v.featureTile == tileLayer) && (!this.currentVisibleTileIds.has(tileLayer.tileId) || + return !tileLayer.preventCulling && !this.selectionTopic.getValue().some(v => v.featureTile.mapTileKey == tileLayer.mapTileKey) && (!this.currentVisibleTileIds.has(tileLayer.tileId) || !this.getMapLayerVisibility(tileLayer.mapName, tileLayer.layerName) || tileLayer.level() != this.getMapLayerLevel(tileLayer.mapName, tileLayer.layerName)) } @@ -533,9 +533,8 @@ export class MapService { this.selectionTileRequest.resolve!(tileLayer); this.selectionTileRequest = null; } - // Don't add a tile that is not supposed to be visible. - if (!preventCulling) { + else if (!preventCulling) { if (!this.currentVisibleTileIds.has(tileLayer.tileId)) return; } @@ -543,7 +542,7 @@ export class MapService { // If this one replaces an older tile with the same key, // then first remove the older existing one. if (this.loadedTileLayers.has(tileLayer.mapTileKey)) { - this.removeTileLayer(this.loadedTileLayers.get(tileLayer.mapTileKey)); + this.removeTileLayer(this.loadedTileLayers.get(tileLayer.mapTileKey)!); } this.loadedTileLayers.set(tileLayer.mapTileKey, tileLayer); @@ -560,11 +559,11 @@ export class MapService { }); } - private removeTileLayer(tileLayer: any) { - tileLayer.destroy() + private removeTileLayer(tileLayer: FeatureTile) { + tileLayer.destroy(); for (const styleId of this.visualizedTileLayers.keys()) { const tileVisus = this.visualizedTileLayers.get(styleId)?.filter(tileVisu => { - if (tileVisu.tile.mapTileKey === tileLayer.id) { + if (tileVisu.tile.mapTileKey === tileLayer.mapTileKey) { this.tileVisualizationDestructionTopic.next(tileVisu); return false; } @@ -577,9 +576,9 @@ export class MapService { } } this.tileVisualizationQueue = this.tileVisualizationQueue.filter(([_, tileVisu]) => { - return tileVisu.tile.mapTileKey !== tileLayer.id; + return tileVisu.tile.mapTileKey !== tileLayer.mapTileKey; }); - this.loadedTileLayers.delete(tileLayer.id); + this.loadedTileLayers.delete(tileLayer.mapTileKey); } private renderTileLayer(tileLayer: FeatureTile, style: ErdblickStyle|FeatureLayerStyle, styleId: string = "") { @@ -609,7 +608,7 @@ export class MapService { } } - setViewport(viewport: any) { + setViewport(viewport: ViewportProperties) { this.currentViewport = viewport; this.setTileLevelForViewport(); this.update(); @@ -708,7 +707,6 @@ export class MapService { features.push(new FeatureWrapper(id!.featureId, tile)); } - console.trace(`features: ${features}`) if (mode == coreLib.HighlightMode.HOVER_HIGHLIGHT) { if (features.length) { if (featureSetsEqual(this.selectionTopic.getValue(), features)) { @@ -759,8 +757,7 @@ export class MapService { switch (mode) { case coreLib.HighlightMode.SELECTION_HIGHLIGHT: if (this.sidePanelService.panel != SidePanelState.FEATURESEARCH) { - this.sidePanelService.panel = SidePanelState.NONE -; + this.sidePanelService.panel = SidePanelState.NONE; } visualizationCollection = this.selectionVisualizations; break; diff --git a/erdblick_app/app/style.service.ts b/erdblick_app/app/style.service.ts index a01e08a5..15f1986b 100644 --- a/erdblick_app/app/style.service.ts +++ b/erdblick_app/app/style.service.ts @@ -363,7 +363,6 @@ export class StyleService { if (style.params.visible) { this.styleAddedForId.next(styleId); } - console.log(`${style.params.visible ? 'Activated' : 'Deactivated'} style: ${styleId}.`); } reapplyStyles(styleIds: Array) { diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index 3c1703ef..df47d412 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -133,12 +133,6 @@ export class ErdblickViewComponent implements AfterViewInit { // Add a handler for selection. this.mouseHandler.setInputAction((movement: any) => { const position = movement.position; - const coordinates = this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); - // TODO: FIXME before release: Reactivate. Currently, this leads to - // a parameter update race condition, which undoes the subsequent selection. - // if (coordinates !== undefined) { - // this.coordinatesService.mouseClickCoordinates.next(Cartographic.fromCartesian(coordinates)); - // } let feature = this.viewer.scene.pick(position); if (defined(feature) && feature.primitive instanceof Billboard && feature.primitive.id.type === "SearchResult") { if (feature.primitive.id) { @@ -162,22 +156,32 @@ export class ErdblickViewComponent implements AfterViewInit { Array.isArray(feature?.id) ? feature.id : [feature?.id], false, coreLib.HighlightMode.SELECTION_HIGHLIGHT).then(); + // Handle position update after highlighting, because otherwise + // there is a race condition between the parameter updates for + // feature selection and position update. + const coordinates = this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); + if (coordinates !== undefined) { + this.coordinatesService.mouseClickCoordinates.next(Cartographic.fromCartesian(coordinates)); + } }, ScreenSpaceEventType.LEFT_CLICK); // Add a handler for hover (i.e., MOUSE_MOVE) functionality. this.mouseHandler.setInputAction((movement: any) => { const position = movement.endPosition; // Notice that for MOUSE_MOVE, it's endPosition + // Do not handle mouse move here, if the first element + // under the cursor is not the Cesium view. + if (document.elementFromPoint(position.x, position.y)?.tagName.toLowerCase() !== "canvas") { + return; + } const coordinates = this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid); if (coordinates !== undefined) { this.coordinatesService.mouseMoveCoordinates.next(Cartographic.fromCartesian(coordinates)) } let feature = this.viewer.scene.pick(position); - // TODO: FIXME before release: Reactivate. - // console.log("Mouse Move (Canvas)"); - // this.mapService.highlightFeatures( - // Array.isArray(feature?.id) ? feature.id : [feature?.id], - // false, - // coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); + this.mapService.highlightFeatures( + Array.isArray(feature?.id) ? feature.id : [feature?.id], + false, + coreLib.HighlightMode.HOVER_HIGHLIGHT).then(); }, ScreenSpaceEventType.MOUSE_MOVE); // Add a handler for camera movement. From e82482c7618aa507d59f0d4d5232826d591cb98f Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 5 Sep 2024 19:35:46 +0200 Subject: [PATCH 35/37] Update default style to improve z-ordering. --- config/styles/default-style.yaml | 44 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/config/styles/default-style.yaml b/config/styles/default-style.yaml index 78d30bc6..740ef594 100644 --- a/config/styles/default-style.yaml +++ b/config/styles/default-style.yaml @@ -14,38 +14,52 @@ rules: filter: showMesh color: teal opacity: 0.8 - offset: [0, 0, -0.5] - - geometry: ["point"] - point-merge-grid-cell: [0.000000084, 0.000000084, 0.01] - filter: showPoint - color-expression: "$mergeCount > 1 and 'red' or 'moccasin'" - opacity: 1.0 - width: 10.0 - # label-text-expression: "$mergeCount" + offset: [0, 0, -0.4] - geometry: ["line"] filter: showLine color: moccasin opacity: 1.0 width: 5.0 + offset: [0, 0, -0.2] + - geometry: ["point"] + point-merge-grid-cell: [0.000000084, 0.000000084, 0.01] + filter: showPoint + color-expression: "$mergeCount > 1 and 'brown' or 'moccasin'" + opacity: 1.0 + width: 15.0 # Hover/Selection styles - geometry: ["mesh", "polygon"] - color: yellow + color: orange opacity: 1.0 - width: 4.0 mode: hover - - geometry: ["point", "line"] - color: yellow + offset: [0, 0, -0.3] + - geometry: ["line"] + color: orange + opacity: 1.0 + width: 10.0 + mode: hover + offset: [0, 0, -0.1] + - geometry: ["point"] + color: orange opacity: 1.0 width: 20.0 mode: hover + offset: [0, 0, 0.1] - geometry: ["mesh", "polygon"] color: red opacity: 1.0 - width: 4.0 mode: selection - - geometry: ["point", "line"] - color: yellow + offset: [0, 0, -0.3] + - geometry: ["line"] + color: red + opacity: 1.0 + width: 10.0 + mode: selection + offset: [0, 0, -0.1] + - geometry: ["point"] + color: red opacity: 1.0 width: 20.0 mode: selection + offset: [0, 0, 0.1] From b5e381b55fb7569552d9ae9df156bc556aa076ab Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 5 Sep 2024 19:36:08 +0200 Subject: [PATCH 36/37] Add merged-point-visualization docs. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 6fae4dd3..91294be2 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Each rule within the YAML `rules` array can have the following fields: | `flat` | Clamps the feature to the ground (Does not work for meshes). | Boolean | `true`, `false` | | `outline-color` | Point outline color. | String | `green`, `#fff` | | `outline-width` | Point outline width in px. | Float | `3.6` | +| `point-merge-grid-cell` | WGS84/altutide meter tolerance for merging point visualizations. | Array of three Floats. | `[0.000000084, 0.000000084, 0.01]` | | `near-far-scale` | For points, indicate (`near-alt-meters`, `near-scale`, `far-alt-meters`, `far-scale`). | Array of four Floats. | `[1.5e2,10,8.0e6,0]` | | `offset` | Apply a fixed offset to each shape-point in meters. Can be used for z-ordering. | Array of three Floats. | `[0, 0, 5]` | | `arrow` | For arrow-heads: One of `none`, `forward`, `backward`, `double`. Not compatible with `dashed`. | String | `single` | @@ -230,6 +231,17 @@ For attributes, style expressions (e.g. `color-expression`) are evaluated in a c set the `offset` field. The spatial `offset` will be multiplied, so it is possible to "stack" attributes over a feature. +### About Merged Point Visualizations + +By setting `point-merge-grid-cell`, a tolerance may be defined which allows merging the visual representations +of point features which share the same 3D spatial cell, map, layer, and style rule. This has two advantages: + +* **Multi-Selection**: When selecting the merged representation, a multi-selection of all merged features happens. +* **Logical Evaluation using `$mergeCount`**: In some map formats, it may be desirable to apply a style based on the number of merged points. + This may be done to display a warning, or to check a matching requirement. + To this end, the `$mergeCount` variable is injected into each simfil evaluation context of a merged-point style rule. + Check out the default style for an example. + ### About `first-of` Normally, all style rules from a style sheet are naively applied to all matching features. From 0ff953030c4766bda08f02a1efd19f7ca1921879 Mon Sep 17 00:00:00 2001 From: Wagram Airiian Date: Tue, 10 Sep 2024 19:45:32 +0200 Subject: [PATCH 37/37] Refactor feature iteration to allow jump to feature --- erdblick_app/app/inspection.service.ts | 19 +++++++++++++++---- erdblick_app/app/view.component.ts | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/erdblick_app/app/inspection.service.ts b/erdblick_app/app/inspection.service.ts index 7529a5f6..e7a09262 100644 --- a/erdblick_app/app/inspection.service.ts +++ b/erdblick_app/app/inspection.service.ts @@ -93,8 +93,10 @@ export class InspectionService { this.selectedFeatureGeoJsonTexts = []; this.selectedFeatures = selectedFeatures; - selectedFeatures.forEach(selectedFeature => { - selectedFeature.peek((feature: Feature) => { + // Currently only takes the first element for Jump to Feature functionality. + // TODO: Allow to use the whole set for Jump to Feature. + if (selectedFeatures.length) { + selectedFeatures[0].peek((feature: Feature) => { this.selectedFeatureInspectionModel.push(...feature.inspectionModel()); this.selectedFeatureGeoJsonTexts.push(feature.geojson() as string); this.isInspectionPanelVisible = true; @@ -106,7 +108,16 @@ export class InspectionService { this.selectedFeatureBoundingRadius = Cartesian3.distance(this.selectedFeatureOrigin, radiusPoint); this.selectedFeatureGeometryType = feature.getGeometryType() as any;this.isInspectionPanelVisible = true; }); - }); + } + if (selectedFeatures.length > 1) { + selectedFeatures.slice(1).forEach(selectedFeature => { + selectedFeature.peek((feature: Feature) => { + this.selectedFeatureInspectionModel.push(...feature.inspectionModel()); + this.selectedFeatureGeoJsonTexts.push(feature.geojson() as string); + this.isInspectionPanelVisible = true; + }); + }); + } this.loadFeatureData(); this.parametersService.setSelectedFeatures(this.selectedFeatures.map(f => f.key())); @@ -205,7 +216,7 @@ export class InspectionService { } zoomToFeature() { - if (!this.selectedFeature) { + if (!this.selectedFeatures) { this.infoMessageService.showError("Could not zoom to feature: no feature is selected!"); return; } diff --git a/erdblick_app/app/view.component.ts b/erdblick_app/app/view.component.ts index df47d412..dfd440b6 100644 --- a/erdblick_app/app/view.component.ts +++ b/erdblick_app/app/view.component.ts @@ -138,8 +138,8 @@ export class ErdblickViewComponent implements AfterViewInit { if (feature.primitive.id) { const featureInfo = this.featureSearchService.searchResults[feature.primitive.id.index]; if (featureInfo.mapId && featureInfo.featureId) { - this.jumpService.highlightFeature(featureInfo.mapId, featureInfo.featureId).then(() => { - if (this.inspectionService.selectedFeature) { + this.jumpService.highlightByJumpTargetFilter(featureInfo.mapId, featureInfo.featureId).then(() => { + if (this.inspectionService.selectedFeatures) { this.inspectionService.zoomToFeature(); } });