sarpyx 0.1.5__py3-none-any.whl → 0.1.6__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.
- docs/examples/advanced/batch_processing.py +1 -1
- docs/examples/advanced/custom_processing_chains.py +1 -1
- docs/examples/advanced/performance_optimization.py +1 -1
- docs/examples/basic/snap_integration.py +1 -1
- docs/examples/intermediate/quality_assessment.py +1 -1
- outputs/baseline/20260205-234828/__init__.py +33 -0
- outputs/baseline/20260205-234828/main.py +493 -0
- outputs/final/20260205-234851/__init__.py +33 -0
- outputs/final/20260205-234851/main.py +493 -0
- sarpyx/__init__.py +2 -2
- sarpyx/algorithms/__init__.py +2 -2
- sarpyx/cli/__init__.py +1 -1
- sarpyx/cli/focus.py +3 -5
- sarpyx/cli/main.py +106 -7
- sarpyx/cli/shipdet.py +1 -1
- sarpyx/cli/worldsar.py +549 -0
- sarpyx/processor/__init__.py +1 -1
- sarpyx/processor/core/decode.py +43 -8
- sarpyx/processor/core/focus.py +104 -57
- sarpyx/science/__init__.py +1 -1
- sarpyx/sla/__init__.py +8 -0
- sarpyx/sla/metrics.py +101 -0
- sarpyx/{snap → snapflow}/__init__.py +1 -1
- sarpyx/snapflow/engine.py +6165 -0
- sarpyx/{snap → snapflow}/op.py +0 -1
- sarpyx/utils/__init__.py +1 -1
- sarpyx/utils/geos.py +652 -0
- sarpyx/utils/grid.py +285 -0
- sarpyx/utils/io.py +77 -9
- sarpyx/utils/meta.py +55 -0
- sarpyx/utils/nisar_utils.py +652 -0
- sarpyx/utils/rfigen.py +108 -0
- sarpyx/utils/wkt_utils.py +109 -0
- sarpyx/utils/zarr_utils.py +55 -37
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/METADATA +9 -5
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/RECORD +41 -32
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/WHEEL +1 -1
- sarpyx-0.1.6.dist-info/licenses/LICENSE +201 -0
- sarpyx-0.1.6.dist-info/top_level.txt +4 -0
- tests/test_zarr_compat.py +35 -0
- sarpyx/processor/core/decode_v0.py +0 -0
- sarpyx/processor/core/decode_v1.py +0 -849
- sarpyx/processor/core/focus_old.py +0 -1550
- sarpyx/processor/core/focus_v1.py +0 -1566
- sarpyx/processor/core/focus_v2.py +0 -1625
- sarpyx/snap/engine.py +0 -633
- sarpyx-0.1.5.dist-info/top_level.txt +0 -2
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/entry_points.txt +0 -0
sarpyx/{snap → snapflow}/op.py
RENAMED
sarpyx/utils/__init__.py
CHANGED
sarpyx/utils/geos.py
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
"""Point containment checker for grid points within WKT polygons."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
|
|
7
|
+
from shapely import wkt
|
|
8
|
+
from shapely.geometry import Point, Polygon
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def check_points_in_polygon(
|
|
12
|
+
wkt_polygon: str,
|
|
13
|
+
geojson_path: str = '/Data_large/SARGFM/grid_10km.geojson'
|
|
14
|
+
) -> List[Dict[str, Any]]:
|
|
15
|
+
"""
|
|
16
|
+
Check which points from the GeoJSON file are contained within the given WKT polygon.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
wkt_polygon: A WKT (Well-Known Text) string representing a polygon
|
|
20
|
+
geojson_path: Path to the GeoJSON file containing point features
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of feature dictionaries containing points that fall within the polygon.
|
|
24
|
+
Each feature includes properties (name, row, col, row_idx, col_idx, utm_zone, epsg)
|
|
25
|
+
and geometry (coordinates).
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> wkt_poly = "POLYGON((10 10, 10 20, 20 20, 20 10, 10 10))"
|
|
29
|
+
>>> contained_points = check_points_in_polygon(wkt_poly)
|
|
30
|
+
>>> print(f"Found {len(contained_points)} points")
|
|
31
|
+
"""
|
|
32
|
+
# Parse the WKT polygon
|
|
33
|
+
polygon = wkt.loads(wkt_polygon)
|
|
34
|
+
|
|
35
|
+
# Load the GeoJSON file
|
|
36
|
+
with open(geojson_path, 'r') as f:
|
|
37
|
+
geojson_data = json.load(f)
|
|
38
|
+
|
|
39
|
+
# Use list comprehension for faster filtering of contained points
|
|
40
|
+
contained_features = [
|
|
41
|
+
feature for feature in geojson_data['features']
|
|
42
|
+
if polygon.contains(Point(*feature['geometry']['coordinates']))
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
return contained_features
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_point_names(
|
|
49
|
+
wkt_polygon: str,
|
|
50
|
+
geojson_path: str = '/Data_large/SARGFM/grid_10km.geojson'
|
|
51
|
+
) -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
Get the names of points contained within the given WKT polygon.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
wkt_polygon: A WKT (Well-Known Text) string representing a polygon
|
|
57
|
+
geojson_path: Path to the GeoJSON file containing point features
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of point names (e.g., ["946D_176L", "946D_175L", ...])
|
|
61
|
+
"""
|
|
62
|
+
contained_features = check_points_in_polygon(wkt_polygon, geojson_path)
|
|
63
|
+
return [feature['properties']['name'] for feature in contained_features]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_point_coordinates(
|
|
67
|
+
wkt_polygon: str,
|
|
68
|
+
geojson_path: str = '/Data_large/SARGFM/grid_10km.geojson'
|
|
69
|
+
) -> List[tuple]:
|
|
70
|
+
"""
|
|
71
|
+
Get the coordinates of points contained within the given WKT polygon.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
wkt_polygon: A WKT (Well-Known Text) string representing a polygon
|
|
75
|
+
geojson_path: Path to the GeoJSON file containing point features
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of coordinate tuples [(lon, lat), ...]
|
|
79
|
+
"""
|
|
80
|
+
contained_features = check_points_in_polygon(wkt_polygon, geojson_path)
|
|
81
|
+
return [
|
|
82
|
+
tuple(feature['geometry']['coordinates'])
|
|
83
|
+
for feature in contained_features
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
class GridNavigator:
|
|
87
|
+
"""
|
|
88
|
+
Grid navigation utilities for moving between adjacent grid points.
|
|
89
|
+
This module provides GridNavigator which navigates using only point names
|
|
90
|
+
without requiring the Grid object.
|
|
91
|
+
|
|
92
|
+
The grid naming convention:
|
|
93
|
+
- Rows: XD (south of equator), 0U (equator), XU (north of equator)
|
|
94
|
+
- Columns: XL (west of prime meridian), 0R (prime meridian), XR (east of prime meridian)
|
|
95
|
+
|
|
96
|
+
Navigation:
|
|
97
|
+
- North (up): Row number decreases in D hemisphere, increases in U hemisphere
|
|
98
|
+
- South (down): Row number increases in D hemisphere, decreases in U hemisphere
|
|
99
|
+
- East (right): Column number increases in R hemisphere, decreases in L hemisphere
|
|
100
|
+
- West (left): Column number decreases in R hemisphere, increases in L hemisphere
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self):
|
|
104
|
+
"""Initialize navigator."""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _parse_row(row: str) -> tuple[int, str]:
|
|
109
|
+
"""Parse row name into number and hemisphere.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
row: Row name like '298D' or '5U'
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Tuple of (number, hemisphere) e.g., (298, 'D')
|
|
116
|
+
"""
|
|
117
|
+
if row.endswith('U'):
|
|
118
|
+
return int(row[:-1]), 'U'
|
|
119
|
+
elif row.endswith('D'):
|
|
120
|
+
return int(row[:-1]), 'D'
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError(f'Invalid row format: {row}')
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _parse_col(col: str) -> tuple[int, str]:
|
|
126
|
+
"""Parse column name into number and hemisphere.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
col: Column name like '323R' or '10L'
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Tuple of (number, hemisphere) e.g., (323, 'R')
|
|
133
|
+
"""
|
|
134
|
+
if col.endswith('R'):
|
|
135
|
+
return int(col[:-1]), 'R'
|
|
136
|
+
elif col.endswith('L'):
|
|
137
|
+
return int(col[:-1]), 'L'
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError(f'Invalid column format: {col}')
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _make_row(num: int, hemisphere: str) -> str:
|
|
143
|
+
"""Create row name from number and hemisphere.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
num: Row number
|
|
147
|
+
hemisphere: 'U' or 'D'
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Row name like '298D' or '5U'
|
|
151
|
+
"""
|
|
152
|
+
return f'{num}{hemisphere}'
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _make_col(num: int, hemisphere: str) -> str:
|
|
156
|
+
"""Create column name from number and hemisphere.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
num: Column number
|
|
160
|
+
hemisphere: 'R' or 'L'
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Column name like '323R' or '10L'
|
|
164
|
+
"""
|
|
165
|
+
return f'{num}{hemisphere}'
|
|
166
|
+
|
|
167
|
+
def move_up(self, row: str, col: str) -> str:
|
|
168
|
+
"""Get the name of the point immediately above (northward).
|
|
169
|
+
|
|
170
|
+
Moving north:
|
|
171
|
+
- In D hemisphere: 298D → 297D → ... → 1D → 0U
|
|
172
|
+
- In U hemisphere: 0U → 1U → 2U → ...
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
row: Current row (e.g., '298D', '5U')
|
|
176
|
+
col: Current column (e.g., '323R', '10L')
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Name of the upper point
|
|
180
|
+
"""
|
|
181
|
+
num, hemisphere = self._parse_row(row)
|
|
182
|
+
|
|
183
|
+
if hemisphere == 'D':
|
|
184
|
+
# Moving north in southern hemisphere
|
|
185
|
+
if num == 1:
|
|
186
|
+
# Cross to northern hemisphere
|
|
187
|
+
new_row = '0U'
|
|
188
|
+
else:
|
|
189
|
+
# Stay in southern hemisphere
|
|
190
|
+
new_row = self._make_row(num - 1, 'D')
|
|
191
|
+
else: # hemisphere == 'U'
|
|
192
|
+
# Moving north in northern hemisphere
|
|
193
|
+
new_row = self._make_row(num + 1, 'U')
|
|
194
|
+
|
|
195
|
+
return f'{new_row}_{col}'
|
|
196
|
+
|
|
197
|
+
def move_down(self, row: str, col: str) -> str:
|
|
198
|
+
"""Get the name of the point immediately below (southward).
|
|
199
|
+
|
|
200
|
+
Moving south:
|
|
201
|
+
- In U hemisphere: 298U → 297U → ... → 1U → 0U
|
|
202
|
+
- In D hemisphere: 0U → 1D → 2D → ...
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
row: Current row (e.g., '298D', '5U')
|
|
206
|
+
col: Current column (e.g., '323R', '10L')
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Name of the lower point
|
|
210
|
+
"""
|
|
211
|
+
num, hemisphere = self._parse_row(row)
|
|
212
|
+
|
|
213
|
+
if hemisphere == 'U':
|
|
214
|
+
# Moving south in northern hemisphere
|
|
215
|
+
if num == 0:
|
|
216
|
+
# Cross to southern hemisphere
|
|
217
|
+
new_row = '1D'
|
|
218
|
+
elif num == 1:
|
|
219
|
+
# Move to equator
|
|
220
|
+
new_row = '0U'
|
|
221
|
+
else:
|
|
222
|
+
# Stay in northern hemisphere
|
|
223
|
+
new_row = self._make_row(num - 1, 'U')
|
|
224
|
+
else: # hemisphere == 'D'
|
|
225
|
+
# Moving south in southern hemisphere
|
|
226
|
+
new_row = self._make_row(num + 1, 'D')
|
|
227
|
+
|
|
228
|
+
return f'{new_row}_{col}'
|
|
229
|
+
|
|
230
|
+
def move_right(self, row: str, col: str) -> str:
|
|
231
|
+
"""Get the name of the point immediately to the right (eastward).
|
|
232
|
+
|
|
233
|
+
Moving east:
|
|
234
|
+
- In L hemisphere: 10L → 9L → ... → 1L → 0R
|
|
235
|
+
- In R hemisphere: 0R → 1R → 2R → ...
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
row: Current row (e.g., '298D', '5U')
|
|
239
|
+
col: Current column (e.g., '323R', '10L')
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Name of the right point
|
|
243
|
+
"""
|
|
244
|
+
num, hemisphere = self._parse_col(col)
|
|
245
|
+
|
|
246
|
+
if hemisphere == 'L':
|
|
247
|
+
# Moving east in western hemisphere
|
|
248
|
+
if num == 1:
|
|
249
|
+
# Cross to eastern hemisphere
|
|
250
|
+
new_col = '0R'
|
|
251
|
+
else:
|
|
252
|
+
# Stay in western hemisphere
|
|
253
|
+
new_col = self._make_col(num - 1, 'L')
|
|
254
|
+
else: # hemisphere == 'R'
|
|
255
|
+
# Moving east in eastern hemisphere
|
|
256
|
+
new_col = self._make_col(num + 1, 'R')
|
|
257
|
+
|
|
258
|
+
return f'{row}_{new_col}'
|
|
259
|
+
|
|
260
|
+
def move_left(self, row: str, col: str) -> str:
|
|
261
|
+
"""Get the name of the point immediately to the left (westward).
|
|
262
|
+
|
|
263
|
+
Moving west:
|
|
264
|
+
- In R hemisphere: 10R → 9R → ... → 1R → 0R
|
|
265
|
+
- In L hemisphere: 0R → 1L → 2L → ...
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
row: Current row (e.g., '298D', '5U')
|
|
269
|
+
col: Current column (e.g., '323R', '10L')
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Name of the left point
|
|
273
|
+
"""
|
|
274
|
+
num, hemisphere = self._parse_col(col)
|
|
275
|
+
|
|
276
|
+
if hemisphere == 'R':
|
|
277
|
+
# Moving west in eastern hemisphere
|
|
278
|
+
if num == 0:
|
|
279
|
+
# Cross to western hemisphere
|
|
280
|
+
new_col = '1L'
|
|
281
|
+
else:
|
|
282
|
+
# Stay in eastern hemisphere
|
|
283
|
+
new_col = self._make_col(num - 1, 'R')
|
|
284
|
+
else: # hemisphere == 'L'
|
|
285
|
+
# Moving west in western hemisphere
|
|
286
|
+
new_col = self._make_col(num + 1, 'L')
|
|
287
|
+
|
|
288
|
+
return f'{row}_{new_col}'
|
|
289
|
+
|
|
290
|
+
def move_up_right(self, row: str, col: str) -> str:
|
|
291
|
+
"""Get the name of the point diagonally upper-right (north-east).
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
row: Current row (e.g., '298D', '5U')
|
|
295
|
+
col: Current column (e.g., '323R', '10L')
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Name of the upper-right point
|
|
299
|
+
"""
|
|
300
|
+
# Move up
|
|
301
|
+
upper_name = self.move_up(row, col)
|
|
302
|
+
upper_row, upper_col = upper_name.split('_')
|
|
303
|
+
|
|
304
|
+
# Then move right
|
|
305
|
+
return self.move_right(upper_row, upper_col)
|
|
306
|
+
|
|
307
|
+
def move_up_left(self, row: str, col: str) -> str:
|
|
308
|
+
"""Get the name of the point diagonally upper-left (north-west).
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
row: Current row (e.g., '298D', '5U')
|
|
312
|
+
col: Current column (e.g., '323R', '10L')
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Name of the upper-left point
|
|
316
|
+
"""
|
|
317
|
+
upper_name = self.move_up(row, col)
|
|
318
|
+
upper_row, upper_col = upper_name.split('_')
|
|
319
|
+
return self.move_left(upper_row, upper_col)
|
|
320
|
+
|
|
321
|
+
def move_down_right(self, row: str, col: str) -> str:
|
|
322
|
+
"""Get the name of the point diagonally lower-right (south-east).
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
row: Current row (e.g., '298D', '5U')
|
|
326
|
+
col: Current column (e.g., '323R', '10L')
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Name of the lower-right point
|
|
330
|
+
"""
|
|
331
|
+
lower_name = self.move_down(row, col)
|
|
332
|
+
lower_row, lower_col = lower_name.split('_')
|
|
333
|
+
return self.move_right(lower_row, lower_col)
|
|
334
|
+
|
|
335
|
+
def move_down_left(self, row: str, col: str) -> str:
|
|
336
|
+
"""Get the name of the point diagonally lower-left (south-west).
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
row: Current row (e.g., '298D', '5U')
|
|
340
|
+
col: Current column (e.g., '323R', '10L')
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Name of the lower-left point
|
|
344
|
+
"""
|
|
345
|
+
lower_name = self.move_down(row, col)
|
|
346
|
+
lower_row, lower_col = lower_name.split('_')
|
|
347
|
+
return self.move_left(lower_row, lower_col)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def is_point_in_contained(name, contained):
|
|
351
|
+
for feature in contained:
|
|
352
|
+
props = feature['properties']
|
|
353
|
+
if props['name'] == name:
|
|
354
|
+
return True
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def rectangle_to_wkt(rectangle: dict) -> str:
|
|
359
|
+
"""Convert a rectangle dictionary to WKT POLYGON format.
|
|
360
|
+
|
|
361
|
+
Takes a dictionary containing four corner points (TL, TR, BR, BL) with their
|
|
362
|
+
coordinates and creates a closed polygon in Well-Known Text format. The polygon
|
|
363
|
+
is closed by repeating the first point at the end.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
rectangle: Dictionary with keys 'TL', 'TR', 'BR', 'BL', each containing
|
|
367
|
+
a GeoJSON Feature with geometry.coordinates [lon, lat].
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
WKT POLYGON string in format 'POLYGON ((lon lat, lon lat, ...))'
|
|
371
|
+
|
|
372
|
+
Example:
|
|
373
|
+
>>> rect = {
|
|
374
|
+
... 'TL': {'geometry': {'coordinates': [32.37, -26.68]}},
|
|
375
|
+
... 'TR': {'geometry': {'coordinates': [32.47, -26.68]}},
|
|
376
|
+
... 'BR': {'geometry': {'coordinates': [32.49, -26.77]}},
|
|
377
|
+
... 'BL': {'geometry': {'coordinates': [32.39, -26.77]}}
|
|
378
|
+
... }
|
|
379
|
+
>>> rectangle_to_wkt(rect)
|
|
380
|
+
'POLYGON ((32.37 -26.68, 32.47 -26.68, 32.49 -26.77, 32.39 -26.77, 32.37 -26.68))'
|
|
381
|
+
"""
|
|
382
|
+
# Extract coordinates in clockwise order: TL -> TR -> BR -> BL -> TL (close polygon)
|
|
383
|
+
corners = ['TL', 'TR', 'BR', 'BL']
|
|
384
|
+
coords = [rectangle[corner]['geometry']['coordinates'] for corner in corners]
|
|
385
|
+
|
|
386
|
+
# Close the polygon by adding the first point at the end
|
|
387
|
+
coords.append(coords[0])
|
|
388
|
+
|
|
389
|
+
# Format as WKT: 'lon lat' pairs separated by commas
|
|
390
|
+
coord_strings = [f'{lon} {lat}' for lon, lat in coords]
|
|
391
|
+
wkt = f"POLYGON (({', '.join(coord_strings)}))"
|
|
392
|
+
|
|
393
|
+
return wkt
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def rectanglify(contained: list) -> list | None:
|
|
397
|
+
"""
|
|
398
|
+
Build rectangles for cutting based on contained points.
|
|
399
|
+
|
|
400
|
+
This function identifies rectangles formed by points in the `contained` list.
|
|
401
|
+
It uses the `GridNavigator` class to navigate between points and checks if
|
|
402
|
+
the points form a valid rectangle.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
contained (list): A list of GeoJSON-like features, where each feature
|
|
406
|
+
contains properties (e.g., 'row', 'col', 'name') and
|
|
407
|
+
geometry (e.g., coordinates).
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
list | None: A list of rectangles, where each rectangle is represented
|
|
411
|
+
as a dictionary with keys 'TL', 'TR', 'BR', 'BL' (top-left,
|
|
412
|
+
top-right, bottom-right, bottom-left). Returns None if no
|
|
413
|
+
rectangles are found.
|
|
414
|
+
|
|
415
|
+
Example:
|
|
416
|
+
>>> contained_points = [
|
|
417
|
+
... {'properties': {'name': '1U_1R', 'row': '1U', 'col': '1R'}, 'geometry': {'coordinates': [0, 0]}},
|
|
418
|
+
... {'properties': {'name': '1U_2R', 'row': '1U', 'col': '2R'}, 'geometry': {'coordinates': [1, 0]}},
|
|
419
|
+
... {'properties': {'name': '2U_1R', 'row': '2U', 'col': '1R'}, 'geometry': {'coordinates': [0, 1]}},
|
|
420
|
+
... {'properties': {'name': '2U_2R', 'row': '2U', 'col': '2R'}, 'geometry': {'coordinates': [1, 1]}}
|
|
421
|
+
... ]
|
|
422
|
+
>>> rectangles = rectanglify(contained_points)
|
|
423
|
+
>>> print(rectangles)
|
|
424
|
+
"""
|
|
425
|
+
rectangles = []
|
|
426
|
+
navi = GridNavigator()
|
|
427
|
+
for idx in range(len(contained)):
|
|
428
|
+
row, col = contained[idx]['properties']['row'], contained[idx]['properties']['col']
|
|
429
|
+
|
|
430
|
+
# Find the top-left corner
|
|
431
|
+
TL = navi.move_up(row, col)
|
|
432
|
+
|
|
433
|
+
if is_point_in_contained(TL, contained):
|
|
434
|
+
# Find the top-right corner
|
|
435
|
+
TR = navi.move_up_right(row, col)
|
|
436
|
+
if is_point_in_contained(TR, contained):
|
|
437
|
+
# Find the bottom-right corner
|
|
438
|
+
BR = navi.move_right(row, col)
|
|
439
|
+
if is_point_in_contained(BR, contained):
|
|
440
|
+
# Bottom-left corner is the current point
|
|
441
|
+
BL = f"{row}_{col}"
|
|
442
|
+
# Create a rectangle dictionary
|
|
443
|
+
rectangle = {
|
|
444
|
+
'TL': [point for point in contained if point['properties']['name'] == TL][0],
|
|
445
|
+
'TR': [point for point in contained if point['properties']['name'] == TR][0],
|
|
446
|
+
'BR': [point for point in contained if point['properties']['name'] == BR][0],
|
|
447
|
+
'BL': [point for point in contained if point['properties']['name'] == BL][0]
|
|
448
|
+
}
|
|
449
|
+
rectangles.append(rectangle)
|
|
450
|
+
else:
|
|
451
|
+
print(f'Point {TL} not in contained points.')
|
|
452
|
+
|
|
453
|
+
return rectangles if len(rectangles) > 0 else None
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# if __name__ == '__main__':
|
|
476
|
+
# print('='*80)
|
|
477
|
+
# print('GRID NAVIGATION TESTS (NAME-BASED)')
|
|
478
|
+
# print('='*80)
|
|
479
|
+
# print('\nNavigator uses only point names - no Grid object required!\n')
|
|
480
|
+
|
|
481
|
+
# # Create navigator (no Grid needed!)
|
|
482
|
+
# navigator = GridNavigator()
|
|
483
|
+
|
|
484
|
+
# # Test 1: Basic movements in Southern Hemisphere
|
|
485
|
+
# print('='*80)
|
|
486
|
+
# print('TEST 1: Basic movements in Southern Hemisphere')
|
|
487
|
+
# print('='*80)
|
|
488
|
+
# test_row, test_col = '100D', '50R'
|
|
489
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
490
|
+
|
|
491
|
+
# up = navigator.move_up(test_row, test_col)
|
|
492
|
+
# print(f' ↑ Up (north): {up}')
|
|
493
|
+
|
|
494
|
+
# down = navigator.move_down(test_row, test_col)
|
|
495
|
+
# print(f' ↓ Down (south): {down}')
|
|
496
|
+
|
|
497
|
+
# right = navigator.move_right(test_row, test_col)
|
|
498
|
+
# print(f' → Right (east): {right}')
|
|
499
|
+
|
|
500
|
+
# left = navigator.move_left(test_row, test_col)
|
|
501
|
+
# print(f' ← Left (west): {left}')
|
|
502
|
+
|
|
503
|
+
# up_right = navigator.move_up_right(test_row, test_col)
|
|
504
|
+
# print(f' ↗ Up-right (NE): {up_right}')
|
|
505
|
+
|
|
506
|
+
# # Test 2: Basic movements in Northern Hemisphere
|
|
507
|
+
# print('\n' + '='*80)
|
|
508
|
+
# print('TEST 2: Basic movements in Northern Hemisphere')
|
|
509
|
+
# print('='*80)
|
|
510
|
+
# test_row, test_col = '50U', '100R'
|
|
511
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
512
|
+
|
|
513
|
+
# up = navigator.move_up(test_row, test_col)
|
|
514
|
+
# print(f' ↑ Up (north): {up}')
|
|
515
|
+
|
|
516
|
+
# down = navigator.move_down(test_row, test_col)
|
|
517
|
+
# print(f' ↓ Down (south): {down}')
|
|
518
|
+
|
|
519
|
+
# right = navigator.move_right(test_row, test_col)
|
|
520
|
+
# print(f' → Right (east): {right}')
|
|
521
|
+
|
|
522
|
+
# left = navigator.move_left(test_row, test_col)
|
|
523
|
+
# print(f' ← Left (west): {left}')
|
|
524
|
+
|
|
525
|
+
# # Test 3: Crossing equator from south to north
|
|
526
|
+
# print('\n' + '='*80)
|
|
527
|
+
# print('TEST 3: Crossing the equator (South → North)')
|
|
528
|
+
# print('='*80)
|
|
529
|
+
# test_row, test_col = '1D', '100R'
|
|
530
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
531
|
+
|
|
532
|
+
# up = navigator.move_up(test_row, test_col)
|
|
533
|
+
# print(f' ↑ Up: {test_row}_{test_col} → {up}')
|
|
534
|
+
# assert up == '0U_100R', f'Expected 0U_100R, got {up}'
|
|
535
|
+
# print(f' ✓ Successfully crossed from D to U!')
|
|
536
|
+
|
|
537
|
+
# # Test 4: Crossing equator from north to south
|
|
538
|
+
# print('\n' + '='*80)
|
|
539
|
+
# print('TEST 4: Crossing the equator (North → South)')
|
|
540
|
+
# print('='*80)
|
|
541
|
+
# test_row, test_col = '0U', '100R'
|
|
542
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
543
|
+
|
|
544
|
+
# down = navigator.move_down(test_row, test_col)
|
|
545
|
+
# print(f' ↓ Down: {test_row}_{test_col} → {down}')
|
|
546
|
+
# assert down == '1D_100R', f'Expected 1D_100R, got {down}'
|
|
547
|
+
# print(f' ✓ Successfully crossed from U to D!')
|
|
548
|
+
|
|
549
|
+
# # Test 5: Moving through equator
|
|
550
|
+
# print('\n' + '='*80)
|
|
551
|
+
# print('TEST 5: Sequential movement through equator')
|
|
552
|
+
# print('='*80)
|
|
553
|
+
# test_row, test_col = '2D', '50R'
|
|
554
|
+
# print(f'Starting: {test_row}_{test_col}')
|
|
555
|
+
|
|
556
|
+
# current_row, current_col = test_row, test_col
|
|
557
|
+
# for i in range(5):
|
|
558
|
+
# next_point = navigator.move_up(current_row, current_col)
|
|
559
|
+
# print(f' Step {i+1}: {current_row}_{current_col} → {next_point}')
|
|
560
|
+
# current_row, current_col = next_point.split('_')
|
|
561
|
+
# print(f' ✓ Traversed: 2D → 1D → 0U → 1U → 2U → 3U')
|
|
562
|
+
|
|
563
|
+
# # Test 6: Crossing prime meridian from west to east
|
|
564
|
+
# print('\n' + '='*80)
|
|
565
|
+
# print('TEST 6: Crossing prime meridian (West → East)')
|
|
566
|
+
# print('='*80)
|
|
567
|
+
# test_row, test_col = '50D', '1L'
|
|
568
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
569
|
+
|
|
570
|
+
# right = navigator.move_right(test_row, test_col)
|
|
571
|
+
# print(f' → Right: {test_row}_{test_col} → {right}')
|
|
572
|
+
# assert right == '50D_0R', f'Expected 50D_0R, got {right}'
|
|
573
|
+
# print(f' ✓ Successfully crossed from L to R!')
|
|
574
|
+
|
|
575
|
+
# # Test 7: Crossing prime meridian from east to west
|
|
576
|
+
# print('\n' + '='*80)
|
|
577
|
+
# print('TEST 7: Crossing prime meridian (East → West)')
|
|
578
|
+
# print('='*80)
|
|
579
|
+
# test_row, test_col = '50D', '0R'
|
|
580
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
581
|
+
|
|
582
|
+
# left = navigator.move_left(test_row, test_col)
|
|
583
|
+
# print(f' ← Left: {test_row}_{test_col} → {left}')
|
|
584
|
+
# assert left == '50D_1L', f'Expected 50D_1L, got {left}'
|
|
585
|
+
# print(f' ✓ Successfully crossed from R to L!')
|
|
586
|
+
|
|
587
|
+
# # Test 8: Sequential movement through prime meridian
|
|
588
|
+
# print('\n' + '='*80)
|
|
589
|
+
# print('TEST 8: Sequential movement through prime meridian')
|
|
590
|
+
# print('='*80)
|
|
591
|
+
# test_row, test_col = '100U', '2L'
|
|
592
|
+
# print(f'Starting: {test_row}_{test_col}')
|
|
593
|
+
|
|
594
|
+
# current_row, current_col = test_row, test_col
|
|
595
|
+
# for i in range(5):
|
|
596
|
+
# next_point = navigator.move_right(current_row, current_col)
|
|
597
|
+
# print(f' Step {i+1}: {current_row}_{current_col} → {next_point}')
|
|
598
|
+
# current_row, current_col = next_point.split('_')
|
|
599
|
+
# print(f' ✓ Traversed: 2L → 1L → 0R → 1R → 2R → 3R')
|
|
600
|
+
|
|
601
|
+
# # Test 9: Consistency - round trip
|
|
602
|
+
# print('\n' + '='*80)
|
|
603
|
+
# print('TEST 9: Round-trip consistency')
|
|
604
|
+
# print('='*80)
|
|
605
|
+
# test_row, test_col = '200D', '150R'
|
|
606
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
607
|
+
|
|
608
|
+
# # Up and down
|
|
609
|
+
# up = navigator.move_up(test_row, test_col)
|
|
610
|
+
# up_row, up_col = up.split('_')
|
|
611
|
+
# back_down = navigator.move_down(up_row, up_col)
|
|
612
|
+
# print(f' Up then down: {test_row}_{test_col} → {up} → {back_down}')
|
|
613
|
+
# assert back_down == f'{test_row}_{test_col}', 'Should return to original'
|
|
614
|
+
# print(f' ✓ Passed!')
|
|
615
|
+
|
|
616
|
+
# # Right and left
|
|
617
|
+
# right = navigator.move_right(test_row, test_col)
|
|
618
|
+
# right_row, right_col = right.split('_')
|
|
619
|
+
# back_left = navigator.move_left(right_row, right_col)
|
|
620
|
+
# print(f' Right then left: {test_row}_{test_col} → {right} → {back_left}')
|
|
621
|
+
# assert back_left == f'{test_row}_{test_col}', 'Should return to original'
|
|
622
|
+
# print(f' ✓ Passed!')
|
|
623
|
+
|
|
624
|
+
# # Test 10: Diagonal movements
|
|
625
|
+
# print('\n' + '='*80)
|
|
626
|
+
# print('TEST 10: Diagonal movements')
|
|
627
|
+
# print('='*80)
|
|
628
|
+
# test_row, test_col = '50D', '50R'
|
|
629
|
+
# print(f'Starting point: {test_row}_{test_col}')
|
|
630
|
+
|
|
631
|
+
# ne = navigator.move_up_right(test_row, test_col)
|
|
632
|
+
# print(f' ↗ NE: {ne}')
|
|
633
|
+
|
|
634
|
+
# nw = navigator.move_up_left(test_row, test_col)
|
|
635
|
+
# print(f' ↖ NW: {nw}')
|
|
636
|
+
|
|
637
|
+
# se = navigator.move_down_right(test_row, test_col)
|
|
638
|
+
# print(f' ↘ SE: {se}')
|
|
639
|
+
|
|
640
|
+
# sw = navigator.move_down_left(test_row, test_col)
|
|
641
|
+
# print(f' ↙ SW: {sw}')
|
|
642
|
+
|
|
643
|
+
# print('\n' + '='*80)
|
|
644
|
+
# print('ALL TESTS COMPLETED')
|
|
645
|
+
# print('='*80)
|
|
646
|
+
# print('\nSUMMARY:')
|
|
647
|
+
# print('- GridNavigator works purely by naming conventions')
|
|
648
|
+
# print('- No Grid object required for navigation')
|
|
649
|
+
# print('- Correctly handles hemisphere transitions (D↔U, L↔R)')
|
|
650
|
+
# print('- Round-trip navigation returns to origin')
|
|
651
|
+
# print('- Diagonal movements combine cardinal directions')
|
|
652
|
+
# print('='*80)
|