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

Position is now geo_types::Point #247

Merged
merged 8 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.
## Unreleased

* Do not try to download tiles with invalid coordinates.
* `Position` is now a type alias for `geo_types::Point`. Previous `from_lat_lon` and `from_lon_lat`
methods are now standalone functions called `lat_lon` and `lon_lat`.

## 0.32.0

Expand Down
10 changes: 5 additions & 5 deletions demo/src/places.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
//! Few common places in the city of Wrocław, used in the example app.

use walkers::Position;
use walkers::{lon_lat, Position};

/// Main train station of the city of Wrocław.
/// https://en.wikipedia.org/wiki/Wroc%C5%82aw_G%C5%82%C3%B3wny_railway_station
pub fn wroclaw_glowny() -> Position {
Position::from_lon_lat(17.03664, 51.09916)
lon_lat(17.03664, 51.09916)
}

/// Taking a public bus (line 106) is probably the cheapest option to get from
/// the train station to the airport.
/// https://www.wroclaw.pl/en/how-and-where-to-buy-public-transport-tickets-in-wroclaw
pub fn dworcowa_bus_stop() -> Position {
Position::from_lon_lat(17.03940, 51.10005)
lon_lat(17.03940, 51.10005)
}

/// Musical Theatre Capitol.
/// https://www.teatr-capitol.pl/
pub fn capitol() -> Position {
Position::from_lon_lat(17.03018, 51.10073)
lon_lat(17.03018, 51.10073)
}

