1
1
"""Extract the burst ID information from a Sentinel-1 SLC product."""
2
2
import argparse
3
+ import sys
4
+ import warnings
5
+ import zipfile
3
6
from itertools import chain
4
7
from pathlib import Path
5
8
from typing import List , Optional , Union
6
- import warnings
9
+
10
+ import lxml .etree as ET
11
+ import shapely .geometry
12
+ import shapely .ops
7
13
8
14
import s1reader
9
15
@@ -22,7 +28,7 @@ def get_bursts(
22
28
return list (chain .from_iterable (burst_nested_list ))
23
29
24
30
25
- def _is_safe_dir (path ) :
31
+ def _is_safe_dir (path : Union [ Path , str ]) -> bool :
26
32
# Rather than matching the name, we just check for the existence of the
27
33
# manifest.safe file and annotation files
28
34
if not (path / "manifest.safe" ).is_file ():
@@ -35,7 +41,7 @@ def _is_safe_dir(path):
35
41
return True
36
42
37
43
38
- def _plot_bursts (safe_path , output_dir = "burst_maps" ):
44
+ def _plot_bursts (safe_path : Union [ Path , str ], output_dir = "burst_maps" ) -> None :
39
45
from s1reader .utils import plot_bursts
40
46
41
47
orbit_dir = None
@@ -48,27 +54,121 @@ def _plot_bursts(safe_path, output_dir="burst_maps"):
48
54
plot_bursts .burst_map (safe_path , orbit_dir , xs , ys , epsg , output_filename )
49
55
50
56
57
+ def get_frame_bounds (safe_path : Union [Path , str ]) -> List [float ]:
58
+ """Get the bounding box of the frame from the union of all burst bounds.
59
+
60
+ bounding box format is [lonmin, latmin, lonmax, latmax]
61
+
62
+ Notes
63
+ -----
64
+ Will use the preview/map-overlay.kml file if it exists, otherwise will
65
+ use the union of all burst bounds (which is slower).
66
+
67
+ Parameters
68
+ ----------
69
+ safe_path : Union[Path, str]
70
+ Path to the SAFE directory or zip file.
71
+
72
+ Returns
73
+ -------
74
+ List[float]
75
+ [lonmin, latmin, lonmax, latmax]
76
+ """
77
+ try :
78
+ return _bounds_from_preview (safe_path )
79
+ except Exception as e :
80
+ warnings .warn (f"Could not get bounds for { safe_path } : { e } " )
81
+ return _bounds_from_bursts (safe_path )
82
+
83
+
84
+ def _bounds_from_preview (safe_path : Union [Path , str ]) -> List [float ]:
85
+ """Get the bounding box of the frame from the preview/map-overlay.kml."""
86
+ # looking for:
87
+ # S1A_IW_SLC__1SDV_20221005T125539_20221005T125606_045307_056AA5_CB45.SAFE/preview/map-overlay.kml
88
+ if _is_safe_dir (safe_path ):
89
+ overlay_path = Path (safe_path ) / "preview" / "map-overlay.kml"
90
+ root = ET .parse (overlay_path ).getroot ()
91
+ else :
92
+ # The name of the unzipped .SAFE directory (with .zip stripped)
93
+ with zipfile .ZipFile (safe_path , "r" ) as zip_ref :
94
+ zname = [
95
+ zi
96
+ for zi in zip_ref .infolist ()
97
+ if "preview/map-overlay.kml" in zi .filename
98
+ ]
99
+ if len (zname ) > 0 :
100
+ with zip_ref .open (zname [0 ].filename , "r" ) as kml_in :
101
+ root = ET .parse (kml_in ).getroot ()
102
+ else :
103
+ root = None
104
+
105
+ if root is None :
106
+ raise ValueError (f"map-overlay.kml does not exist in { safe_path } ." )
107
+
108
+ # point_str looks like:
109
+ # <coordinates>-102.552971,31.482372 -105.191353,31.887299...
110
+ point_str = list (elem .text for elem in root .iter ("coordinates" ))[0 ]
111
+ coords = [p .split ("," ) for p in point_str .split ()]
112
+ lons , lats = zip (* [(float (lon ), float (lat )) for lon , lat in coords ])
113
+ return [min (lons ), min (lats ), max (lons ), max (lats )]
114
+
115
+
116
+ def _bounds_from_bursts (safe_path : Union [Path , str ]) -> List [float ]:
117
+ """Get the bounding box of the frame from the union of all burst bounds."""
118
+ # Get all the bursts from subswath 1, 2, 3
119
+ bursts = None
120
+ for pol in ["vv" , "hh" , "hv" , "vh" ]:
121
+ try :
122
+ bursts = get_bursts (safe_path , pol = pol )
123
+ break
124
+ except ValueError :
125
+ # This is raised if the product doesn't have the specified polarization
126
+ continue
127
+ if bursts is None :
128
+ raise ValueError ("Could not load any polarizations in {safe_path}." )
129
+
130
+ # Convert the border (list of polygons) into a MultiPolygon
131
+ all_borders = [shapely .geometry .MultiPolygon (b .border ) for b in bursts ]
132
+ # Perform a union to get one shape for the entire frame
133
+ border_geom = shapely .ops .unary_union (all_borders )
134
+ # grab the bounds and pad as needed
135
+ return list (border_geom .bounds )
136
+
137
+
51
138
EXAMPLE = """
52
139
Example usage:
53
140
54
141
# Print all bursts in a Sentinel-1 SLC product
55
- s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip
142
+ s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip
56
143
57
144
# Print only the burst IDs
58
- s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE --burst-id
145
+ s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE --burst-id
146
+
147
+ # Print the burst IDs, and the bounding box for each burst
148
+ s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE -b --burst-bbox
59
149
60
150
# Print burst ids for all files matching the pattern
61
- s1_info.py -b S1A_IW_SLC__1SDV_2018*
151
+ s1_info -b S1A_IW_SLC__1SDV_2018*
62
152
63
153
# Print only from subswath IW1, and "vv" polarization
64
- s1_info.py -b S1A_IW_SLC__1SDV_2018* --iw 1 --pol vv
154
+ s1_info -b S1A_IW_SLC__1SDV_2018* --iw 1 --pol vv
65
155
66
156
# Get info for all products in the 'data/' directory
67
- s1_info.py data/
68
-
157
+ s1_info data/
158
+
159
+ # Print the bounding box of the full frame for each product
160
+ s1_info --frame-bbox data/
161
+
162
+ # Using https://github.com/scottstanie/sardem , create a DEM covering the SLC product
163
+ s1_info --frame-bbox S1A_IW_SLC__1SDV_20220226T124745_20220226T124812_042084_050378_F69A.zip |
164
+ cut -d':' -f2 | # separate the bbox from the label
165
+ tr -d ',[]' | # remove brackets and commas
166
+ xargs sardem --data cop --bbox # pass the bbox to sardem as an argument
167
+
168
+
69
169
# Plot the burst map, saving files into the 'burst_maps/' directory
70
- s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE/ --plot
71
- s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip -p -o my_burst_maps
170
+ s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE/ --plot
171
+ s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip -p -o my_burst_maps
72
172
"""
73
173
74
174
@@ -102,6 +202,16 @@ def get_cli_args():
102
202
action = "store_true" ,
103
203
help = "Print only the burst IDs for all bursts." ,
104
204
)
205
+ parser .add_argument (
206
+ "--frame-bbox" ,
207
+ action = "store_true" ,
208
+ help = "Print the frame bounding box (lonmin, latmin, lonmax, latmax) of the S1 product." ,
209
+ )
210
+ parser .add_argument (
211
+ "--burst-bbox" ,
212
+ action = "store_true" ,
213
+ help = "Print each burst's bounding box (lonmin, latmin, lonmax, latmax)." ,
214
+ )
105
215
parser .add_argument (
106
216
"-p" ,
107
217
"--plot" ,
@@ -134,19 +244,28 @@ def main():
134
244
else :
135
245
warnings .warn (f"{ path } is not a file or directory. Skipping." )
136
246
137
- print (f"Found { len (all_files )} Sentinel-1 SLC products." )
247
+ print (f"Found { len (all_files )} Sentinel-1 SLC products." , file = sys . stderr )
138
248
for path in all_files :
139
249
if args .plot :
140
250
_plot_bursts (path )
141
251
continue
252
+ elif args .frame_bbox :
253
+ msg = f"{ path } : { get_frame_bounds (path )} "
254
+ print (msg )
255
+ continue
256
+
142
257
print (f"Bursts in { path } :" )
143
258
print ("-" * 80 )
144
259
# Do we want to pretty-print this with rich?
145
260
for burst in get_bursts (path , args .pol , args .iw ):
146
261
if args .burst_id :
147
- print (burst .burst_id )
262
+ print (burst .burst_id , end = " " )
148
263
else :
149
- print (burst )
264
+ print (burst , end = " " )
265
+
266
+ if args .burst_bbox :
267
+ print (list (shapely .geometry .MultiPolygon (burst .border ).bounds ), end = " " )
268
+ print ()
150
269
151
270
152
271
if __name__ == "__main__" :
0 commit comments