"""Tests for Image Rotation Manager (F06).""" from datetime import datetime, timezone import numpy as np import pytest from gps_denied.core.rotation import IImageMatcher, ImageRotationManager from gps_denied.schemas import GPSPoint from gps_denied.schemas.rotation import RotationResult from gps_denied.schemas.satellite import TileBounds @pytest.fixture def rotation_manager(): return ImageRotationManager() def test_rotate_image_360(rotation_manager): img = np.zeros((100, 100, 3), dtype=np.uint8) # Just draw a white rectangle to test rotation img[10:40, 10:40] = [255, 255, 255] r90 = rotation_manager.rotate_image_360(img, 90.0) assert r90.shape == (100, 100, 3) # Top left corner should move assert not np.array_equal(img, r90) def test_heading_management(rotation_manager): fid = "flight_1" now = datetime.now(timezone.utc) assert rotation_manager.get_current_heading(fid) is None rotation_manager.update_heading(fid, 1, 370.0, now) # should modulo to 10 assert rotation_manager.get_current_heading(fid) == 10.0 rotation_manager.update_heading(fid, 2, 90.0, now) assert rotation_manager.get_current_heading(fid) == 90.0 def test_detect_sharp_turn(rotation_manager): fid = "flight_2" now = datetime.now(timezone.utc) assert rotation_manager.detect_sharp_turn(fid, 90.0) is False # no history rotation_manager.update_heading(fid, 1, 90.0, now) assert rotation_manager.detect_sharp_turn(fid, 100.0) is False # delta 10 assert rotation_manager.detect_sharp_turn(fid, 180.0) is True # delta 90 assert rotation_manager.detect_sharp_turn(fid, 350.0) is True # delta 100 assert rotation_manager.detect_sharp_turn(fid, 80.0) is False # delta 10 (wraparound) # Wraparound test explicitly rotation_manager.update_heading(fid, 2, 350.0, now) assert rotation_manager.detect_sharp_turn(fid, 10.0) is False # delta 20 class MockMatcher(IImageMatcher): def align_to_satellite( self, uav_image: np.ndarray, satellite_tile: np.ndarray, tile_bounds: TileBounds, ) -> RotationResult: # Mock that only matches when angle was originally 90 # By checking internal state or just returning generic true for test return RotationResult(matched=True, initial_angle=90.0, precise_angle=90.0, confidence=0.99) def test_try_rotation_steps(rotation_manager): fid = "flight_3" img = np.zeros((10, 10, 3), dtype=np.uint8) sat = np.zeros((10, 10, 3), dtype=np.uint8) nw = GPSPoint(lat=10.0, lon=10.0) se = GPSPoint(lat=9.0, lon=11.0) tb = TileBounds(nw=nw, ne=nw, sw=se, se=se, center=nw, gsd=0.5) matcher = MockMatcher() # Should perform sweep and mock matcher says matched=True immediately in the loop res = rotation_manager.try_rotation_steps(fid, 1, img, sat, tb, datetime.now(timezone.utc), matcher) assert res is not None assert res.matched is True # The first step is 0 degrees, the mock matcher returns matched=True. # Therefore the first matched angle is 0. assert res.initial_angle == 0.0