Skip to content

Commit ffcb6c4

Browse files
authored
Merge pull request #293 from nsidc/issue-292
Issue-292
2 parents 45fc6d6 + cbf0dd4 commit ffcb6c4

File tree

7 files changed

+113
-98
lines changed

7 files changed

+113
-98
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## UNRELEASED
2+
3+
* Issue 292: Exclude `.spo` files from polygon-generation logic, regardless of
4+
the value of the `spatial_polygon_enabled` flag in the `ini` file.
5+
16
## v1.12.0 (2025-09-15)
27

38
* Minor version release encompassing all features in v1.12.0rc0 - v1.12.0rc3.

src/nsidc/metgen/metgen.py

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
from nsidc.metgen.collection_metadata import get_collection_metadata
4242
from nsidc.metgen.models import CollectionMetadata
4343
from nsidc.metgen.readers import registry, utilities
44-
from nsidc.metgen.spatial import create_flightline_polygon
4544

4645
# -------------------------------------------------------------------
4746
CONSOLE_FORMAT = "%(message)s"
@@ -742,9 +741,7 @@ def create_ummg(configuration: config.Config, granule: Granule) -> Granule:
742741
)
743742

744743
# Get spatial coverage from spatial file if it exists
745-
spatial_content = utilities.external_spatial_values(
746-
configuration.collection_geometry_override, gsr, granule
747-
)
744+
spatial_content = utilities.external_spatial_values(configuration, gsr, granule)
748745

749746
# Populated summary dict looks like:
750747
# {
@@ -767,9 +764,7 @@ def create_ummg(configuration: config.Config, granule: Granule) -> Granule:
767764
gsr,
768765
)
769766

770-
summary["spatial_extent"] = populate_spatial(
771-
gsr, summary["geometry"], configuration, spatial_content
772-
)
767+
summary["spatial_extent"] = populate_spatial(gsr, summary["geometry"])
773768
summary["temporal_extent"] = populate_temporal(summary["temporal"])
774769
summary["additional_attributes"] = populate_additional_attributes(
775770
premet_content, constants.UMMG_ADDITIONAL_ATTRIBUTES
@@ -983,60 +978,10 @@ def checksum(file):
983978
def populate_spatial(
984979
spatial_representation: str,
985980
spatial_values: list,
986-
configuration: config.Config = None,
987-
spatial_content: list = None,
988981
) -> str:
989982
"""
990983
Return a string representation of a geometry (point, bounding box, gpolygon)
991-
Optionally generates optimized polygons when spatial files are present.
992-
"""
993-
# Check if we should generate polygons for spatial file data
994-
if (
995-
configuration is not None
996-
and configuration.spatial_polygon_enabled
997-
and spatial_content is not None
998-
and spatial_representation == constants.GEODETIC
999-
and len(spatial_values) >= 3
1000-
):
1001-
try:
1002-
# Create configured polygon generator using partial application
1003-
generate_polygon = partial(
1004-
create_flightline_polygon,
1005-
target_coverage=configuration.spatial_polygon_target_coverage,
1006-
max_vertices=configuration.spatial_polygon_max_vertices,
1007-
cartesian_tolerance=configuration.spatial_polygon_cartesian_tolerance,
1008-
)
1009-
1010-
# Extract lon/lat arrays from spatial_values
1011-
lons = [point["Longitude"] for point in spatial_values]
1012-
lats = [point["Latitude"] for point in spatial_values]
1013-
1014-
# Generate polygon using our configured spatial module
1015-
polygon, metadata = generate_polygon(lons, lats)
1016-
1017-
if polygon is not None:
1018-
# Convert shapely polygon to UMM-G format
1019-
coords = list(polygon.exterior.coords)
1020-
polygon_points = [
1021-
{"Longitude": float(lon), "Latitude": float(lat)}
1022-
for lon, lat in coords
1023-
]
1024-
1025-
# Use the existing template system
1026-
return ummg_spatial_gpolygon_template().safe_substitute(
1027-
{"points": json.dumps(polygon_points)}
1028-
)
1029-
1030-
except Exception as e:
1031-
# If polygon generation fails, fall back to original behavior
1032-
import logging
1033-
1034-
logger = logging.getLogger(constants.ROOT_LOGGER)
1035-
logger.warning(
1036-
f"Polygon generation failed, using original spatial processing: {e}"
1037-
)
1038-
1039-
# Original behavior for all other cases
984+
"""
1040985
match spatial_representation:
1041986
case constants.CARTESIAN:
1042987
return populate_bounding_rectangle(spatial_values)

src/nsidc/metgen/readers/utilities.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from datetime import timezone
33

44
from 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

1011
def 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+
230284
def 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

240301
def 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(

tests/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Shared test fixtures for the test suite."""
22

3+
from configparser import ConfigParser, ExtendedInterpolation
4+
35
import pytest
46

57
from nsidc.metgen.models import CollectionMetadata
@@ -15,3 +17,23 @@ def simple_collection_metadata():
1517
return CollectionMetadata(
1618
short_name="ABCD", version="2", entry_title="Test Collection ABCD V002"
1719
)
20+
21+
22+
@pytest.fixture
23+
def cfg_parser():
24+
cp = ConfigParser(interpolation=ExtendedInterpolation())
25+
cp["Source"] = {"data_dir": "/data/example"}
26+
cp["Collection"] = {"auth_id": "DATA-0001", "version": 42, "provider": "FOO"}
27+
cp["Destination"] = {
28+
"local_output_dir": "/output/here",
29+
"ummg_dir": "ummg",
30+
"kinesis_stream_name": "xyzzy-${environment}-stream",
31+
"staging_bucket_name": "xyzzy-${environment}-bucket",
32+
"write_cnm_file": False,
33+
}
34+
cp["Settings"] = {
35+
"checksum_type": "SHA256",
36+
"number": 1,
37+
"log_dir": "/tmp",
38+
}
39+
return cp

tests/integration/configs/OLVIS1A_DUCk.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ spatial_dir = ./data/spatial
77
auth_id = OLVIS1A_DUCk
88
version = 1
99
provider = NSIDC_CUAT
10-
granule_regex = (?P<granuleid>.*_DUCK)\.JPG
10+
granule_regex = (?P<granuleid>.*_DUCk)\.JPG
1111
browse_regex = _reduced.JPG
1212

1313
[Destination]

tests/test_config.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dataclasses
22
import re
3-
from configparser import ConfigParser, ExtendedInterpolation
3+
from configparser import ConfigParser
44
from unittest.mock import patch
55

66
import pytest
@@ -55,26 +55,6 @@ def expected_keys():
5555
)
5656

