Skip to content

Commit b31795c

Browse files
authored
Merge pull request #848 from OpenTrafficCam/bug/10404-no-orthophoto-video-to-laod-when-loading-fusion-track-files
bug/10404-no-orthophoto-video-to-laod-when-loading-fusion-track-files
2 parents 8fe3b8c + d844b5c commit b31795c

75 files changed

Lines changed: 3410 additions & 83 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.pre-commit-config.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ repos:
3939
- id: mypy
4040
entry: mypy OTAnalytics tests --config-file=pyproject.toml
4141
additional_dependencies:
42+
- affine==3.0.1
4243
- av==13.0.0
4344
- black==25.1.0
4445
- customtkinter==5.2.2
@@ -66,6 +67,7 @@ repos:
6667
- pytest-playwright==0.7.1
6768
- pytest-timeout==2.4.0
6869
- pytest==8.3.4
70+
- rasterio==1.5.1
6971
- starlette==0.49.1
7072
- testcontainers==4.13.1
7173
- twine==6.2.0

‎CONTEXT.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,18 @@ one opens a file chooser, the S3 one asks for a Load Window and downloads. This
5454
is the seam that makes the Transfer Mode interchangeable.
5555
_Avoid_: file loader, importer (both suggest they also parse, which they do not —
5656
they only obtain paths).
57+
58+
### Placing Tracks in the world
59+
60+
**Geo-only Track File**:
61+
A Track file whose positions are given only in geo coordinates, with no image
62+
coordinates and no Video. OTFusion produces these, because it fuses several
63+
cameras into one world view rather than into any single camera's image.
64+
_Avoid_: fusion video, bird's-eye-view file (there is no video or rendered image).
65+
66+
**Orthophoto**:
67+
A georeferenced aerial image of a site, shown as the background for Tracks that
68+
have no Video. Its own georeference is what relates a point on the image to a
69+
point in the world.
70+
_Avoid_: map, GeoTIFF (a file format, not the concept), bird's-eye view (the image
71+
OTFusion used to render, which no longer exists).

‎OTAnalytics/adapter_ui/dummy_viewmodel.py‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
WeatherType,
9595
)
9696
from OTAnalytics.application.project_location import UnsupportedProjectLocation
97+
from OTAnalytics.application.state import CurrentOrthophoto
9798
from OTAnalytics.application.use_cases.config import ConfigValidationError
9899
from OTAnalytics.application.use_cases.config_has_changed import NoExistingConfigFound
99100
from OTAnalytics.application.use_cases.cut_tracks_with_sections import CutTracksDto
@@ -137,6 +138,7 @@
137138
from OTAnalytics.domain.files import DifferentDrivesException
138139
from OTAnalytics.domain.filter import FilterElement
139140
from OTAnalytics.domain.flow import Flow, FlowId, FlowListObserver
141+
from OTAnalytics.domain.orthophoto import Orthophoto, OrthophotoError
140142
from OTAnalytics.domain.section import (
141143
COORDINATES,
142144
ID,
@@ -148,6 +150,9 @@
148150
SectionRepositoryEvent,
149151
)
150152
from OTAnalytics.domain.track import TrackImage
153+
from OTAnalytics.domain.track_dataset.track_dataset import (
154+
IncompatibleGeoreferenceMetadataError,
155+
)
151156
from OTAnalytics.domain.track_repository import TrackListObserver, TrackRepositoryEvent
152157
from OTAnalytics.domain.types import EventType
153158
from OTAnalytics.domain.video import Video, VideoListObserver
@@ -340,8 +345,10 @@ def __init__(
340345
update_section_coordinates: UpdateSectionCoordinates,
341346
provide_track_files: ProvideTrackFiles,
342347
provide_video_files: ProvideVideoFiles,
348+
current_orthophoto: CurrentOrthophoto,
343349
) -> None:
344350
self._application = application
351+
self._current_orthophoto = current_orthophoto
345352
self._ui_factory = ui_factory
346353
self._provide_track_files = provide_track_files
347354
self._provide_video_files = provide_video_files
@@ -382,6 +389,10 @@ def notify_videos(self, videos: list[Video]) -> None:
382389
self.treeview_videos.update_items()
383390
self._update_enabled_buttons()
384391

