Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inertial movement #31

Merged
merged 19 commits into from
Aug 29, 2023
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.

### Breaking

* New `Center` variant - `Inertia`. It means that the map is moving due to inertia and
soon will stop with `Center` switching to `Exact`. To keep things easy, `Center` now
has new method `detached`, will returns a position the map is currently at, or `None`
if it just follows `my_position`.
* `openstreetmap` is now in `walkers::providers` module.
* `osm` example is now called `myapp` and it shows a small windows with an orthophotomap
layer from <https://geoportal.gov.pl>.
Expand Down
35 changes: 16 additions & 19 deletions examples/myapp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct MyApp {
tiles: Tiles,
geoportal_tiles: Tiles,
map_memory: MapMemory,
satellite: bool,
}

impl MyApp {
Expand All @@ -22,6 +23,7 @@ impl MyApp {
tiles: Tiles::new(walkers::providers::openstreetmap, egui_ctx.to_owned()),
geoportal_tiles: Tiles::new(walkers::providers::geoportal, egui_ctx),
map_memory: MapMemory::default(),
satellite: false,
}
}
}
Expand All @@ -39,8 +41,15 @@ impl eframe::App for MyApp {
// Typically this would be a GPS acquired position which is tracked by the map.
let my_position = places::wroclaw_glowny();

// Select either OSM standard map or satellite.
let tiles = if self.satellite {
&mut self.geoportal_tiles
} else {
&mut self.tiles
};

// In egui, widgets are constructed and consumed in each frame.
let map = Map::new(Some(&mut self.tiles), &mut self.map_memory, my_position);
let map = Map::new(Some(tiles), &mut self.map_memory, my_position);

// Optionally, a function which draw custom stuff on the map can be attached.
let ctx_clone = ctx.clone();
Expand All @@ -57,14 +66,7 @@ impl eframe::App for MyApp {

zoom(ui, &mut self.map_memory);
go_to_my_position(ui, &mut self.map_memory);

orthophotomap(
ui,
&mut self.geoportal_tiles,
&mut self.map_memory,
my_position,
);

satellite(ui, &mut self.satellite);
acknowledge(ui);
}
});
Expand Down Expand Up @@ -122,7 +124,7 @@ fn draw_custom_shapes(ctx: Context, painter: Painter, projector: &Projector) {

mod windows {
use egui::{Align2, RichText, Ui, Window};
use walkers::{Center, Map, MapMemory, Position, Tiles};
use walkers::{Center, MapMemory};

pub fn acknowledge(ui: &Ui) {
Window::new("Acknowledge")
Expand All @@ -139,20 +141,15 @@ mod windows {
});
}

pub fn orthophotomap(
ui: &Ui,
tiles: &mut Tiles,
map_memory: &mut MapMemory,
my_position: Position,
) {
Window::new("Orthophotomap")
pub fn satellite(ui: &Ui, satellite: &mut bool) {
Window::new("Satellite")
.collapsible(false)
.resizable(false)
.title_bar(false)
.anchor(Align2::RIGHT_TOP, [-10., 10.])
.fixed_size([150., 150.])
.show(ui.ctx(), |ui| {
ui.add(Map::new(Some(tiles), map_memory, my_position));
ui.checkbox(satellite, "satellite view");
});
}

Expand All @@ -178,7 +175,7 @@ mod windows {

/// When map is "detached", show a windows with an option to go back to my position.
pub fn go_to_my_position(ui: &Ui, map_memory: &mut MapMemory) {
if let Center::Exact(position) = map_memory.center_mode {
if let Some(position) = map_memory.center_mode.detached() {
Window::new("Center")
.collapsible(false)
.resizable(false)
Expand Down
101 changes: 75 additions & 26 deletions src/map.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::{hash_map::Entry, HashMap};

use egui::{Mesh, Painter, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};
use egui::{Context, Mesh, Painter, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};

use crate::{
mercator::{screen_to_position, PositionExt, TileId},
Expand Down Expand Up @@ -83,22 +83,27 @@ impl Widget for Map<'_, '_> {
fn ui(self, ui: &mut Ui) -> Response {
let (rect, response) = ui.allocate_exact_size(ui.available_size(), Sense::drag());

if response.hovered() {
let zoom_delta = ui.input(|input| input.zoom_delta());
//if response.hovered() {
let zoom_delta = ui.input(|input| input.zoom_delta());

// Zooming and dragging need to be exclusive, otherwise the map will get dragged when
// pinch gesture is used.
if !(0.99..=1.01).contains(&zoom_delta) {
// Shift by 1 because of the values given by zoom_delta(). Multiple by 2, because
// then it felt right with both mouse wheel, and an Android phone.
self.memory.zoom.zoom_by((zoom_delta - 1.) * 2.);
} else {
self.memory
.center_mode
.drag(&response, self.my_position, self.memory.zoom.round());
}
// Zooming and dragging need to be exclusive, otherwise the map will get dragged when
// pinch gesture is used.
if !(0.99..=1.01).contains(&zoom_delta) {
// Shift by 1 because of the values given by zoom_delta(). Multiple by 2, because
// then it felt right with both mouse wheel, and an Android phone.
self.memory.zoom.zoom_by((zoom_delta - 1.) * 2.);
} else {
self.memory
.center_mode
.recalculate_drag(&response, self.my_position);
}

self.memory.center_mode.recalculate_inertial_movement(
ui.ctx(),
self.my_position,
self.memory.zoom.round(),
);

let map_center = self.memory.center_mode.position(self.my_position);
let painter = ui.painter().with_clip_rect(rect);

Expand Down Expand Up @@ -140,32 +145,76 @@ impl Widget for Map<'_, '_> {
/// [`Center::MyPosition`].
#[derive(Clone, PartialEq, Default)]
pub enum Center {
/// Center at `my_position` argument of the [`Map::new()`] function.
/// Centered at `my_position` argument of the [`Map::new()`] function.
#[default]
MyPosition,

/// Center at the exact position.
/// Centered at the exact position.
Exact(Position),

/// Map's currently moving due to inertia, and will slow down and stop after a short while.
Inertia {
position: Position,
direction: Vec2,
amount: f32,
},
}

impl Center {
fn drag(&mut self, response: &Response, my_position: Position, zoom: u8) {
fn recalculate_drag(&mut self, response: &Response, my_position: Position) {
if response.dragged_by(egui::PointerButton::Primary) {
// We always end up in some exact, "detached" position, regardless of the current mode.
*self = Center::Exact(screen_to_position(
self.position(my_position).project(zoom) - response.drag_delta(),
zoom,
));
*self = Center::Inertia {
position: self.position(my_position),
direction: response.drag_delta(),
amount: 1.0,
};
}
}

/// Get the real position at the map's center.
pub fn position(&self, my_position: Position) -> Position {
fn recalculate_inertial_movement(&mut self, ctx: &Context, my_position: Position, zoom: u8) {
if let Center::Inertia {
position,
direction,
amount,
} = &self
{
*self = if amount <= &mut 0.0 {
Center::Exact(*position)
} else {
Center::Inertia {
position: screen_to_position(
self.position(my_position).project(zoom) - (*direction * *amount),
zoom,
),
direction: *direction,
amount: *amount - 0.03,
}
};

// Map is moving due to interia, therefore we need to recalculate in the next frame.
log::trace!("Requesting repaint due to non-zero inertia.");
ctx.request_repaint();
}
}

/// Returns exact position if map is detached (i.e. not following `my_position`),
/// `None` otherwise.
pub fn detached(&self) -> Option<Position> {
match self {
Center::MyPosition => my_position,
Center::Exact(position) => *position,
Center::MyPosition => None,
Center::Exact(position) => Some(*position),
Center::Inertia {
position,
direction: _,
amount: _,
} => Some(*position),
}
}

/// Get the real position at the map's center.
pub fn position(&self, my_position: Position) -> Position {
self.detached().unwrap_or(my_position)
}
}

/// State of the map widget which must persist between frames.
Expand Down
Loading