exonware-xwnode 0.0.1.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/__init__.py +14 -0
- exonware/xwnode/__init__.py +127 -0
- exonware/xwnode/base.py +676 -0
- exonware/xwnode/config.py +178 -0
- exonware/xwnode/contracts.py +730 -0
- exonware/xwnode/errors.py +503 -0
- exonware/xwnode/facade.py +460 -0
- exonware/xwnode/strategies/__init__.py +158 -0
- exonware/xwnode/strategies/advisor.py +463 -0
- exonware/xwnode/strategies/edges/__init__.py +32 -0
- exonware/xwnode/strategies/edges/adj_list.py +227 -0
- exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
- exonware/xwnode/strategies/edges/base.py +169 -0
- exonware/xwnode/strategies/flyweight.py +328 -0
- exonware/xwnode/strategies/impls/__init__.py +13 -0
- exonware/xwnode/strategies/impls/_base_edge.py +403 -0
- exonware/xwnode/strategies/impls/_base_node.py +307 -0
- exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
- exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
- exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
- exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
- exonware/xwnode/strategies/impls/edge_coo.py +533 -0
- exonware/xwnode/strategies/impls/edge_csc.py +447 -0
- exonware/xwnode/strategies/impls/edge_csr.py +492 -0
- exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
- exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
- exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
- exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
- exonware/xwnode/strategies/impls/edge_octree.py +574 -0
- exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
- exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
- exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
- exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
- exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
- exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
- exonware/xwnode/strategies/manager.py +775 -0
- exonware/xwnode/strategies/metrics.py +538 -0
- exonware/xwnode/strategies/migration.py +432 -0
- exonware/xwnode/strategies/nodes/__init__.py +50 -0
- exonware/xwnode/strategies/nodes/_base_node.py +307 -0
- exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
- exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
- exonware/xwnode/strategies/nodes/array_list.py +209 -0
- exonware/xwnode/strategies/nodes/base.py +247 -0
- exonware/xwnode/strategies/nodes/deque.py +200 -0
- exonware/xwnode/strategies/nodes/hash_map.py +135 -0
- exonware/xwnode/strategies/nodes/heap.py +307 -0
- exonware/xwnode/strategies/nodes/linked_list.py +232 -0
- exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
- exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
- exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
- exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
- exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
- exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
- exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
- exonware/xwnode/strategies/nodes/node_btree.py +357 -0
- exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
- exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
- exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
- exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
- exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
- exonware/xwnode/strategies/nodes/node_heap.py +191 -0
- exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
- exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
- exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
- exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
- exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
- exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
- exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
- exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
- exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
- exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
- exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
- exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
- exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
- exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
- exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
- exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
- exonware/xwnode/strategies/nodes/node_treap.py +387 -0
- exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
- exonware/xwnode/strategies/nodes/node_trie.py +252 -0
- exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
- exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
- exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
- exonware/xwnode/strategies/nodes/queue.py +161 -0
- exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
- exonware/xwnode/strategies/nodes/stack.py +152 -0
- exonware/xwnode/strategies/nodes/trie.py +274 -0
- exonware/xwnode/strategies/nodes/union_find.py +283 -0
- exonware/xwnode/strategies/pattern_detector.py +603 -0
- exonware/xwnode/strategies/performance_monitor.py +487 -0
- exonware/xwnode/strategies/queries/__init__.py +24 -0
- exonware/xwnode/strategies/queries/base.py +236 -0
- exonware/xwnode/strategies/queries/cql.py +201 -0
- exonware/xwnode/strategies/queries/cypher.py +181 -0
- exonware/xwnode/strategies/queries/datalog.py +70 -0
- exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
- exonware/xwnode/strategies/queries/eql.py +70 -0
- exonware/xwnode/strategies/queries/flux.py +70 -0
- exonware/xwnode/strategies/queries/gql.py +70 -0
- exonware/xwnode/strategies/queries/graphql.py +240 -0
- exonware/xwnode/strategies/queries/gremlin.py +181 -0
- exonware/xwnode/strategies/queries/hiveql.py +214 -0
- exonware/xwnode/strategies/queries/hql.py +70 -0
- exonware/xwnode/strategies/queries/jmespath.py +219 -0
- exonware/xwnode/strategies/queries/jq.py +66 -0
- exonware/xwnode/strategies/queries/json_query.py +66 -0
- exonware/xwnode/strategies/queries/jsoniq.py +248 -0
- exonware/xwnode/strategies/queries/kql.py +70 -0
- exonware/xwnode/strategies/queries/linq.py +238 -0
- exonware/xwnode/strategies/queries/logql.py +70 -0
- exonware/xwnode/strategies/queries/mql.py +68 -0
- exonware/xwnode/strategies/queries/n1ql.py +210 -0
- exonware/xwnode/strategies/queries/partiql.py +70 -0
- exonware/xwnode/strategies/queries/pig.py +215 -0
- exonware/xwnode/strategies/queries/promql.py +70 -0
- exonware/xwnode/strategies/queries/sparql.py +220 -0
- exonware/xwnode/strategies/queries/sql.py +275 -0
- exonware/xwnode/strategies/queries/xml_query.py +66 -0
- exonware/xwnode/strategies/queries/xpath.py +223 -0
- exonware/xwnode/strategies/queries/xquery.py +258 -0
- exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
- exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
- exonware/xwnode/strategies/registry.py +604 -0
- exonware/xwnode/strategies/simple.py +273 -0
- exonware/xwnode/strategies/utils.py +532 -0
- exonware/xwnode/types.py +912 -0
- exonware/xwnode/version.py +78 -0
- exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
- exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
- exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
- exonware_xwnode-0.0.1.12.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,820 @@
|
|
1
|
+
"""
|
2
|
+
R-Tree Edge Strategy Implementation
|
3
|
+
|
4
|
+
This module implements the R_TREE strategy for spatial indexing of edges
|
5
|
+
with geometric coordinates and efficient spatial queries.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, NamedTuple
|
9
|
+
from collections import defaultdict
|
10
|
+
import math
|
11
|
+
from ._base_edge import aEdgeStrategy
|
12
|
+
from ...types import EdgeMode, EdgeTrait
|
13
|
+
|
14
|
+
|
15
|
+
class Rectangle(NamedTuple):
|
16
|
+
"""Represents a bounding rectangle."""
|
17
|
+
min_x: float
|
18
|
+
min_y: float
|
19
|
+
max_x: float
|
20
|
+
max_y: float
|
21
|
+
|
22
|
+
def area(self) -> float:
|
23
|
+
"""Calculate rectangle area."""
|
24
|
+
return max(0, self.max_x - self.min_x) * max(0, self.max_y - self.min_y)
|
25
|
+
|
26
|
+
def perimeter(self) -> float:
|
27
|
+
"""Calculate rectangle perimeter."""
|
28
|
+
return 2 * (max(0, self.max_x - self.min_x) + max(0, self.max_y - self.min_y))
|
29
|
+
|
30
|
+
def contains_point(self, x: float, y: float) -> bool:
|
31
|
+
"""Check if rectangle contains point."""
|
32
|
+
return self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y
|
33
|
+
|
34
|
+
def intersects(self, other: 'Rectangle') -> bool:
|
35
|
+
"""Check if rectangle intersects with another."""
|
36
|
+
return not (other.max_x < self.min_x or other.min_x > self.max_x or
|
37
|
+
other.max_y < self.min_y or other.min_y > self.max_y)
|
38
|
+
|
39
|
+
def contains_rectangle(self, other: 'Rectangle') -> bool:
|
40
|
+
"""Check if this rectangle contains another."""
|
41
|
+
return (self.min_x <= other.min_x and self.max_x >= other.max_x and
|
42
|
+
self.min_y <= other.min_y and self.max_y >= other.max_y)
|
43
|
+
|
44
|
+
def union(self, other: 'Rectangle') -> 'Rectangle':
|
45
|
+
"""Get bounding rectangle of union."""
|
46
|
+
return Rectangle(
|
47
|
+
min(self.min_x, other.min_x),
|
48
|
+
min(self.min_y, other.min_y),
|
49
|
+
max(self.max_x, other.max_x),
|
50
|
+
max(self.max_y, other.max_y)
|
51
|
+
)
|
52
|
+
|
53
|
+
def enlargement_needed(self, other: 'Rectangle') -> float:
|
54
|
+
"""Calculate area enlargement needed to include another rectangle."""
|
55
|
+
union_rect = self.union(other)
|
56
|
+
return union_rect.area() - self.area()
|
57
|
+
|
58
|
+
|
59
|
+
class SpatialEdge:
|
60
|
+
"""Represents an edge with spatial coordinates."""
|
61
|
+
|
62
|
+
def __init__(self, edge_id: str, source: str, target: str,
|
63
|
+
source_coords: Tuple[float, float],
|
64
|
+
target_coords: Tuple[float, float], **properties):
|
65
|
+
self.edge_id = edge_id
|
66
|
+
self.source = source
|
67
|
+
self.target = target
|
68
|
+
self.source_coords = source_coords
|
69
|
+
self.target_coords = target_coords
|
70
|
+
self.properties = properties.copy()
|
71
|
+
|
72
|
+
# Calculate bounding rectangle
|
73
|
+
self.bounding_rect = Rectangle(
|
74
|
+
min(source_coords[0], target_coords[0]),
|
75
|
+
min(source_coords[1], target_coords[1]),
|
76
|
+
max(source_coords[0], target_coords[0]),
|
77
|
+
max(source_coords[1], target_coords[1])
|
78
|
+
)
|
79
|
+
|
80
|
+
# Calculate edge length
|
81
|
+
dx = target_coords[0] - source_coords[0]
|
82
|
+
dy = target_coords[1] - source_coords[1]
|
83
|
+
self.length = math.sqrt(dx * dx + dy * dy)
|
84
|
+
|
85
|
+
def intersects_rectangle(self, rect: Rectangle) -> bool:
|
86
|
+
"""Check if edge intersects with rectangle."""
|
87
|
+
# First check if bounding rectangles intersect
|
88
|
+
if not self.bounding_rect.intersects(rect):
|
89
|
+
return False
|
90
|
+
|
91
|
+
# Check if either endpoint is inside rectangle
|
92
|
+
if (rect.contains_point(*self.source_coords) or
|
93
|
+
rect.contains_point(*self.target_coords)):
|
94
|
+
return True
|
95
|
+
|
96
|
+
# Check line-rectangle intersection (simplified)
|
97
|
+
return self._line_intersects_rectangle(rect)
|
98
|
+
|
99
|
+
def _line_intersects_rectangle(self, rect: Rectangle) -> bool:
|
100
|
+
"""Check if line segment intersects rectangle edges."""
|
101
|
+
x1, y1 = self.source_coords
|
102
|
+
x2, y2 = self.target_coords
|
103
|
+
|
104
|
+
# Check intersection with each rectangle edge
|
105
|
+
edges = [
|
106
|
+
((rect.min_x, rect.min_y), (rect.max_x, rect.min_y)), # Bottom
|
107
|
+
((rect.max_x, rect.min_y), (rect.max_x, rect.max_y)), # Right
|
108
|
+
((rect.max_x, rect.max_y), (rect.min_x, rect.max_y)), # Top
|
109
|
+
((rect.min_x, rect.max_y), (rect.min_x, rect.min_y)) # Left
|
110
|
+
]
|
111
|
+
|
112
|
+
for (ex1, ey1), (ex2, ey2) in edges:
|
113
|
+
if self._segments_intersect((x1, y1), (x2, y2), (ex1, ey1), (ex2, ey2)):
|
114
|
+
return True
|
115
|
+
|
116
|
+
return False
|
117
|
+
|
118
|
+
def _segments_intersect(self, p1: Tuple[float, float], p2: Tuple[float, float],
|
119
|
+
p3: Tuple[float, float], p4: Tuple[float, float]) -> bool:
|
120
|
+
"""Check if two line segments intersect."""
|
121
|
+
def ccw(A, B, C):
|
122
|
+
return (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0])
|
123
|
+
|
124
|
+
return ccw(p1, p3, p4) != ccw(p2, p3, p4) and ccw(p1, p2, p3) != ccw(p1, p2, p4)
|
125
|
+
|
126
|
+
def distance_to_point(self, x: float, y: float) -> float:
|
127
|
+
"""Calculate distance from point to edge."""
|
128
|
+
x1, y1 = self.source_coords
|
129
|
+
x2, y2 = self.target_coords
|
130
|
+
|
131
|
+
# Vector from source to target
|
132
|
+
dx = x2 - x1
|
133
|
+
dy = y2 - y1
|
134
|
+
|
135
|
+
if dx == 0 and dy == 0:
|
136
|
+
# Degenerate case: source and target are same point
|
137
|
+
return math.sqrt((x - x1) ** 2 + (y - y1) ** 2)
|
138
|
+
|
139
|
+
# Parameter t for projection onto line
|
140
|
+
t = max(0, min(1, ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy)))
|
141
|
+
|
142
|
+
# Closest point on line segment
|
143
|
+
closest_x = x1 + t * dx
|
144
|
+
closest_y = y1 + t * dy
|
145
|
+
|
146
|
+
return math.sqrt((x - closest_x) ** 2 + (y - closest_y) ** 2)
|
147
|
+
|
148
|
+
def to_dict(self) -> Dict[str, Any]:
|
149
|
+
"""Convert to dictionary representation."""
|
150
|
+
return {
|
151
|
+
'id': self.edge_id,
|
152
|
+
'source': self.source,
|
153
|
+
'target': self.target,
|
154
|
+
'source_coords': self.source_coords,
|
155
|
+
'target_coords': self.target_coords,
|
156
|
+
'length': self.length,
|
157
|
+
'bounding_rect': {
|
158
|
+
'min_x': self.bounding_rect.min_x,
|
159
|
+
'min_y': self.bounding_rect.min_y,
|
160
|
+
'max_x': self.bounding_rect.max_x,
|
161
|
+
'max_y': self.bounding_rect.max_y
|
162
|
+
},
|
163
|
+
'properties': self.properties
|
164
|
+
}
|
165
|
+
|
166
|
+
|
167
|
+
class RTreeNode:
|
168
|
+
"""Node in the R-Tree structure."""
|
169
|
+
|
170
|
+
def __init__(self, is_leaf: bool = False, max_entries: int = 4):
|
171
|
+
self.is_leaf = is_leaf
|
172
|
+
self.max_entries = max_entries
|
173
|
+
self.entries: List[Tuple[Rectangle, Any]] = [] # (bounding_rect, child_or_edge)
|
174
|
+
self.bounding_rect: Optional[Rectangle] = None
|
175
|
+
|
176
|
+
def is_full(self) -> bool:
|
177
|
+
"""Check if node is full."""
|
178
|
+
return len(self.entries) >= self.max_entries
|
179
|
+
|
180
|
+
def update_bounding_rect(self) -> None:
|
181
|
+
"""Update bounding rectangle to encompass all entries."""
|
182
|
+
if not self.entries:
|
183
|
+
self.bounding_rect = None
|
184
|
+
return
|
185
|
+
|
186
|
+
min_x = min(rect.min_x for rect, _ in self.entries)
|
187
|
+
min_y = min(rect.min_y for rect, _ in self.entries)
|
188
|
+
max_x = max(rect.max_x for rect, _ in self.entries)
|
189
|
+
max_y = max(rect.max_y for rect, _ in self.entries)
|
190
|
+
|
191
|
+
self.bounding_rect = Rectangle(min_x, min_y, max_x, max_y)
|
192
|
+
|
193
|
+
def add_entry(self, rect: Rectangle, data: Any) -> None:
|
194
|
+
"""Add entry to node."""
|
195
|
+
self.entries.append((rect, data))
|
196
|
+
self.update_bounding_rect()
|
197
|
+
|
198
|
+
|
199
|
+
class xRTreeStrategy(aEdgeStrategy):
|
200
|
+
"""
|
201
|
+
R-Tree edge strategy for spatial indexing of edges.
|
202
|
+
|
203
|
+
Efficiently manages edges with spatial coordinates, supporting
|
204
|
+
fast spatial queries like range searches, nearest neighbor,
|
205
|
+
and geometric intersections.
|
206
|
+
"""
|
207
|
+
|
208
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
209
|
+
"""Initialize the R-Tree strategy."""
|
210
|
+
super().__init__(EdgeMode.R_TREE, traits, **options)
|
211
|
+
|
212
|
+
self.max_entries = options.get('max_entries', 4)
|
213
|
+
self.min_entries = max(1, self.max_entries // 2)
|
214
|
+
self.is_directed = options.get('directed', True)
|
215
|
+
|
216
|
+
# Core storage
|
217
|
+
self._edges: Dict[str, SpatialEdge] = {}
|
218
|
+
self._vertex_coords: Dict[str, Tuple[float, float]] = {}
|
219
|
+
self._vertices: Set[str] = set()
|
220
|
+
|
221
|
+
# R-Tree structure
|
222
|
+
self._root: Optional[RTreeNode] = None
|
223
|
+
self._edge_count = 0
|
224
|
+
self._edge_id_counter = 0
|
225
|
+
|
226
|
+
# Statistics
|
227
|
+
self._tree_height = 0
|
228
|
+
self._total_nodes = 0
|
229
|
+
|
230
|
+
def get_supported_traits(self) -> EdgeTrait:
|
231
|
+
"""Get the traits supported by the R-tree strategy."""
|
232
|
+
return (EdgeTrait.SPATIAL | EdgeTrait.DIRECTED | EdgeTrait.WEIGHTED | EdgeTrait.SPARSE)
|
233
|
+
|
234
|
+
def _generate_edge_id(self) -> str:
|
235
|
+
"""Generate unique edge ID."""
|
236
|
+
self._edge_id_counter += 1
|
237
|
+
return f"spatial_edge_{self._edge_id_counter}"
|
238
|
+
|
239
|
+
def _choose_leaf(self, rect: Rectangle) -> RTreeNode:
|
240
|
+
"""Choose leaf node for insertion using minimum enlargement heuristic."""
|
241
|
+
if self._root is None:
|
242
|
+
self._root = RTreeNode(is_leaf=True, max_entries=self.max_entries)
|
243
|
+
return self._root
|
244
|
+
|
245
|
+
current = self._root
|
246
|
+
|
247
|
+
while not current.is_leaf:
|
248
|
+
best_child = None
|
249
|
+
best_enlargement = float('inf')
|
250
|
+
best_area = float('inf')
|
251
|
+
|
252
|
+
for child_rect, child_node in current.entries:
|
253
|
+
enlargement = child_rect.enlargement_needed(rect)
|
254
|
+
area = child_rect.area()
|
255
|
+
|
256
|
+
if (enlargement < best_enlargement or
|
257
|
+
(enlargement == best_enlargement and area < best_area)):
|
258
|
+
best_enlargement = enlargement
|
259
|
+
best_area = area
|
260
|
+
best_child = child_node
|
261
|
+
|
262
|
+
current = best_child
|
263
|
+
|
264
|
+
return current
|
265
|
+
|
266
|
+
def _split_node(self, node: RTreeNode) -> Tuple[RTreeNode, RTreeNode]:
|
267
|
+
"""Split overfull node using quadratic split algorithm."""
|
268
|
+
# Find two entries with maximum waste of space
|
269
|
+
max_waste = -1
|
270
|
+
seed1_idx = seed2_idx = 0
|
271
|
+
|
272
|
+
for i in range(len(node.entries)):
|
273
|
+
for j in range(i + 1, len(node.entries)):
|
274
|
+
rect1, _ = node.entries[i]
|
275
|
+
rect2, _ = node.entries[j]
|
276
|
+
|
277
|
+
union_area = rect1.union(rect2).area()
|
278
|
+
waste = union_area - rect1.area() - rect2.area()
|
279
|
+
|
280
|
+
if waste > max_waste:
|
281
|
+
max_waste = waste
|
282
|
+
seed1_idx = i
|
283
|
+
seed2_idx = j
|
284
|
+
|
285
|
+
# Create two new nodes
|
286
|
+
node1 = RTreeNode(is_leaf=node.is_leaf, max_entries=self.max_entries)
|
287
|
+
node2 = RTreeNode(is_leaf=node.is_leaf, max_entries=self.max_entries)
|
288
|
+
|
289
|
+
# Add seeds
|
290
|
+
node1.add_entry(*node.entries[seed1_idx])
|
291
|
+
node2.add_entry(*node.entries[seed2_idx])
|
292
|
+
|
293
|
+
# Distribute remaining entries
|
294
|
+
remaining_entries = [node.entries[i] for i in range(len(node.entries))
|
295
|
+
if i != seed1_idx and i != seed2_idx]
|
296
|
+
|
297
|
+
for rect, data in remaining_entries:
|
298
|
+
enlargement1 = node1.bounding_rect.enlargement_needed(rect)
|
299
|
+
enlargement2 = node2.bounding_rect.enlargement_needed(rect)
|
300
|
+
|
301
|
+
if enlargement1 < enlargement2:
|
302
|
+
node1.add_entry(rect, data)
|
303
|
+
elif enlargement2 < enlargement1:
|
304
|
+
node2.add_entry(rect, data)
|
305
|
+
else:
|
306
|
+
# Equal enlargement, choose smaller area
|
307
|
+
if node1.bounding_rect.area() <= node2.bounding_rect.area():
|
308
|
+
node1.add_entry(rect, data)
|
309
|
+
else:
|
310
|
+
node2.add_entry(rect, data)
|
311
|
+
|
312
|
+
return node1, node2
|
313
|
+
|
314
|
+
def _insert_with_split(self, edge: SpatialEdge) -> None:
|
315
|
+
"""Insert edge with node splitting if necessary."""
|
316
|
+
leaf = self._choose_leaf(edge.bounding_rect)
|
317
|
+
leaf.add_entry(edge.bounding_rect, edge)
|
318
|
+
|
319
|
+
# Handle overflow
|
320
|
+
if leaf.is_full() and len(leaf.entries) > self.max_entries:
|
321
|
+
node1, node2 = self._split_node(leaf)
|
322
|
+
|
323
|
+
if leaf == self._root:
|
324
|
+
# Create new root
|
325
|
+
new_root = RTreeNode(is_leaf=False, max_entries=self.max_entries)
|
326
|
+
new_root.add_entry(node1.bounding_rect, node1)
|
327
|
+
new_root.add_entry(node2.bounding_rect, node2)
|
328
|
+
self._root = new_root
|
329
|
+
self._tree_height += 1
|
330
|
+
else:
|
331
|
+
# Replace leaf with split nodes (simplified - full implementation would propagate splits)
|
332
|
+
pass
|
333
|
+
|
334
|
+
# ============================================================================
|
335
|
+
# CORE EDGE OPERATIONS
|
336
|
+
# ============================================================================
|
337
|
+
|
338
|
+
def add_edge(self, source: str, target: str, **properties) -> str:
|
339
|
+
"""Add spatial edge with coordinates."""
|
340
|
+
# Extract coordinates from properties
|
341
|
+
source_coords = properties.pop('source_coords', None)
|
342
|
+
target_coords = properties.pop('target_coords', None)
|
343
|
+
|
344
|
+
if source_coords is None and source in self._vertex_coords:
|
345
|
+
source_coords = self._vertex_coords[source]
|
346
|
+
if target_coords is None and target in self._vertex_coords:
|
347
|
+
target_coords = self._vertex_coords[target]
|
348
|
+
|
349
|
+
if source_coords is None or target_coords is None:
|
350
|
+
raise ValueError("Both source_coords and target_coords must be provided")
|
351
|
+
|
352
|
+
# Generate edge ID
|
353
|
+
edge_id = properties.pop('edge_id', self._generate_edge_id())
|
354
|
+
|
355
|
+
# Create spatial edge
|
356
|
+
spatial_edge = SpatialEdge(edge_id, source, target, source_coords, target_coords, **properties)
|
357
|
+
|
358
|
+
# Store edge and update vertices
|
359
|
+
self._edges[edge_id] = spatial_edge
|
360
|
+
self._vertex_coords[source] = source_coords
|
361
|
+
self._vertex_coords[target] = target_coords
|
362
|
+
self._vertices.add(source)
|
363
|
+
self._vertices.add(target)
|
364
|
+
|
365
|
+
# Insert into R-Tree
|
366
|
+
self._insert_with_split(spatial_edge)
|
367
|
+
self._edge_count += 1
|
368
|
+
|
369
|
+
return edge_id
|
370
|
+
|
371
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
372
|
+
"""Remove spatial edge with proper R-Tree cleanup."""
|
373
|
+
edge_to_remove = None
|
374
|
+
edge_id_to_remove = None
|
375
|
+
|
376
|
+
# Find the edge to remove
|
377
|
+
if edge_id and edge_id in self._edges:
|
378
|
+
edge = self._edges[edge_id]
|
379
|
+
if edge.source == source and edge.target == target:
|
380
|
+
edge_to_remove = edge
|
381
|
+
edge_id_to_remove = edge_id
|
382
|
+
else:
|
383
|
+
# Find edge by endpoints
|
384
|
+
for eid, edge in self._edges.items():
|
385
|
+
if edge.source == source and edge.target == target:
|
386
|
+
edge_to_remove = edge
|
387
|
+
edge_id_to_remove = eid
|
388
|
+
break
|
389
|
+
|
390
|
+
if edge_to_remove is None:
|
391
|
+
return False
|
392
|
+
|
393
|
+
# Remove from edge storage
|
394
|
+
del self._edges[edge_id_to_remove]
|
395
|
+
self._edge_count -= 1
|
396
|
+
|
397
|
+
# Remove from R-Tree structure
|
398
|
+
self._remove_from_rtree(edge_to_remove)
|
399
|
+
|
400
|
+
return True
|
401
|
+
|
402
|
+
def _remove_from_rtree(self, edge: SpatialEdge) -> bool:
|
403
|
+
"""Remove edge from R-Tree structure with proper cleanup."""
|
404
|
+
if self._root is None:
|
405
|
+
return False
|
406
|
+
|
407
|
+
# Find the leaf node containing this edge
|
408
|
+
leaf_node = self._find_leaf_containing_edge(self._root, edge)
|
409
|
+
if leaf_node is None:
|
410
|
+
return False
|
411
|
+
|
412
|
+
# Remove edge from leaf node
|
413
|
+
edge_removed = False
|
414
|
+
for i, (rect, data) in enumerate(leaf_node.entries):
|
415
|
+
if isinstance(data, SpatialEdge) and data.edge_id == edge.edge_id:
|
416
|
+
leaf_node.entries.pop(i)
|
417
|
+
edge_removed = True
|
418
|
+
break
|
419
|
+
|
420
|
+
if not edge_removed:
|
421
|
+
return False
|
422
|
+
|
423
|
+
# Update leaf node's bounding rectangle
|
424
|
+
leaf_node.update_bounding_rect()
|
425
|
+
|
426
|
+
# Handle underflow in leaf node
|
427
|
+
if len(leaf_node.entries) < self.min_entries and leaf_node != self._root:
|
428
|
+
self._handle_underflow(leaf_node)
|
429
|
+
else:
|
430
|
+
# Propagate bounding rectangle changes up the tree
|
431
|
+
self._propagate_changes_up(leaf_node)
|
432
|
+
|
433
|
+
# Rebuild tree if root becomes empty
|
434
|
+
if self._root and len(self._root.entries) == 0:
|
435
|
+
self._rebuild_tree()
|
436
|
+
|
437
|
+
return True
|
438
|
+
|
439
|
+
def _find_leaf_containing_edge(self, node: RTreeNode, edge: SpatialEdge) -> Optional[RTreeNode]:
|
440
|
+
"""Find the leaf node containing the specified edge."""
|
441
|
+
if node.is_leaf:
|
442
|
+
# Check if this leaf contains the edge
|
443
|
+
for rect, data in node.entries:
|
444
|
+
if isinstance(data, SpatialEdge) and data.edge_id == edge.edge_id:
|
445
|
+
return node
|
446
|
+
return None
|
447
|
+
|
448
|
+
# Search in child nodes
|
449
|
+
for rect, child_node in node.entries:
|
450
|
+
if rect.intersects(edge.bounding_rect):
|
451
|
+
result = self._find_leaf_containing_edge(child_node, edge)
|
452
|
+
if result is not None:
|
453
|
+
return result
|
454
|
+
|
455
|
+
return None
|
456
|
+
|
457
|
+
def _handle_underflow(self, node: RTreeNode) -> None:
|
458
|
+
"""Handle underflow in a node by redistributing or merging entries."""
|
459
|
+
# Find sibling nodes
|
460
|
+
parent = self._find_parent(self._root, node)
|
461
|
+
if parent is None:
|
462
|
+
return
|
463
|
+
|
464
|
+
siblings = []
|
465
|
+
for rect, child in parent.entries:
|
466
|
+
if child != node:
|
467
|
+
siblings.append(child)
|
468
|
+
|
469
|
+
# Try to redistribute entries from siblings
|
470
|
+
for sibling in siblings:
|
471
|
+
if len(sibling.entries) > self.min_entries:
|
472
|
+
# Redistribute one entry from sibling
|
473
|
+
entry_to_move = sibling.entries.pop()
|
474
|
+
node.entries.append(entry_to_move)
|
475
|
+
|
476
|
+
# Update bounding rectangles
|
477
|
+
node.update_bounding_rect()
|
478
|
+
sibling.update_bounding_rect()
|
479
|
+
self._propagate_changes_up(parent)
|
480
|
+
return
|
481
|
+
|
482
|
+
# If redistribution fails, merge with a sibling
|
483
|
+
if siblings:
|
484
|
+
sibling = siblings[0]
|
485
|
+
# Move all entries from node to sibling
|
486
|
+
sibling.entries.extend(node.entries)
|
487
|
+
sibling.update_bounding_rect()
|
488
|
+
|
489
|
+
# Remove node from parent
|
490
|
+
for i, (rect, child) in enumerate(parent.entries):
|
491
|
+
if child == node:
|
492
|
+
parent.entries.pop(i)
|
493
|
+
break
|
494
|
+
|
495
|
+
parent.update_bounding_rect()
|
496
|
+
self._propagate_changes_up(parent)
|
497
|
+
|
498
|
+
def _find_parent(self, current: RTreeNode, target: RTreeNode) -> Optional[RTreeNode]:
|
499
|
+
"""Find the parent of a target node."""
|
500
|
+
if current.is_leaf:
|
501
|
+
return None
|
502
|
+
|
503
|
+
for rect, child in current.entries:
|
504
|
+
if child == target:
|
505
|
+
return current
|
506
|
+
|
507
|
+
result = self._find_parent(child, target)
|
508
|
+
if result is not None:
|
509
|
+
return result
|
510
|
+
|
511
|
+
return None
|
512
|
+
|
513
|
+
def _propagate_changes_up(self, node: RTreeNode) -> None:
|
514
|
+
"""Propagate bounding rectangle changes up the tree."""
|
515
|
+
parent = self._find_parent(self._root, node)
|
516
|
+
if parent is not None:
|
517
|
+
parent.update_bounding_rect()
|
518
|
+
self._propagate_changes_up(parent)
|
519
|
+
|
520
|
+
def _rebuild_tree(self) -> None:
|
521
|
+
"""Rebuild the R-Tree from existing edges."""
|
522
|
+
if not self._edges:
|
523
|
+
self._root = None
|
524
|
+
self._tree_height = 0
|
525
|
+
self._total_nodes = 0
|
526
|
+
return
|
527
|
+
|
528
|
+
# Collect all edges
|
529
|
+
edges = list(self._edges.values())
|
530
|
+
|
531
|
+
# Clear current tree
|
532
|
+
self._root = None
|
533
|
+
self._tree_height = 0
|
534
|
+
self._total_nodes = 0
|
535
|
+
|
536
|
+
# Rebuild tree by re-inserting all edges
|
537
|
+
for edge in edges:
|
538
|
+
self._insert_edge_into_tree(edge)
|
539
|
+
|
540
|
+
def _insert_edge_into_tree(self, edge: SpatialEdge) -> None:
|
541
|
+
"""Insert edge into R-Tree structure."""
|
542
|
+
if self._root is None:
|
543
|
+
self._root = RTreeNode(is_leaf=True, max_entries=self.max_entries)
|
544
|
+
self._tree_height = 1
|
545
|
+
self._total_nodes = 1
|
546
|
+
|
547
|
+
# Choose leaf for insertion
|
548
|
+
leaf = self._choose_leaf(edge.bounding_rect)
|
549
|
+
|
550
|
+
# Add edge to leaf
|
551
|
+
leaf.add_entry(edge.bounding_rect, edge)
|
552
|
+
|
553
|
+
# Handle overflow
|
554
|
+
if leaf.is_full():
|
555
|
+
self._handle_overflow(leaf)
|
556
|
+
|
557
|
+
def _handle_overflow(self, node: RTreeNode) -> None:
|
558
|
+
"""Handle overflow in a node by splitting."""
|
559
|
+
if not node.is_full():
|
560
|
+
return
|
561
|
+
|
562
|
+
# Split the node
|
563
|
+
node1, node2 = self._split_node(node)
|
564
|
+
|
565
|
+
# If this is the root, create a new root
|
566
|
+
if node == self._root:
|
567
|
+
new_root = RTreeNode(is_leaf=False, max_entries=self.max_entries)
|
568
|
+
new_root.add_entry(node1.bounding_rect, node1)
|
569
|
+
new_root.add_entry(node2.bounding_rect, node2)
|
570
|
+
self._root = new_root
|
571
|
+
self._tree_height += 1
|
572
|
+
self._total_nodes += 1
|
573
|
+
else:
|
574
|
+
# Replace the original node with the first split node
|
575
|
+
parent = self._find_parent(self._root, node)
|
576
|
+
if parent is not None:
|
577
|
+
# Find and replace the original node
|
578
|
+
for i, (rect, child) in enumerate(parent.entries):
|
579
|
+
if child == node:
|
580
|
+
parent.entries[i] = (node1.bounding_rect, node1)
|
581
|
+
break
|
582
|
+
|
583
|
+
# Add the second split node to parent
|
584
|
+
parent.add_entry(node2.bounding_rect, node2)
|
585
|
+
parent.update_bounding_rect()
|
586
|
+
|
587
|
+
# Handle overflow in parent if necessary
|
588
|
+
if parent.is_full():
|
589
|
+
self._handle_overflow(parent)
|
590
|
+
|
591
|
+
def has_edge(self, source: str, target: str) -> bool:
|
592
|
+
"""Check if edge exists."""
|
593
|
+
for edge in self._edges.values():
|
594
|
+
if edge.source == source and edge.target == target:
|
595
|
+
return True
|
596
|
+
return False
|
597
|
+
|
598
|
+
def get_edge_data(self, source: str, target: str) -> Optional[Dict[str, Any]]:
|
599
|
+
"""Get edge data."""
|
600
|
+
for edge in self._edges.values():
|
601
|
+
if edge.source == source and edge.target == target:
|
602
|
+
return edge.to_dict()
|
603
|
+
return None
|
604
|
+
|
605
|
+
def neighbors(self, vertex: str, direction: str = 'out') -> Iterator[str]:
|
606
|
+
"""Get neighbors of vertex."""
|
607
|
+
neighbors = set()
|
608
|
+
|
609
|
+
for edge in self._edges.values():
|
610
|
+
if direction in ['out', 'both'] and edge.source == vertex:
|
611
|
+
neighbors.add(edge.target)
|
612
|
+
if direction in ['in', 'both'] and edge.target == vertex:
|
613
|
+
neighbors.add(edge.source)
|
614
|
+
|
615
|
+
return iter(neighbors)
|
616
|
+
|
617
|
+
def degree(self, vertex: str, direction: str = 'out') -> int:
|
618
|
+
"""Get degree of vertex."""
|
619
|
+
return len(list(self.neighbors(vertex, direction)))
|
620
|
+
|
621
|
+
def edges(self, data: bool = False) -> Iterator[tuple]:
|
622
|
+
"""Get all edges."""
|
623
|
+
for edge in self._edges.values():
|
624
|
+
if data:
|
625
|
+
yield (edge.source, edge.target, edge.to_dict())
|
626
|
+
else:
|
627
|
+
yield (edge.source, edge.target)
|
628
|
+
|
629
|
+
def vertices(self) -> Iterator[str]:
|
630
|
+
"""Get all vertices."""
|
631
|
+
return iter(self._vertices)
|
632
|
+
|
633
|
+
def __len__(self) -> int:
|
634
|
+
"""Get number of edges."""
|
635
|
+
return self._edge_count
|
636
|
+
|
637
|
+
def vertex_count(self) -> int:
|
638
|
+
"""Get number of vertices."""
|
639
|
+
return len(self._vertices)
|
640
|
+
|
641
|
+
def clear(self) -> None:
|
642
|
+
"""Clear all data."""
|
643
|
+
self._edges.clear()
|
644
|
+
self._vertex_coords.clear()
|
645
|
+
self._vertices.clear()
|
646
|
+
self._root = None
|
647
|
+
self._edge_count = 0
|
648
|
+
self._edge_id_counter = 0
|
649
|
+
self._tree_height = 0
|
650
|
+
|
651
|
+
def add_vertex(self, vertex: str, coords: Tuple[float, float] = None) -> None:
|
652
|
+
"""Add vertex with coordinates."""
|
653
|
+
self._vertices.add(vertex)
|
654
|
+
if coords:
|
655
|
+
self._vertex_coords[vertex] = coords
|
656
|
+
|
657
|
+
def remove_vertex(self, vertex: str) -> bool:
|
658
|
+
"""Remove vertex and all its edges."""
|
659
|
+
if vertex not in self._vertices:
|
660
|
+
return False
|
661
|
+
|
662
|
+
# Remove all edges involving this vertex
|
663
|
+
edges_to_remove = [eid for eid, edge in self._edges.items()
|
664
|
+
if edge.source == vertex or edge.target == vertex]
|
665
|
+
|
666
|
+
for edge_id in edges_to_remove:
|
667
|
+
edge = self._edges[edge_id]
|
668
|
+
self.remove_edge(edge.source, edge.target, edge_id)
|
669
|
+
|
670
|
+
self._vertices.discard(vertex)
|
671
|
+
self._vertex_coords.pop(vertex, None)
|
672
|
+
|
673
|
+
return True
|
674
|
+
|
675
|
+
# ============================================================================
|
676
|
+
# SPATIAL QUERY OPERATIONS
|
677
|
+
# ============================================================================
|
678
|
+
|
679
|
+
def range_query(self, min_x: float, min_y: float, max_x: float, max_y: float) -> List[SpatialEdge]:
|
680
|
+
"""Find all edges intersecting with rectangle."""
|
681
|
+
query_rect = Rectangle(min_x, min_y, max_x, max_y)
|
682
|
+
result = []
|
683
|
+
|
684
|
+
def search_node(node: RTreeNode):
|
685
|
+
if node is None:
|
686
|
+
return
|
687
|
+
|
688
|
+
for rect, data in node.entries:
|
689
|
+
if query_rect.intersects(rect):
|
690
|
+
if node.is_leaf:
|
691
|
+
# data is SpatialEdge
|
692
|
+
if data.intersects_rectangle(query_rect):
|
693
|
+
result.append(data)
|
694
|
+
else:
|
695
|
+
# data is child node
|
696
|
+
search_node(data)
|
697
|
+
|
698
|
+
search_node(self._root)
|
699
|
+
return result
|
700
|
+
|
701
|
+
def point_query(self, x: float, y: float, radius: float = 0.0) -> List[SpatialEdge]:
|
702
|
+
"""Find edges near a point within radius."""
|
703
|
+
if radius == 0.0:
|
704
|
+
# Exact point query
|
705
|
+
query_rect = Rectangle(x, y, x, y)
|
706
|
+
else:
|
707
|
+
# Range query with radius
|
708
|
+
query_rect = Rectangle(x - radius, y - radius, x + radius, y + radius)
|
709
|
+
|
710
|
+
candidates = self.range_query(query_rect.min_x, query_rect.min_y,
|
711
|
+
query_rect.max_x, query_rect.max_y)
|
712
|
+
|
713
|
+
if radius == 0.0:
|
714
|
+
return candidates
|
715
|
+
|
716
|
+
# Filter by actual distance
|
717
|
+
result = []
|
718
|
+
for edge in candidates:
|
719
|
+
if edge.distance_to_point(x, y) <= radius:
|
720
|
+
result.append(edge)
|
721
|
+
|
722
|
+
return result
|
723
|
+
|
724
|
+
def nearest_neighbor(self, x: float, y: float, k: int = 1) -> List[Tuple[SpatialEdge, float]]:
|
725
|
+
"""Find k nearest edges to point."""
|
726
|
+
# Simple implementation - can be optimized with priority queue
|
727
|
+
distances = []
|
728
|
+
|
729
|
+
for edge in self._edges.values():
|
730
|
+
distance = edge.distance_to_point(x, y)
|
731
|
+
distances.append((edge, distance))
|
732
|
+
|
733
|
+
distances.sort(key=lambda x: x[1])
|
734
|
+
return distances[:k]
|
735
|
+
|
736
|
+
def edges_in_region(self, center_x: float, center_y: float, radius: float) -> List[SpatialEdge]:
|
737
|
+
"""Find all edges within circular region."""
|
738
|
+
# Use square approximation for efficiency
|
739
|
+
candidates = self.range_query(center_x - radius, center_y - radius,
|
740
|
+
center_x + radius, center_y + radius)
|
741
|
+
|
742
|
+
result = []
|
743
|
+
for edge in candidates:
|
744
|
+
# Check if any part of edge is within radius
|
745
|
+
dist_to_source = math.sqrt((edge.source_coords[0] - center_x) ** 2 +
|
746
|
+
(edge.source_coords[1] - center_y) ** 2)
|
747
|
+
dist_to_target = math.sqrt((edge.target_coords[0] - center_x) ** 2 +
|
748
|
+
(edge.target_coords[1] - center_y) ** 2)
|
749
|
+
|
750
|
+
if dist_to_source <= radius or dist_to_target <= radius:
|
751
|
+
result.append(edge)
|
752
|
+
elif edge.distance_to_point(center_x, center_y) <= radius:
|
753
|
+
result.append(edge)
|
754
|
+
|
755
|
+
return result
|
756
|
+
|
757
|
+
def get_bounding_box(self) -> Optional[Rectangle]:
|
758
|
+
"""Get bounding box of all edges."""
|
759
|
+
if self._root and self._root.bounding_rect:
|
760
|
+
return self._root.bounding_rect
|
761
|
+
return None
|
762
|
+
|
763
|
+
def spatial_statistics(self) -> Dict[str, Any]:
|
764
|
+
"""Get spatial statistics."""
|
765
|
+
if not self._edges:
|
766
|
+
return {'edges': 0, 'vertices': 0, 'total_length': 0, 'avg_length': 0}
|
767
|
+
|
768
|
+
lengths = [edge.length for edge in self._edges.values()]
|
769
|
+
bounding_box = self.get_bounding_box()
|
770
|
+
|
771
|
+
return {
|
772
|
+
'edges': len(self._edges),
|
773
|
+
'vertices': len(self._vertices),
|
774
|
+
'total_length': sum(lengths),
|
775
|
+
'avg_length': sum(lengths) / len(lengths),
|
776
|
+
'min_length': min(lengths),
|
777
|
+
'max_length': max(lengths),
|
778
|
+
'bounding_box': bounding_box._asdict() if bounding_box else None,
|
779
|
+
'tree_height': self._tree_height,
|
780
|
+
'spatial_extent': {
|
781
|
+
'width': bounding_box.max_x - bounding_box.min_x if bounding_box else 0,
|
782
|
+
'height': bounding_box.max_y - bounding_box.min_y if bounding_box else 0
|
783
|
+
}
|
784
|
+
}
|
785
|
+
|
786
|
+
# ============================================================================
|
787
|
+
# PERFORMANCE CHARACTERISTICS
|
788
|
+
# ============================================================================
|
789
|
+
|
790
|
+
@property
|
791
|
+
def backend_info(self) -> Dict[str, Any]:
|
792
|
+
"""Get backend implementation info."""
|
793
|
+
return {
|
794
|
+
'strategy': 'R_TREE',
|
795
|
+
'backend': 'R-Tree spatial index with quadratic split',
|
796
|
+
'max_entries': self.max_entries,
|
797
|
+
'min_entries': self.min_entries,
|
798
|
+
'is_directed': self.is_directed,
|
799
|
+
'complexity': {
|
800
|
+
'insert': f'O(log_M n)', # M = max_entries
|
801
|
+
'delete': f'O(log_M n)',
|
802
|
+
'range_query': f'O(log_M n + k)', # k = result size
|
803
|
+
'point_query': f'O(log_M n + k)',
|
804
|
+
'space': 'O(n)'
|
805
|
+
}
|
806
|
+
}
|
807
|
+
|
808
|
+
@property
|
809
|
+
def metrics(self) -> Dict[str, Any]:
|
810
|
+
"""Get performance metrics."""
|
811
|
+
stats = self.spatial_statistics()
|
812
|
+
|
813
|
+
return {
|
814
|
+
'edges': self._edge_count,
|
815
|
+
'vertices': len(self._vertices),
|
816
|
+
'tree_height': self._tree_height,
|
817
|
+
'avg_edge_length': f"{stats.get('avg_length', 0):.2f}",
|
818
|
+
'spatial_extent': f"{stats.get('spatial_extent', {}).get('width', 0):.1f} x {stats.get('spatial_extent', {}).get('height', 0):.1f}",
|
819
|
+
'memory_usage': f"{self._edge_count * 150 + len(self._vertices) * 50} bytes (estimated)"
|
820
|
+
}
|