From 7e33d0410e5242f78f75a92928ac28a38593787c Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 24 Oct 2024 11:08:15 +0200 Subject: [PATCH] Rewrote the auto-pan&zoom heuristics --- .../re_space_view_map/src/map_space_view.rs | 132 ++++++++++-------- .../src/visualizers/geo_points.rs | 13 +- .../re_space_view_map/src/visualizers/mod.rs | 118 ++++++++++++++++ 3 files changed, 201 insertions(+), 62 deletions(-) diff --git a/crates/viewer/re_space_view_map/src/map_space_view.rs b/crates/viewer/re_space_view_map/src/map_space_view.rs index 0fa0ce0c1af3..234f7ea27396 100644 --- a/crates/viewer/re_space_view_map/src/map_space_view.rs +++ b/crates/viewer/re_space_view_map/src/map_space_view.rs @@ -1,4 +1,4 @@ -use egui::{self, Context}; +use egui::{Context, NumExt as _}; use walkers::{HttpTiles, Map, MapMemory, Tiles}; use re_log_types::EntityPath; @@ -15,8 +15,8 @@ use re_ui::{ModifiersMarkdown, MouseButtonMarkdown}; use re_viewer_context::{ SpaceViewClass, SpaceViewClassLayoutPriority, SpaceViewClassRegistryError, SpaceViewId, SpaceViewSpawnHeuristics, SpaceViewState, SpaceViewStateExt as _, - SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, SystemExecutionOutput, - TypedComponentFallbackProvider, ViewQuery, ViewerContext, + SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, SystemExecutionOutput, ViewQuery, + ViewerContext, }; use re_viewport_blueprint::ViewProperty; @@ -144,17 +144,6 @@ Displays a Position3D on a map. ); }); - // "Center and follow" button to reset view following mode after interacting - // with the map. - let map_state = state.downcast_mut::()?; - ui.horizontal(|ui| { - let is_detached = map_state.map_memory.detached().is_none(); - - if !is_detached && ui.button("Center and follow positions").clicked() { - map_state.map_memory.follow_my_position(); - } - }); - Ok(()) } @@ -163,68 +152,106 @@ Displays a Position3D on a map. ctx: &ViewerContext<'_>, ui: &mut egui::Ui, state: &mut dyn SpaceViewState, - query: &ViewQuery<'_>, system_output: SystemExecutionOutput, ) -> Result<(), SpaceViewSystemExecutionError> { let state = state.downcast_mut::()?; + let map_options = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + query.space_view_id, + ); - let blueprint_db = ctx.blueprint_db(); - let view_id = query.space_view_id; - let map_options = - ViewProperty::from_archetype::(blueprint_db, ctx.blueprint_query, view_id); - let map_provider = map_options.component_or_fallback::(ctx, self, state)?; - let zoom_level = map_options - .component_or_fallback::(ctx, self, state)? - .0; + let geo_points_visualizer = system_output.view_systems.get::()?; - if state.map_memory.set_zoom(*zoom_level).is_err() { - re_log::warn!( - "Failed to set zoom level for map. Zoom level should be between zero and 22" - ); - }; + // + // Map Provider + // - // if state changed let's update it from the blueprint + let map_provider = map_options.component_or_fallback::(ctx, self, state)?; if state.selected_provider != map_provider { state.tiles = None; state.selected_provider = map_provider; } + // + // Pan/Zoom handling + // + + // Rationale: + // - `walkers` has an auto vs. manual pan state, switching to the latter upon + // user interaction. We let it keep track of that state. + // - The tracked location is the center of the lat/lon span of the geo objects. + // - When unset in the blueprint, the zoom level is computed from the geo objects and + // saved as is. + // - Zoom computation: if multiple objects, fit them on screen, otherwise use 16.0. + // + // TODO(ab): show in UI and save in blueprint the auto vs. manual pan state (may require + // changes in walkers + // TODO(#7884): support more elaborate auto-pan/zoom modes. + + let span = geo_points_visualizer.span(); + + let default_center_position = span + .as_ref() + .map(|span| span.center()) + .unwrap_or(walkers::Position::from_lat_lon(59.319224, 18.075514)); // Rerun HQ + + let blueprint_zoom_level = map_options + .component_or_empty::()? + .map(|zoom| **zoom); + let default_zoom_level = span.and_then(|span| { + span.zoom_for_screen_size( + (ui.available_size() - egui::vec2(15.0, 15.0)).at_least(egui::Vec2::ZERO), + ) + }); + let zoom_level = blueprint_zoom_level.or(default_zoom_level).unwrap_or(16.0); + + if state.map_memory.set_zoom(zoom_level).is_err() { + re_log::warn_once!( + "Failed to set zoom level for map. Zoom level should be between zero and 22" + ); + }; + + // + // Map UI + // + let (tiles, map_memory) = match state.ensure_and_get_mut_refs(ui.ctx()) { Ok(refs) => refs, Err(err) => return Err(err), }; - - let geo_points_visualizer = system_output.view_systems.get::()?; - egui::Frame::default().show(ui, |ui| { let some_tiles_manager: Option<&mut dyn Tiles> = Some(tiles); let map_widget = ui.add( - Map::new( - some_tiles_manager, - map_memory, - geo_points_visualizer.default_position(), - ) - .with_plugin(geo_points_visualizer.plugin()), + Map::new(some_tiles_manager, map_memory, default_center_position) + .with_plugin(geo_points_visualizer.plugin()), ); - map_widget.double_clicked().then(|| { + if map_widget.double_clicked() { map_memory.follow_my_position(); - }); + if let Some(zoom_level) = default_zoom_level { + let _ = map_memory.set_zoom(zoom_level); + } + } let map_pos = map_widget.rect; let window_id = query.space_view_id.uuid().to_string(); map_windows::zoom(ui, &window_id, &map_pos, map_memory); map_windows::acknowledge(ui, &window_id, &map_pos, tiles.attribution()); - - // update blueprint if zoom level changed from ui - if map_memory.zoom() != *zoom_level { - map_options.save_blueprint_component( - ctx, - &ZoomLevel(re_types::datatypes::Float32(map_memory.zoom())), - ); - } }); + + // + // Save Blueprint + // + + if Some(map_memory.zoom()) != blueprint_zoom_level { + map_options.save_blueprint_component( + ctx, + &ZoomLevel(re_types::datatypes::Float32(map_memory.zoom())), + ); + } + Ok(()) } } @@ -263,11 +290,4 @@ fn get_tile_manager(provider: MapProvider, egui_ctx: &Context) -> HttpTiles { } } -impl TypedComponentFallbackProvider for MapSpaceView { - fn fallback_for(&self, _ctx: &re_viewer_context::QueryContext<'_>) -> ZoomLevel { - // default zoom level is 16. - 16.0.into() - } -} - -re_viewer_context::impl_component_fallback_provider!(MapSpaceView => [ZoomLevel]); +re_viewer_context::impl_component_fallback_provider!(MapSpaceView => []); diff --git a/crates/viewer/re_space_view_map/src/visualizers/geo_points.rs b/crates/viewer/re_space_view_map/src/visualizers/geo_points.rs index 89e675d6b00b..e4be9bdd9a49 100644 --- a/crates/viewer/re_space_view_map/src/visualizers/geo_points.rs +++ b/crates/viewer/re_space_view_map/src/visualizers/geo_points.rs @@ -119,12 +119,13 @@ impl GeoPointsVisualizer { } } - pub fn default_position(&self) -> walkers::Position { - //TODO: need a better heuristics - self.map_entries - .first() - .map(|entry| entry.position) - .unwrap_or(walkers::Position::from_lat_lon(59.319224, 18.075514)) + /// Compute the [`GeoSpan`] of all the points in the visualizer. + pub fn span(&self) -> Option { + super::GeoSpan::from_lat_long( + self.map_entries + .iter() + .map(|entry| (entry.position.lat(), entry.position.lon())), + ) } } diff --git a/crates/viewer/re_space_view_map/src/visualizers/mod.rs b/crates/viewer/re_space_view_map/src/visualizers/mod.rs index 039576cd7157..a4484c1a61a7 100644 --- a/crates/viewer/re_space_view_map/src/visualizers/mod.rs +++ b/crates/viewer/re_space_view_map/src/visualizers/mod.rs @@ -1 +1,119 @@ pub mod geo_points; + +/// Helper to track an area span in latitude and longitude. +#[derive(Debug, Clone)] +pub struct GeoSpan { + pub min_latitude: f64, + pub max_latitude: f64, + pub min_longitude: f64, + pub max_longitude: f64, +} + +impl GeoSpan { + pub fn from_lat_long(mut lat_lon: impl Iterator) -> Option { + if let Some((lat, lon)) = lat_lon.next() { + let mut span = Self { + min_latitude: lat, + max_latitude: lat, + min_longitude: lon, + max_longitude: lon, + }; + + for (lat, lon) in lat_lon { + span.min_latitude = span.min_latitude.min(lat); + span.max_latitude = span.max_latitude.max(lat); + span.min_longitude = span.min_longitude.min(lon); + span.max_longitude = span.max_longitude.max(lon); + } + + Some(span) + } else { + None + } + } + + pub fn center(&self) -> walkers::Position { + walkers::Position::from_lat_lon( + (self.min_latitude + self.max_latitude) / 2.0, + (self.min_longitude + self.max_longitude) / 2.0, + ) + } + + pub fn zoom_for_screen_size(&self, screen_size: egui::Vec2) -> Option { + // Thanks, Claude: https://claude.site/artifacts/cb4f7f53-07a6-4ad0-bce3-eee3cb7e3177 + + if self.min_latitude == self.max_latitude || self.min_longitude == self.max_longitude { + return None; + } + + const TILE_SIZE: f64 = 256.0; + + // Convert latitude to y coordinate in mercator projection (scaled to 0..1) + fn lat_to_y(lat: f64) -> f64 { + let lat_rad = lat.to_radians(); + let y = (1.0 + lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0; + y.clamp(0.0, 1.0) + } + + // Calculate ranges + let lat_range = lat_to_y(self.max_latitude) - lat_to_y(self.min_latitude); + let lon_range = (self.max_longitude - self.min_longitude) / 360.0; + + // Calculate the required number of tiles for both dimensions + let tiles_x = lon_range * TILE_SIZE; + let tiles_y = lat_range * TILE_SIZE; + + // Calculate zoom levels needed for both dimensions + let zoom_x = (screen_size.x as f64 / tiles_x).ln() / 2.0_f64.ln(); + let zoom_y = (screen_size.y as f64 / tiles_y).ln() / 2.0_f64.ln(); + + // Use the minimum zoom level to ensure the entire range fits + Some(zoom_x.min(zoom_y) as f32) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_zoom_for_screen_size() { + // smaller area + let span = GeoSpan { + min_latitude: 46.0, + max_latitude: 47.0, + min_longitude: 6.0, + max_longitude: 7.0, + }; + + let zoom = span + .zoom_for_screen_size(egui::Vec2::new(1024.0, 500.0)) + .unwrap(); + assert!(zoom >= 8.0 && zoom <= 9.0); + + // whole world + let span = GeoSpan { + min_latitude: -85.0, + max_latitude: 85.0, + min_longitude: -180.0, + max_longitude: 180.0, + }; + + let zoom = span + .zoom_for_screen_size(egui::Vec2::new(1024.0, 512.0)) + .unwrap(); + assert!(zoom >= 0.0 && zoom <= 2.0); // Should be very zoomed out + + // single point + let span = GeoSpan { + min_latitude: 46.0, + max_latitude: 46.0, + min_longitude: 6.0, + max_longitude: 6.0, + }; + assert_eq!( + None, + span.zoom_for_screen_size(egui::Vec2::new(1024.0, 512.0)) + ); + } +}