1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! Map projection trait.
use crate::bounds::GeoBounds;
use crate::coord::{GeoCoord, WorldCoord};
/// A map projection that converts between geographic and projected coordinates.
///
/// Implementations must be thread-safe (`Send + Sync`) so they can be
/// shared across rendering and engine threads.
pub trait Projection: Send + Sync {
/// Project a geographic coordinate to world space (meters).
fn project(&self, geo: &GeoCoord) -> WorldCoord;
/// Inverse-project world coordinates back to geographic.
fn unproject(&self, world: &WorldCoord) -> GeoCoord;
/// Local linear scale factor at the given geographic coordinate.
///
/// Returns the ratio of projected distance to true geodesic distance
/// at `geo`. For a conformal projection like Web Mercator this is
/// `1 / cos(lat)` (i.e. `sec(lat)`); for Equirectangular at the
/// equator it is `1.0`.
///
/// Useful for computing meters-per-pixel, line-width scaling, and
/// LOD thresholds.
///
/// The default implementation returns `1.0` (no distortion),
/// which is correct only at the standard parallel.
fn scale_factor(&self, _geo: &GeoCoord) -> f64 {
1.0
}
/// The geographic bounding box of valid input for this projection.
///
/// Coordinates outside this range may produce `NaN` or `Infinity`
/// when projected. For example, Web Mercator is valid only within
/// approximately 85.06 degrees latitude.
///
/// The default implementation returns the full geographic range
/// (90 degrees lat, 180 degrees lon).
fn projection_bounds(&self) -> GeoBounds {
GeoBounds::new(
GeoCoord::from_lat_lon(-90.0, -180.0),
GeoCoord::from_lat_lon(90.0, 180.0),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Verify the trait is object-safe (can be used as `dyn Projection`).
#[test]
fn object_safety() {
fn _accept(_p: &dyn Projection) {}
}
/// Verify a boxed trait object is Send + Sync.
#[test]
fn boxed_send_sync() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<Box<dyn Projection>>();
}
/// Default `scale_factor` returns 1.0.
#[test]
fn default_scale_factor() {
struct Dummy;
impl Projection for Dummy {
fn project(&self, _geo: &GeoCoord) -> WorldCoord {
WorldCoord::default()
}
fn unproject(&self, _world: &WorldCoord) -> GeoCoord {
GeoCoord::default()
}
}
let d = Dummy;
assert!((d.scale_factor(&GeoCoord::from_lat_lon(45.0, 10.0)) - 1.0).abs() < f64::EPSILON);
}
/// Default `projection_bounds` covers the full globe.
#[test]
fn default_projection_bounds() {
struct Dummy;
impl Projection for Dummy {
fn project(&self, _geo: &GeoCoord) -> WorldCoord {
WorldCoord::default()
}
fn unproject(&self, _world: &WorldCoord) -> GeoCoord {
GeoCoord::default()
}
}
let bounds = Dummy.projection_bounds();
assert!((bounds.sw().lat - (-90.0)).abs() < f64::EPSILON);
assert!((bounds.ne().lat - 90.0).abs() < f64::EPSILON);
assert!((bounds.sw().lon - (-180.0)).abs() < f64::EPSILON);
assert!((bounds.ne().lon - 180.0).abs() < f64::EPSILON);
}
}