392+
def notify_orthophoto(self, _: Orthophoto | None) -> None:
393+
"""Re-enable the buttons that wait for a background to draw on."""
394+
self._update_enabled_buttons()
395+
385396
def notify_files(self) -> None:
386397
self.treeview_files.update_items()
387398
self._update_enabled_buttons()
@@ -432,12 +443,11 @@ def _update_enabled_video_buttons(self) -> None:
432443

433444
def _update_enabled_section_buttons(self) -> None:
434445
action_running = self._application.action_state.action_running.get()
435-
videos_exist = len(self._application.get_all_videos()) > 0
436446
selected_section_ids = self.get_selected_section_ids()
437447
single_section_selected = len(selected_section_ids) == 1
438448
any_section_selected = len(selected_section_ids) > 0
439449

440-
add_section_enabled = (not action_running) and videos_exist
450+
add_section_enabled = (not action_running) and self._background_exists()
441451
single_section_enabled = add_section_enabled and single_section_selected
442452
multiple_sections_enabled = add_section_enabled and any_section_selected
443453

@@ -469,10 +479,14 @@ def _update_enabled_flow_buttons(self) -> None:
469479

470480
def _update_enabled_video_control_buttons(self) -> None:
471481
action_running = self._application.action_state.action_running.get()
472-
videos_exist = len(self._application.get_all_videos()) > 0
473-
general_activated = not action_running and videos_exist
482+
general_activated = not action_running and self._background_exists()
474483
self.frame_video_control.set_enabled_general_buttons(general_activated)
475484

485+
def _background_exists(self) -> bool:
486+
"""Whether a Video or an Orthophoto is there to draw Sections on."""
487+
videos_exist = len(self._application.get_all_videos()) > 0
488+
return videos_exist or self._current_orthophoto.get() is not None
489+
476490
def _on_section_changed(self, section: SectionId) -> None:
477491
self._refresh_sections_in_ui()
478492

@@ -586,7 +600,13 @@ async def add_video(self) -> None:
586600
if not video_files:
587601
return
588602
logger().info(f"Video files to load: {video_files}")
589-
self._application.add_videos(files=video_files)
603+
try:
604+
self._application.add_videos(files=video_files)
605+
except OrthophotoError as cause:
606+
logger().warning(str(cause))
607+
self._ui_factory.info_box(
608+
message=str(cause), initial_position=self._get_window_position()
609+
)
590610

591611
def remove_videos(self) -> None:
592612
self._application.remove_videos()
@@ -900,7 +920,13 @@ async def load_tracks(self) -> None:
900920
if not track_files:
901921
return
902922
logger().info(f"Tracks files to load: {track_files}")
903-
await self._application.add_tracks_of_files_async(track_files=track_files)
923+
try:
924+
await self._application.add_tracks_of_files_async(track_files=track_files)
925+
except (OrthophotoError, IncompatibleGeoreferenceMetadataError) as cause:
926+
logger().warning(str(cause))
927+
self._ui_factory.info_box(
928+
message=str(cause), initial_position=self._get_window_position()
929+
)
904930

905931
async def load_configuration(self) -> None: # sourcery skip: avoid-builtin-shadow
906932
# INFO: Current behavior: Overwrites existing sections

‎OTAnalytics/adapter_ui/local_file_providers.py‎

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44

55
from OTAnalytics.adapter_ui.ui_factory import UiFactory
66
from OTAnalytics.application.config import SUPPORTED_VIDEO_FILE_TYPES
7+
from OTAnalytics.application.orthophoto import ProvideOrthophoto
78
from OTAnalytics.application.use_cases.provide_input_files import (
89
ProvideTrackFiles,
910
ProvideVideoFiles,
1011
)
1112

1213
ALL_FILE_ENDINGS = "All File Endings"
14+
ORTHOPHOTO_EXTENSIONS = [".tif", ".tiff"]
15+
ORTHOPHOTO_FILE_TYPES = [
16+
(f"orthophoto ({extension})", f"*{extension}")
17+
for extension in ORTHOPHOTO_EXTENSIONS
18+
]
19+
ORTHOPHOTO_DEFAULT_EXTENSION = ORTHOPHOTO_EXTENSIONS[0]
20+
NO_FILE_CHOSEN = ""
1321

1422

