diff --git a/README.md b/README.md index 0878ec6f..ecfefb55 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Each rule within the YAML `rules` array can have the following fields: | `relation-recursive` | Specifies whether relations should be resolved recursively. Only done if `mode=="Highlight"`, and only works for relations within the same layer. | Boolean | `true`, `false` | | `relation-merge-twoway` | Specifies whether bidirectional relations should be followed and merged. | Boolean | `true`, `false` | | `attribute-type` | A regular expression to match against an attribute type. | String | `SPEED_LIMIT_.*` | +| `attribute-mask` | A regular expression to match against a value of the attribute's field. | String | `attributeValue.speedLimitKmh > 100` | | `attribute-layer-type` | A regular expression to match against the attribute layer type name. | String | `Road.*Layer` | | `attribute-validity-geom` | Set to `required`, `none` or `any` to control whether matching attributes must have a validity geometry. | String | `Road.*Layer` | | `label-color` | Text color of the label. | String | `#00ccdd` | diff --git a/erdblick_app/app/editor.component.ts b/erdblick_app/app/editor.component.ts index f4b40d06..45768e5f 100644 --- a/erdblick_app/app/editor.component.ts +++ b/erdblick_app/app/editor.component.ts @@ -60,6 +60,7 @@ const completionsList = [ {label: "offset-scale-by-distance", type: "property"}, {label: "first-of", type: "property"}, {label: "attribute-type", type: "property"}, + {label: "attribute-mask", type: "property"}, {label: "attribute-layer-type", type: "property"}, {label: "point-merge-grid-cell", type: "property"}, {label: "FILL", type: "keyword"}, diff --git a/erdblick_app/app/sourcedataselection.dialog.component.ts b/erdblick_app/app/sourcedataselection.dialog.component.ts index 9368e6cb..b2dfbe3f 100644 --- a/erdblick_app/app/sourcedataselection.dialog.component.ts +++ b/erdblick_app/app/sourcedataselection.dialog.component.ts @@ -124,8 +124,19 @@ export class SourceDataLayerSelectionDialogComponent { this.mapIdsPerTileId.set(id, maps); } + if (this.menuService.lastInspectedTileSourceDataOption.getValue()) { + const savedTileId = this.menuService.lastInspectedTileSourceDataOption.getValue()?.tileId; + let tileIdSelection = this.tileIds.find(element => + Number(element.id) == savedTileId && !element.disabled && [...this.mapService.tileLayersForTileId(element.id as bigint)].length + ); + if (tileIdSelection) { + this.setCurrentTileId(tileIdSelection); + } + return; + } + // Pre-select the tile ID. - let tileIdSelection = this.tileIds.find(element => + const tileIdSelection = this.tileIds.find(element => !element.disabled && [...this.mapService.tileLayersForTileId(element.id as bigint)].length ); if (tileIdSelection) { @@ -165,13 +176,18 @@ export class SourceDataLayerSelectionDialogComponent { this.onTileIdChange(tileId); if (this.customMapId) { - let mapSelection = this.mapIds.find(entry => entry.id == this.customMapId); + const mapSelection = this.mapIds.find(entry => entry.id == this.customMapId); if (mapSelection) { this.selectedMapId = mapSelection; - } - else { + } else { this.mapIds.unshift({ id: this.customMapId, name: this.customMapId }); } + } else if (this.menuService.lastInspectedTileSourceDataOption.getValue()) { + const savedMapId = this.menuService.lastInspectedTileSourceDataOption.getValue()?.mapId; + const mapSelection = this.mapIds.find(entry => entry.id == savedMapId); + if (mapSelection) { + this.selectedMapId = mapSelection; + } } if (this.mapIds.length) { @@ -180,7 +196,17 @@ export class SourceDataLayerSelectionDialogComponent { } this.onMapIdChange(this.selectedMapId); if (this.sourceDataLayers.length) { - this.selectedSourceDataLayer = this.sourceDataLayers[0]; + if (this.menuService.lastInspectedTileSourceDataOption.getValue()) { + const savedLayerId = this.menuService.lastInspectedTileSourceDataOption.getValue()?.layerId; + const layerSelection = this.sourceDataLayers.find(entry => entry.id == savedLayerId); + if (layerSelection) { + this.selectedSourceDataLayer = layerSelection; + } else { + this.selectedSourceDataLayer = this.sourceDataLayers[0]; + } + } else { + this.selectedSourceDataLayer = this.sourceDataLayers[0]; + } this.onLayerIdChange(this.selectedSourceDataLayer); } } diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index 5b924c92..187e33f6 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -78,6 +78,7 @@ class FeatureStyleRule [[nodiscard]] bool relationMergeTwoWay() const; [[nodiscard]] std::optional const& attributeType() const; + [[nodiscard]] std::optional const& attributeMask() const; [[nodiscard]] std::optional const& attributeLayerType() const; [[nodiscard]] std::optional const& attributeValidityGeometry() const; @@ -166,6 +167,7 @@ class FeatureStyleRule bool relationMergeTwoWay_ = false; std::optional attributeType_; + std::optional attributeMask_; std::optional attributeLayerType_; std::optional attributeValidityGeometry_; diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index e64c3b73..c2279270 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -259,6 +259,10 @@ void FeatureStyleRule::parse(const YAML::Node& yaml) // Parse an attribute type regular expression, e.g. `SPEED_LIMIT_.*` attributeType_ = yaml["attribute-type"].as(); } + if (yaml["attribute-mask"].IsDefined()) { + // Parse an attribute based on it's field value, e.g. `speedLimitKmh > 100` + attributeMask_ = yaml["attribute-mask"].as(); + } if (yaml["attribute-layer-type"].IsDefined()) { // Parse an attribute type regular expression, e.g. `Road.*Layer` attributeLayerType_ = yaml["attribute-layer-type"].as(); @@ -691,6 +695,11 @@ std::optional const& FeatureStyleRule::attributeType() const return attributeType_; } +std::optional const& FeatureStyleRule::attributeMask() const +{ + return attributeMask_; +} + std::optional const& FeatureStyleRule::attributeLayerType() const { return attributeLayerType_; diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp index cf1eb26f..dc93305a 100644 --- a/libs/core/src/visualization.cpp +++ b/libs/core/src/visualization.cpp @@ -281,6 +281,52 @@ void FeatureLayerVisualization::addFeature( } // Iterate over all the layer's attributes. layer->forEachAttribute([&, this](auto&& attr){ + // Check if the attribute's values match the attribute mask for the rule. + if (auto const& attrMask = rule.attributeMask()) { + if (!attrMask->empty()) { + auto const& constAttr = static_cast(*attr); + simfil::OverlayNode attrEvaluationContext(simfil::Value::field(constAttr)); + + try { + constAttr.forEachField([&](auto const& fieldName, auto const& fieldValue) { + if (fieldValue) { + attrEvaluationContext.set( + internalStringPoolCopy_->emplace(fieldName), + simfil::Value::field(fieldValue)); + } + return true; + }); + + addOptionsToSimfilContext(attrEvaluationContext); + + auto boundEvalFun = BoundEvalFun{ + attrEvaluationContext, + [this, &attrEvaluationContext](auto&& str) { + try { + return evaluateExpression(str, attrEvaluationContext); + } catch (const std::exception& e) { + std::cerr << "Error evaluating attribute expression: " << e.what() << std::endl; + return simfil::Value::null(); + } + }}; + + try { + auto result = boundEvalFun.eval_(*attrMask); + if ((result.isa(simfil::ValueType::Bool) && !result.as()) || + result.isa(simfil::ValueType::Undef) || result.isa(simfil::ValueType::Null)) { + return true; + } + } catch (std::exception const& e) { + std::cerr << "Error evaluating attribute mask: " << e.what() << std::endl; + return true; + } + } catch (const std::exception& e) { + std::cerr << "Error setting up attribute evaluation context: " << e.what() << std::endl; + return true; + } + } + } + addAttribute( feature, layerName,