22from datetime import timezone
33
44from dateutil .parser import parse
5- from funcy import distinct , first , last , lkeep
5+ from funcy import distinct , first , last , lkeep , partial
66
7- from nsidc .metgen import constants
7+ from nsidc .metgen import config , constants
8+ from nsidc .metgen .spatial import create_flightline_polygon
89
910
1011def temporal_from_premet (pdict : dict ) -> list :
@@ -184,19 +185,24 @@ def refine_temporal(tvals: list):
184185 return tvals
185186
186187
187- def external_spatial_values (collection_geometry_override , gsr , granule ) -> list :
188+ def external_spatial_values (configuration , gsr , granule ) -> list :
188189 """
189- Retrieve spatial information from a granule-specific spatial ( or spo) file, or
190+ Retrieve spatial information from a granule-specific spatial or spo file, or
190191 the collection metadata.
191192 """
192- if collection_geometry_override :
193+ if configuration . collection_geometry_override :
193194 # Get spatial coverage from collection
194195 return points_from_collection (granule .collection .spatial_extent )
195196
196- return points_from_spatial (granule .spatial_filename , gsr )
197+ return points_from_spatial (granule .spatial_filename , gsr , configuration )
197198
198199
199- def points_from_spatial (spatial_path : str , gsr : str ) -> list :
200+ # TODO: Rename methods and parameters as needed to reduce overloading of the term "spatial."
201+ # Clarify whether we're talking about a spatial file, a spo file, or "spatial content" more
202+ # generically.
203+ def points_from_spatial (
204+ spatial_path : str , gsr : str , configuration : config .Config = None
205+ ) -> list :
200206 """
201207 Read (lon, lat) points from a .spatial or .spo file.
202208 """
@@ -206,30 +212,85 @@ def points_from_spatial(spatial_path: str, gsr: str) -> list:
206212 "spatial_dir is specified but no .spatial or .spo file exists for granule."
207213 )
208214
215+ # If spatial_path doesn't exist, then spatial information is assumed to be available
216+ # in the granule data file or via collection metadata.
209217 if spatial_path is None :
210218 return None
211219
212220 points = raw_points (spatial_path )
221+ if not points :
222+ raise Exception (f"No spatial values found in { spatial_path } ." )
213223
214224 # TODO: We really only need to do the "spo vs spatial" check once, since the same
215225 # file type will (should) be used for all granules.
216226 if re .search (constants .SPO_SUFFIX , spatial_path ):
217227 return parse_spo (gsr , points )
218228
229+ else :
230+ points = parse_spatial (points , configuration )
231+
219232 # confirm the number of points makes sense for this granule spatial representation
220233 if not valid_spatial_config (gsr , len (points )):
221234 raise Exception (
222235 f"Unsupported combination of { gsr } and point count of { len (points )} ."
223236 )
224237
225- # TODO: Handle point cloud creation here if point count is greater than 1 and gsr
226- # is geodetic. Note! Flight line files can be huge!
227238 return points
228239
229240
241+ def parse_spatial (spatial_values : list , configuration : config .Config = None ):
242+ # If only a single point, or two points (assumed to identify a bounding rectangle),
243+ # return spatial values without further processing
244+ if len (spatial_values ) <= 2 :
245+ return spatial_values
246+
247+ # Generate polygon from spatial file data
248+ if configuration is not None and configuration .spatial_polygon_enabled :
249+ try :
250+ # Create configured polygon generator using partial application
251+ generate_polygon = partial (
252+ create_flightline_polygon ,
253+ target_coverage = configuration .spatial_polygon_target_coverage ,
254+ max_vertices = configuration .spatial_polygon_max_vertices ,
255+ cartesian_tolerance = configuration .spatial_polygon_cartesian_tolerance ,
256+ )
257+
258+ # Extract lon/lat arrays from spatial_values
259+ lons = [point ["Longitude" ] for point in spatial_values ]
260+ lats = [point ["Latitude" ] for point in spatial_values ]
261+
262+ # Generate polygon using our configured spatial module
263+ polygon , metadata = generate_polygon (lons , lats )
264+
265+ if polygon is not None :
266+ coords = list (polygon .exterior .coords )
267+ polygon_points = [
268+ {"Longitude" : float (lon ), "Latitude" : float (lat )}
269+ for lon , lat in coords
270+ ]
271+ return polygon_points
272+
273+ except Exception as e :
274+ import logging
275+
276+ logger = logging .getLogger (constants .ROOT_LOGGER )
277+ logger .error (f"Polygon generation failed: { e } " )
278+
279+ # Configuration does not exist, or polygon processing is not enabled.
280+ # Return values without further processing
281+ return spatial_values
282+
283+
230284def valid_spatial_config (gsr : str , point_count : int ) -> str :
231- if (gsr == constants .CARTESIAN ) and (point_count == 2 ):
232- return True
285+ if not point_count :
286+ return False
287+
288+ if point_count == 2 :
289+ if gsr == constants .CARTESIAN :
290+ return True
291+
292+ else :
293+ return False
233294
234295 if gsr == constants .GEODETIC :
235296 return True
@@ -239,10 +300,10 @@ def valid_spatial_config(gsr: str, point_count: int) -> str:
239300
240301def parse_spo (gsr : str , points : list ) -> list :
241302 """
242- Read points from a .spo file, reverse the order of the points to comply with
243- the Cumulus requirement for a clockwise order to polygon points, and ensure
244- the polygon is closed. Raise an exception if either the granule spatial representation
245- or the number of points don't support a gpolygon.
303+ Reverse the order of the points to comply with the Cumulus requirement for a
304+ clockwise order to polygon points, and ensure the polygon is closed. Raise an
305+ exception if either the granule spatial representation or the number of
306+ points don't support a gpolygon.
246307 """
247308 if gsr == constants .CARTESIAN :
248309 raise Exception (
0 commit comments