1523
class LocalTrackFileProvider(ProvideTrackFiles):
@@ -43,15 +51,39 @@ async def provide(self) -> list[Path]:
4351
return await self._ui_factory.askopenfilenames(
4452
title="Load video files",
4553
filetypes=[("video file", SUPPORTED_VIDEO_FILE_TYPES)],
46-
extension_options=_video_extension_options(),
54+
extension_options=_extension_options(SUPPORTED_VIDEO_FILE_TYPES),
4755
)
4856

4957

50-
def _video_extension_options() -> dict[str, list[str] | None]:
51-
"""One entry offering every supported type, then one per type."""
52-
options: dict[str, list[str] | None] = {
53-
ALL_FILE_ENDINGS: list(SUPPORTED_VIDEO_FILE_TYPES)
54-
}
55-
for extension in SUPPORTED_VIDEO_FILE_TYPES:
58+
class LocalOrthophotoProvider(ProvideOrthophoto):
59+
"""Asks the user for the Orthophoto of Geo-only Track Files being loaded.
60+
61+
Args:
62+
ui_factory (UiFactory): builds the file chooser for the active front-end.
63+
"""
64+
65+
def __init__(self, ui_factory: UiFactory) -> None:
66+
self._ui_factory = ui_factory
67+
68+
async def provide(self) -> Path | None:
69+
chosen = await self._ui_factory.askopenfilename(
70+
title="Choose the orthophoto for these track files",
71+
filetypes=ORTHOPHOTO_FILE_TYPES,
72+
defaultextension=ORTHOPHOTO_DEFAULT_EXTENSION,
73+
extension_options=_extension_options(ORTHOPHOTO_EXTENSIONS),
74+
)
75+
if chosen == NO_FILE_CHOSEN:
76+
return None
77+
return Path(chosen)
78+
79+
80+
def _extension_options(extensions: list[str]) -> dict[str, list[str] | None]:
81+
"""One entry offering every given extension, then one per extension.
82+
83+
Without these options the file picker falls back to its otflow/otconfig filter
84+
and hides every other file.
85+
"""
86+
options: dict[str, list[str] | None] = {ALL_FILE_ENDINGS: list(extensions)}
87+
for extension in extensions:
5688
options[extension] = [extension]
5789
return options

‎OTAnalytics/adapter_ui/ui_factory.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ async def askopenfilename(
3636
title: str,
3737
filetypes: list[tuple[str, str]],
3838
defaultextension: str,
39+
extension_options: dict[str, list[str] | None] | None = None,
3940
) -> str:
4041
raise NotImplementedError
4142

‎OTAnalytics/application/application.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
)
1010
from OTAnalytics.application.datastore import Datastore
1111
from OTAnalytics.application.key_prefix import S3KeyPrefix
12+
from OTAnalytics.application.orthophoto import AddVideoFiles
1213
from OTAnalytics.application.project import SvzMetadata
1314
from OTAnalytics.application.state import (
1415
ActionState,
@@ -160,6 +161,7 @@ def __init__(
160161
get_current_remark: GetCurrentRemark,
161162
update_count_plots: CountPlotsUpdater,
162163
current_key_prefix: CurrentKeyPrefix,
164+
add_video_files: AddVideoFiles,
163165
) -> None:
164166
self._datastore: Datastore = datastore
165167
self.track_state: TrackState = track_state
@@ -208,6 +210,7 @@ def __init__(
208210
self._get_current_remark = get_current_remark
209211
self._update_count_plots = update_count_plots
210212
self._current_key_prefix = current_key_prefix
213+
self._add_video_files = add_video_files
211214

212215
def connect_observers(self) -> None:
213216
"""
@@ -245,7 +248,15 @@ def get_section_for(self, section_id: SectionId) -> Optional[Section]:
245248
return self._datastore.get_section_for(section_id)
246249

247250
def add_videos(self, files: list[Path]) -> None:
248-
self._datastore.load_video_files(files)
251+
"""Load video files into the project.
252+
253+
Args:
254+
files (list[Path]): the video files.
255+
256+
Raises:
257+
MixedTrackFiles: if the project shows an Orthophoto.
258+
"""
259+
self._add_video_files.add(files)
249260

250261
def remove_videos(self) -> None:
251262
"""

0 commit comments

Comments
 (0)