Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 28 additions & 27 deletions murgtools/getdata/getOutsideData.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,33 +397,34 @@ def getSatelliteImagery(corners, filename=None, collection='sentinel-2-l2a',

# Extract actual image extent from GeoTIFF tags and convert to lat/lon
# STAC bbox is not reliable for pixel coordinate calculation
with tifffile.TiffFile(tmp_path) as tif:
tags = tif.pages[0].tags
tiepoint = tags[33922].value # (i, j, k, x, y, z) - UTM coordinates
scale = tags[33550].value # (scaleX, scaleY, scaleZ)
img_h, img_w = tif.pages[0].shape[:2]

# UTM bounds
utm_left = tiepoint[3]
utm_right = tiepoint[3] + img_w * scale[0]
utm_top = tiepoint[4]
utm_bottom = tiepoint[4] - img_h * scale[1]

# Determine UTM zone from GeoKey (tag 34737 contains projection info)
proj_str = tags.get(34737, None)
if proj_str:
proj_str = proj_str.value
# Parse UTM zone from string like "WGS 84 / UTM zone 18N"
match = re.search(r'UTM zone (\d+)([NS])', str(proj_str))
if match:
zone = int(match.group(1))
hemisphere = match.group(2)
epsg = 32600 + zone if hemisphere == 'N' else 32700 + zone
else:
epsg = 32618

# Convert UTM corners to lat/lon
transformer = Transformer.from_crs(f'EPSG:{epsg}', 'EPSG:4326', always_xy=True)
try:
with tifffile.TiffFile(tmp_path) as tif:
tags = tif.pages[0].tags
tiepoint = tags[33922].value # (i, j, k, x, y, z) - UTM coordinates
scale = tags[33550].value # (scaleX, scaleY, scaleZ)
img_h, img_w = tif.pages[0].shape[:2]

# UTM bounds
utm_left = tiepoint[3]
utm_right = tiepoint[3] + img_w * scale[0]
utm_top = tiepoint[4]
utm_bottom = tiepoint[4] - img_h * scale[1]

# Determine UTM zone from GeoKey (tag 34737 contains projection info)
proj_str = tags.get(34737, None)
if proj_str:
proj_str = proj_str.value
# Parse UTM zone from string like "WGS 84 / UTM zone 18N"
match = re.search(r'UTM zone (\d+)([NS])', str(proj_str))
if match:
zone = int(match.group(1))
hemisphere = match.group(2)
epsg = 32600 + zone if hemisphere == 'N' else 32700 + zone
else:
epsg = 32618

# Convert UTM corners to lat/lon
transformer = Transformer.from_crs(f'EPSG:{epsg}', 'EPSG:4326', always_xy=True)

tl_lon, tl_lat = transformer.transform(utm_left, utm_top)
br_lon, br_lat = transformer.transform(utm_right, utm_bottom)
Expand Down
13 changes: 13 additions & 0 deletions murgtools/utils/sblib.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import netCDF4 as nc
import math, warnings, os
from dateutil.relativedelta import relativedelta
from murgtools.exceptions import InvalidParameterError

########################################
# following functions deal with general things
Expand Down Expand Up @@ -505,6 +506,9 @@ def roundDatetimeToInterval(dt, interval_minutes=30, method='nearest'):
Returns:
datetime: Rounded datetime with seconds and microseconds zeroed

Raises:
InvalidParameterError: If method is not one of 'nearest', 'floor', or 'ceil'

Examples:
>>> import datetime as DT
>>> dt = DT.datetime(2024, 6, 15, 14, 23, 45)
Expand All @@ -513,6 +517,15 @@ def roundDatetimeToInterval(dt, interval_minutes=30, method='nearest'):
>>> roundDatetimeToInterval(dt, 30, method='floor')
datetime.datetime(2024, 6, 15, 14, 0, 0)
"""
valid_methods = ['nearest', 'floor', 'ceil']
if method not in valid_methods:
raise InvalidParameterError(
f"Invalid rounding method: '{method}'",
parameter_name='method',
value=method,
expected=f"One of {valid_methods}"
)

total_minutes = dt.hour * 60 + dt.minute

if method == 'floor':
Expand Down
81 changes: 81 additions & 0 deletions tests/test_sblib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Tests for murgtools.utils.sblib utility functions."""

import pytest
import datetime as DT

from murgtools.utils.sblib import roundDatetimeToInterval
from murgtools.exceptions import InvalidParameterError


class TestRoundDatetimeToInterval:
"""Test roundDatetimeToInterval function."""

def test_round_to_nearest_30min(self):
"""Test rounding to nearest 30-minute interval."""
dt = DT.datetime(2024, 6, 15, 14, 23, 45)
result = roundDatetimeToInterval(dt, 30, method='nearest')
expected = DT.datetime(2024, 6, 15, 14, 30, 0)
assert result == expected

def test_round_to_floor_30min(self):
"""Test rounding down (floor) to 30-minute interval."""
dt = DT.datetime(2024, 6, 15, 14, 23, 45)
result = roundDatetimeToInterval(dt, 30, method='floor')
expected = DT.datetime(2024, 6, 15, 14, 0, 0)
assert result == expected

def test_round_to_ceil_30min(self):
"""Test rounding up (ceil) to 30-minute interval."""
dt = DT.datetime(2024, 6, 15, 14, 23, 45)
result = roundDatetimeToInterval(dt, 30, method='ceil')
expected = DT.datetime(2024, 6, 15, 14, 30, 0)
assert result == expected

def test_default_method_is_nearest(self):
"""Test that default method is 'nearest'."""
dt = DT.datetime(2024, 6, 15, 14, 23, 45)
result = roundDatetimeToInterval(dt, 30)
expected = DT.datetime(2024, 6, 15, 14, 30, 0)
assert result == expected

def test_invalid_method_raises_error(self):
"""Test that invalid method raises InvalidParameterError."""
dt = DT.datetime(2024, 6, 15, 14, 23, 45)
with pytest.raises(InvalidParameterError) as exc_info:
roundDatetimeToInterval(dt, 30, method='round')

# Check error message details
assert "Invalid rounding method: 'round'" in str(exc_info.value)
assert exc_info.value.parameter_name == 'method'
assert exc_info.value.value == 'round'
assert "['nearest', 'floor', 'ceil']" in str(exc_info.value.expected)

def test_invalid_method_average_raises_error(self):
"""Test that 'average' method raises InvalidParameterError."""
dt = DT.datetime(2024, 6, 15, 14, 23, 45)
with pytest.raises(InvalidParameterError) as exc_info:
roundDatetimeToInterval(dt, 30, method='average')

assert "Invalid rounding method: 'average'" in str(exc_info.value)

def test_invalid_method_empty_string_raises_error(self):
"""Test that empty string method raises InvalidParameterError."""
dt = DT.datetime(2024, 6, 15, 14, 23, 45)
with pytest.raises(InvalidParameterError) as exc_info:
roundDatetimeToInterval(dt, 30, method='')

assert "Invalid rounding method: ''" in str(exc_info.value)

def test_seconds_and_microseconds_zeroed(self):
"""Test that seconds and microseconds are always zeroed."""
dt = DT.datetime(2024, 6, 15, 14, 30, 45, 123456)
result = roundDatetimeToInterval(dt, 30, method='nearest')
assert result.second == 0
assert result.microsecond == 0

def test_midnight_overflow(self):
"""Test handling of overflow past midnight."""
dt = DT.datetime(2024, 6, 15, 23, 50, 0)
result = roundDatetimeToInterval(dt, 30, method='ceil')
expected = DT.datetime(2024, 6, 16, 0, 0, 0)
assert result == expected
Loading