/// Shopping center, and the main intercity bus station.
pub fn wroclavia() -> Position {
Position::from_lon_lat(17.03471, 51.09648)
lon_lat(17.03471, 51.09648)
}
2 changes: 1 addition & 1 deletion demo/src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl ClickWatcher {
.title_bar(false)
.anchor(egui::Align2::CENTER_BOTTOM, [0., -10.])
.show(ui.ctx(), |ui| {
ui.label(format!("{:.04} {:.04}", clicked_at.lon(), clicked_at.lat()))
ui.label(format!("{:.04} {:.04}", clicked_at.x(), clicked_at.y()))
.on_hover_text("last clicked position");
});
}
Expand Down
2 changes: 1 addition & 1 deletion demo/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub fn go_to_my_position(ui: &Ui, map_memory: &mut MapMemory) {
.anchor(Align2::RIGHT_BOTTOM, [-10., -10.])
.show(ui.ctx(), |ui| {
ui.label("map center: ");
ui.label(format!("{:.04} {:.04}", position.lon(), position.lat()));
ui.label(format!("{:.04} {:.04}", position.x(), position.y()));
if ui
.button(RichText::new("go to the starting point").heading())
.clicked()
Expand Down
4 changes: 2 additions & 2 deletions walkers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ such as OpenStreetMap and stores them in a cache, `MapMemory` keeps track of
the widget's state and `Map` is the widget itself.

```rust
use walkers::{HttpTiles, Map, MapMemory, Position, sources::OpenStreetMap};
use walkers::{HttpTiles, Map, MapMemory, Position, sources::OpenStreetMap, lon_lat};
use egui::{Context, CentralPanel};
use eframe::{App, Frame};

Expand All @@ -47,7 +47,7 @@ impl App for MyApp {
ui.add(Map::new(
Some(&mut self.tiles),
&mut self.map_memory,
Position::from_lon_lat(17.03664, 51.09916)
lon_lat(17.03664, 51.09916)
));
});
}
Expand Down
2 changes: 1 addition & 1 deletion walkers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ mod zoom;

pub use download::{HeaderValue, HttpOptions};
pub use map::{Map, MapMemory, Plugin, Projector};
pub use mercator::{screen_to_position, Position, TileId};
pub use mercator::{lat_lon, lon_lat, screen_to_position, Position, TileId};
pub use tiles::{HttpTiles, Texture, TextureWithUv, Tiles};
pub use zoom::InvalidZoom;
27 changes: 13 additions & 14 deletions walkers/src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use egui::{Mesh, PointerButton, Rect, Response, Sense, Ui, UiBuilder, Vec2, Widg

use crate::{
center::Center,
mercator::{screen_to_position, Pixels, PixelsExt, TileId},
mercator::{project, screen_to_position, tile_id, Pixels, PixelsExt, TileId},
tiles,
zoom::{InvalidZoom, Zoom},
Position, Tiles,
Expand All @@ -31,13 +31,13 @@ pub trait Plugin {
/// # Examples
///
/// ```
/// # use walkers::{Map, Tiles, MapMemory, Position};
/// # use walkers::{Map, Tiles, MapMemory, Position, lon_lat};
///
/// fn update(ui: &mut egui::Ui, tiles: &mut dyn Tiles, map_memory: &mut MapMemory) {
/// ui.add(Map::new(
/// Some(tiles), // `None`, if you don't want to show any tiles.
/// map_memory,
/// Position::from_lon_lat(17.03664, 51.09916)
/// lon_lat(17.03664, 51.09916)
/// ));
/// }
/// ```
Expand Down Expand Up @@ -150,18 +150,17 @@ impl Projector {
/// Project `position` into pixels on the viewport.
pub fn project(&self, position: Position) -> Vec2 {
// Turn that into a flat, mercator projection.
let projected_position = position.project(self.memory.zoom.into());
let projected_position = project(position, self.memory.zoom.into());

// We need the precision of f64 here,
// since some "gaps" between tiles are noticeable on large zoom levels (e.g. 16+)
let zoom: f64 = self.memory.zoom.into();

// We also need to know where the map center is.
let map_center_projected_position = self
.memory
.center_mode
.position(self.my_position, zoom)
.project(self.memory.zoom.into());
let map_center_projected_position = project(
self.memory.center_mode.position(self.my_position, zoom),
self.memory.zoom.into(),
);

// From the two points above we can calculate the actual point on the screen.
self.clip_rect.center().to_vec2()
Expand All @@ -187,7 +186,7 @@ impl Projector {
let zoom = self.memory.zoom.into();

// return f32 for ergonomics, as the result is typically used for egui code
calculate_meters_per_pixel(position.lat(), zoom) as f32
calculate_meters_per_pixel(position.y(), zoom) as f32
}
}

Expand Down Expand Up @@ -315,8 +314,8 @@ impl Widget for Map<'_, '_, '_> {
let mut meshes = Default::default();
flood_fill_tiles(
painter.clip_rect(),
map_center.tile_id(zoom.round(), tiles.tile_size()),
map_center.project(zoom.into()),
tile_id(map_center, zoom.round(), tiles.tile_size()),
project(map_center, zoom.into()),
zoom.into(),
tiles,
&mut meshes,
Expand Down Expand Up @@ -355,13 +354,13 @@ impl AdjustedPosition {

/// Calculate the real position, i.e. including the offset.
pub(crate) fn position(&self, zoom: f64) -> Position {
screen_to_position(self.position.project(zoom) - self.offset, zoom)
screen_to_position(project(self.position, zoom) - self.offset, zoom)
}

/// Recalculate `position` so that `offset` is zero.
pub(crate) fn zero_offset(self, zoom: f64) -> Self {
Self {
position: screen_to_position(self.position.project(zoom) - self.offset, zoom),
position: screen_to_position(project(self.position, zoom) - self.offset, zoom),
offset: Default::default(),
}
}
Expand Down
124 changes: 45 additions & 79 deletions walkers/src/mercator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,18 @@
// 2 4 × 4 tiles 16 tiles 90° x [variable]

/// Geographical position with latitude and longitude.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Position(geo_types::Point);
pub type Position = geo_types::Point;

impl Position {
/// Construct from latitude and longitude.
pub fn from_lat_lon(lat: f64, lon: f64) -> Self {
Self(geo_types::Point::new(lon, lat))
}

/// Construct from longitude and latitude. Note that it is common standard to write coordinates
/// starting with the latitude instead (e.g. `51.104465719934176, 17.075169894118684` is
/// the [Wrocław's zoo](https://zoo.wroclaw.pl/en/)).
pub fn from_lon_lat(lon: f64, lat: f64) -> Self {
Self(geo_types::Point::new(lon, lat))
}

pub fn lat(&self) -> f64 {
self.0.y()
}

pub fn lon(&self) -> f64 {
self.0.x()
}

/// Project geographical position into a 2D plane using Mercator.
pub(crate) fn project(&self, zoom: f64) -> Pixels {
let total_pixels = total_pixels(zoom);
let (x, y) = mercator_normalized(*self);
Pixels::new(x * total_pixels, y * total_pixels)
}

/// Tile this position is on.
pub(crate) fn tile_id(&self, mut zoom: u8, source_tile_size: u32) -> TileId {
let (x, y) = mercator_normalized(*self);

// Some sources provide larger tiles, effectively bundling e.g. 4 256px tiles in one
// 512px one. Walkers uses 256px internally, so we need to adjust the zoom level.
zoom -= (source_tile_size as f64 / TILE_SIZE as f64).log2() as u8;

// Map that into a big bitmap made out of web tiles.
let number_of_tiles = 2u32.pow(zoom as u32) as f64;
let x = (x * number_of_tiles).floor() as u32;
let y = (y * number_of_tiles).floor() as u32;
/// Construct `Position` from latitude and longitude.
pub fn lat_lon(lat: f64, lon: f64) -> Position {
Position::new(lon, lat)
}

TileId { x, y, zoom }
}
/// Construct `Position` from longitude and latitude. Note that it is common standard to write
/// coordinates starting with the latitude instead (e.g. `51.104465719934176, 17.075169894118684` is
/// the [Wrocław's zoo](https://zoo.wroclaw.pl/en/)).
pub fn lon_lat(lon: f64, lat: f64) -> Position {
Position::new(lon, lat)
}

/// Zoom specifies how many pixels are in the whole map. For example, zoom 0 means that the whole
Expand All @@ -70,18 +36,6 @@ pub fn total_tiles(zoom: u8) -> u32 {
/// Size of a single tile in pixels. Walkers uses 256px tiles as most of the tile sources do.
const TILE_SIZE: u32 = 256;

impl From<geo_types::Point> for Position {
fn from(value: geo_types::Point) -> Self {
Self(value)
}
}

impl From<Position> for geo_types::Point {
fn from(value: Position) -> Self {
value.0
}
}

/// Location projected on the screen or an abstract bitmap.
pub type Pixels = geo_types::Point;

Expand All @@ -100,8 +54,8 @@ impl PixelsExt for Pixels {
/// Project the position into the Mercator projection and normalize it to 0-1 range.
fn mercator_normalized(position: Position) -> (f64, f64) {
// Project into Mercator (cylindrical map projection).
let x = position.lon().to_radians();
let y = position.lat().to_radians().tan().asinh();
let x = position.x().to_radians();
let y = position.y().to_radians().tan().asinh();

// Scale both x and y to 0-1 range.
let x = (1. + (x / PI)) / 2.;
Expand Down Expand Up @@ -163,6 +117,29 @@ impl TileId {
}
}

/// Calculate the tile coordinated for the given position.
pub(crate) fn tile_id(position: Position, mut zoom: u8, source_tile_size: u32) -> TileId {
let (x, y) = mercator_normalized(position);

// Some sources provide larger tiles, effectively bundling e.g. 4 256px tiles in one
// 512px one. Walkers uses 256px internally, so we need to adjust the zoom level.
zoom -= (source_tile_size as f64 / TILE_SIZE as f64).log2() as u8;

// Map that into a big bitmap made out of web tiles.
let number_of_tiles = 2u32.pow(zoom as u32) as f64;
let x = (x * number_of_tiles).floor() as u32;
let y = (y * number_of_tiles).floor() as u32;

TileId { x, y, zoom }
}

/// Project geographical position into a 2D plane using Mercator.
pub(crate) fn project(position: Position, zoom: f64) -> Pixels {
let total_pixels = total_pixels(zoom);
let (x, y) = mercator_normalized(position);
Pixels::new(x * total_pixels, y * total_pixels)
}

/// Transforms screen pixels into a geographical position.
pub fn screen_to_position(pixels: Pixels, zoom: f64) -> Position {
let number_of_pixels: f64 = 2f64.powf(zoom) * (TILE_SIZE as f64);
Expand All @@ -177,7 +154,7 @@ pub fn screen_to_position(pixels: Pixels, zoom: f64) -> Position {
let lat = (-lat * 2. + 1.) * PI;
let lat = lat.sinh().atan().to_degrees();

Position::from_lon_lat(lon, lat)
lon_lat(lon, lat)
}

#[cfg(test)]
Expand All @@ -186,7 +163,7 @@ mod tests {

#[test]
fn projecting_position_and_tile() {
let citadel = Position::from_lon_lat(21.00027, 52.26470);
let citadel = lon_lat(21.00027, 52.26470);

// Just a bit higher than what most providers support,
// to make sure we cover the worst case in terms of precision.
Expand All @@ -198,7 +175,7 @@ mod tests {
y: 345104,
zoom
},
citadel.tile_id(zoom, 256)
tile_id(citadel, zoom, 256)
);

// Automatically zooms out for larger tiles
Expand All @@ -208,42 +185,31 @@ mod tests {
y: 172552,
zoom: zoom - 1
},
citadel.tile_id(zoom, 512)
tile_id(citadel, zoom, 512)
);

// Projected tile is just its x, y multiplied by the size of tiles.
assert_eq!(
Pixels::new(585455. * 256., 345104. * 256.),
citadel.tile_id(zoom, 256).project(256.)
tile_id(citadel, zoom, 256).project(256.)
);

// Projected Citadel position should be somewhere near projected tile, shifted only by the
// position on the tile.
let calculated = citadel.project(zoom as f64);
let calculated = project(citadel, zoom as f64);
let citadel_proj = Pixels::new(585455. * 256. + 184., 345104. * 256. + 116.5);
approx::assert_relative_eq!(calculated.x(), citadel_proj.x(), max_relative = 0.5);
approx::assert_relative_eq!(calculated.y(), citadel_proj.y(), max_relative = 0.5);
}

#[test]
fn project_there_and_back() {
let citadel = Position::from_lat_lon(21.00027, 52.26470);
let citadel = lat_lon(21.00027, 52.26470);
let zoom = 16;
let calculated = screen_to_position(citadel.project(zoom as f64), zoom as f64);
let calculated = screen_to_position(project(citadel, zoom as f64), zoom as f64);

approx::assert_relative_eq!(calculated.lon(), citadel.lon(), max_relative = 1.0);
approx::assert_relative_eq!(calculated.lat(), citadel.lat(), max_relative = 1.0);
}

#[test]
/// Just to be compatible with the `geo` ecosystem.
fn position_is_compatible_with_geo_types() {
let original = Position::from_lat_lon(21.00027, 52.26470);
let converted: geo_types::Point = original.into();
let brought_back: Position = converted.into();

approx::assert_relative_eq!(original.lon(), brought_back.lon());
approx::assert_relative_eq!(original.lat(), brought_back.lat());
approx::assert_relative_eq!(calculated.x(), citadel.x(), max_relative = 1.0);
approx::assert_relative_eq!(calculated.y(), citadel.y(), max_relative = 1.0);
}

#[test]
Expand Down
Loading