fix: make burst impact coverage envelopes exclusive
This commit is contained in:
@@ -139,9 +139,12 @@ def estimate_burst_impact(network: str, start: datetime, simulation: dict[str, A
|
||||
return output
|
||||
groups = [{key: group[key] for key in ("group_id", "title", "node_ids")}
|
||||
for group in impact["grades"]]
|
||||
geometry = generate_node_coverage(network, groups)
|
||||
# GRADES are ordered from low to high. Keep the numerical classes intact
|
||||
# while removing overlap between their independently derived envelopes.
|
||||
geometry = generate_node_coverage(network, groups, exclusive_groups=True)
|
||||
impact["coverage_status"] = geometry["coverage"]["status"]
|
||||
geometry["coverage"]["boundary"] = impact["boundary"]
|
||||
geometry["coverage"]["boundary"] += "分档包络互斥展示,重叠区域优先显示较高降压档,不改变节点分档或表示精确水力等值边界。"
|
||||
for feature in geometry["map_payload"]["features"]:
|
||||
feature["properties"].update(
|
||||
impact_phase=impact["phase"], pressure_drop_unit="m",
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
|
||||
from pyproj import CRS, Transformer
|
||||
from shapely import MultiPoint, concave_hull, get_num_coordinates
|
||||
from shapely.geometry import mapping
|
||||
from shapely.geometry import GeometryCollection, mapping, shape
|
||||
from shapely.ops import transform, unary_union
|
||||
|
||||
from app.native.wndb.gis.network_views import get_node_coverage_links, get_node_geographic_positions
|
||||
@@ -17,6 +17,47 @@ SIMPLIFY_METERS = 2.0
|
||||
MAX_COVERAGE_NODES = 10000
|
||||
|
||||
|
||||
def exclusive_coverage_features(features: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Clip display envelopes, with later groups taking precedence; retain input order.
|
||||
|
||||
This changes only derived geometry, never node membership or model values.
|
||||
No post-clipping buffer or simplification may reintroduce an overlap.
|
||||
"""
|
||||
occupied = GeometryCollection()
|
||||
result = []
|
||||
summaries = {}
|
||||
for feature in features:
|
||||
properties = feature["properties"]
|
||||
summaries.update(properties.get("coverage_group_summary", {}))
|
||||
for feature in features:
|
||||
properties = feature["properties"]
|
||||
summaries[properties["coverage_group"]] = {
|
||||
key: properties.get(key) for key in ("node_count", "mapped_node_count", "missing_node_count")
|
||||
}
|
||||
for feature in reversed(features):
|
||||
original = shape(feature["geometry"])
|
||||
visible = original.difference(occupied)
|
||||
occupied = unary_union([occupied, original])
|
||||
if visible.is_empty:
|
||||
continue
|
||||
if (visible.geom_type not in {"Polygon", "MultiPolygon"} or not visible.is_valid
|
||||
or get_num_coordinates(visible) > 20000):
|
||||
raise ValueError("Invalid exclusive derived coverage")
|
||||
result.append({
|
||||
**feature,
|
||||
"properties": {**feature["properties"], "overlap_policy": "higher_priority_group_first"},
|
||||
"geometry": mapping(visible),
|
||||
})
|
||||
visible_groups = {feature["properties"]["coverage_group"] for feature in result}
|
||||
summaries = {key: {**value, "display_area_available": key in visible_groups}
|
||||
for key, value in summaries.items()}
|
||||
for feature in result:
|
||||
# Retain counts even when that group's entire display envelope was
|
||||
# clipped away. Overlay transport deliberately sends only GeoJSON.
|
||||
feature["properties"]["coverage_group_summary"] = summaries
|
||||
return list(reversed(result))
|
||||
|
||||
|
||||
def _components(node_ids: list[str], links: list[dict[str, Any]], closed: set[str]) -> list[list[str]]:
|
||||
parent = {node_id: node_id for node_id in node_ids}
|
||||
|
||||
@@ -41,6 +82,7 @@ def build_node_coverage(
|
||||
links: list[dict[str, Any]], *, split_by_connectivity: bool = False,
|
||||
excluded_link_ids: list[str] | None = None,
|
||||
concave_ratio: float = CONCAVE_RATIO, buffer_m: float = BUFFER_METERS,
|
||||
exclusive_groups: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Keep caller-defined comparison groups and derive geometry without assigning business meaning."""
|
||||
coordinates = {}
|
||||
@@ -99,6 +141,12 @@ def build_node_coverage(
|
||||
}
|
||||
features.append(feature)
|
||||
|
||||
if exclusive_groups:
|
||||
features = exclusive_coverage_features(features)
|
||||
visible_groups = {feature["properties"]["coverage_group"] for feature in features}
|
||||
for group_id, metadata in groups_metadata.items():
|
||||
metadata["display_area_available"] = group_id in visible_groups
|
||||
|
||||
return {
|
||||
"status": "partial" if any(group["missing_node_ids"] for group in groups_metadata.values())
|
||||
else "ready" if features else "empty",
|
||||
@@ -106,7 +154,8 @@ def build_node_coverage(
|
||||
"parameters": {"concave_ratio": concave_ratio, "buffer_m": buffer_m,
|
||||
"split_by_connectivity": split_by_connectivity,
|
||||
"excluded_link_ids": sorted(closed),
|
||||
"simplify_m": simplify_m, "projection": "local_azimuthal_equidistant"},
|
||||
"simplify_m": simplify_m, "projection": "local_azimuthal_equidistant",
|
||||
"exclusive_groups": exclusive_groups},
|
||||
"output_crs": "EPSG:4326", "display_only": True,
|
||||
"position_source": "gis.node_geometries",
|
||||
"boundary": "节点空间覆盖示意;不推断故障、污染、停水、水力影响或用户服务边界。",
|
||||
|
||||
Reference in New Issue
Block a user