5757

58-
@pytest.fixture
59-
def cfg_parser():
60-
cp = ConfigParser(interpolation=ExtendedInterpolation())
61-
cp["Source"] = {"data_dir": "/data/example"}
62-
cp["Collection"] = {"auth_id": "DATA-0001", "version": 42, "provider": "FOO"}
63-
cp["Destination"] = {
64-
"local_output_dir": "/output/here",
65-
"ummg_dir": "ummg",
66-
"kinesis_stream_name": "xyzzy-${environment}-stream",
67-
"staging_bucket_name": "xyzzy-${environment}-bucket",
68-
"write_cnm_file": False,
69-
}
70-
cp["Settings"] = {
71-
"checksum_type": "SHA256",
72-
"number": 1,
73-
"log_dir": "/tmp",
74-
}
75-
return cp
76-
77-
7858
def test_config_parser_without_filename():
7959
with pytest.raises(ValueError):
8060
config.config_parser_factory(None)

tests/test_utilities.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from nsidc.metgen import constants, metgen
6+
from nsidc.metgen import config, constants, metgen
77
from nsidc.metgen.readers import utilities
88

99
# Unit tests for the 'utilities' module functions.
@@ -294,12 +294,14 @@ def test_spo_is_geodetic():
294294

295295
@patch("nsidc.metgen.readers.utilities.points_from_collection")
296296
def test_uses_spatial_from_collection(
297-
collection_handler_mock, collection_spatial, simple_collection_metadata
297+
collection_handler_mock, collection_spatial, simple_collection_metadata, cfg_parser
298298
):
299299
simple_collection_metadata.spatial_extent = collection_spatial
300300
fake_granule = metgen.Granule("fake_granule", collection=simple_collection_metadata)
301+
cfg_parser["Source"] = {"collection_geometry_override": True}
302+
cfg = config.configuration(cfg_parser, {}, constants.DEFAULT_CUMULUS_ENVIRONMENT)
301303

302-
utilities.external_spatial_values(True, constants.CARTESIAN, fake_granule)
304+
utilities.external_spatial_values(cfg, constants.CARTESIAN, fake_granule)
303305
assert collection_handler_mock.called
304306
assert collection_handler_mock.call_args.args[0] == collection_spatial
305307

0 commit comments

Comments
 (0)