Skip to content

Commit

Permalink
Rewrote the auto-pan&zoom heuristics
Browse files Browse the repository at this point in the history
  • Loading branch information
abey79 committed Oct 24, 2024
1 parent 698af7b commit 7e33d04
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 62 deletions.
132 changes: 76 additions & 56 deletions crates/viewer/re_space_view_map/src/map_space_view.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use egui::{self, Context};
use egui::{Context, NumExt as _};
use walkers::{HttpTiles, Map, MapMemory, Tiles};

use re_log_types::EntityPath;
Expand All @@ -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;

Expand Down Expand Up @@ -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::<MapSpaceViewState>()?;
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(())
}

Expand All @@ -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::<MapSpaceViewState>()?;
let map_options = ViewProperty::from_archetype::<MapOptions>(
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::<MapOptions>(blueprint_db, ctx.blueprint_query, view_id);
let map_provider = map_options.component_or_fallback::<MapProvider>(ctx, self, state)?;
let zoom_level = map_options
.component_or_fallback::<ZoomLevel>(ctx, self, state)?
.0;
let geo_points_visualizer = system_output.view_systems.get::<GeoPointsVisualizer>()?;

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::<MapProvider>(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::<ZoomLevel>()?
.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::<GeoPointsVisualizer>()?;

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(())
}
}
Expand Down Expand Up @@ -263,11 +290,4 @@ fn get_tile_manager(provider: MapProvider, egui_ctx: &Context) -> HttpTiles {
}
}

impl TypedComponentFallbackProvider<ZoomLevel> 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 => []);
13 changes: 7 additions & 6 deletions crates/viewer/re_space_view_map/src/visualizers/geo_points.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
super::GeoSpan::from_lat_long(
self.map_entries
.iter()
.map(|entry| (entry.position.lat(), entry.position.lon())),
)
}
}

Expand Down
118 changes: 118 additions & 0 deletions crates/viewer/re_space_view_map/src/visualizers/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Item = (f64, f64)>) -> Option<Self> {
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<f32> {
// 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))
);
}
}

0 comments on commit 7e33d04

Please sign in to comment.