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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
use std::f64::consts::PI;

pub struct Mercator {
    tile_size: f64,
}

impl Mercator {
    /// Create a new Mercator with custom tile size. Tile sizes must be a power of two (256, 512,
    /// and so on).
    pub fn with_size(tile_size: usize) -> Mercator {
        Mercator { tile_size: tile_size as f64 }
    }

    /// Projects a given LL coordinate at a specific zoom level into pixel screen-coordinates.
    ///
    /// Zoom level is between 0 and 29 (inclusive). Every other zoom level will return a `None`.
    pub fn from_ll_to_pixel<T: Coord>(&self, ll: &T, zoom: usize) -> Option<T> {
        if 30 > zoom {
            let c = self.tile_size * 2.0_f64.powi(zoom as i32);
            let bc = c / 360.0;
            let cc = c / (2.0 * PI);

            let d = c / 2.0;
            let e = ((d + ll.x() * bc) + 0.5).floor();
            let f = ll.y().to_radians().sin().max(-0.9999).min(0.9999);
            let g = ((d + 0.5 * ((1.0 + f) / (1.0 - f)).ln() * -cc) + 0.5).floor();

            Some(T::with_xy(e, g))
        } else {
            None
        }
    }

    /// Projects a given pixel position at a specific zoom level into LL world-coordinates.
    ///
    /// Zoom level is between 0 and 29 (inclusive). Every other zoom level will return a `None`.
    pub fn from_pixel_to_ll<T: Coord>(&self, px: &T, zoom: usize) -> Option<T> {
        if 30 > zoom {
            let c = self.tile_size * 2.0_f64.powi(zoom as i32);
            let bc = c / 360.0;
            let cc = c / (2.0 * PI);

            let e = c / 2.0;
            let f = (px.x() - e) / bc;
            let g = (px.y() - e) / -cc;
            let h = (2.0 * g.exp().atan() - 0.5 * PI).to_degrees();

            Some(T::with_xy(f, h))
        } else {
            None
        }
    }
}

impl Default for Mercator {
    fn default() -> Mercator {
        Mercator { tile_size: 256.0 }
    }
}

/// Projects a given LL coordinate at a specific zoom level into pixel screen-coordinates using a
/// default tile size of 256.
///
/// Zoom level is between 0 and 29 (inclusive). Every other zoom level will return a `None`.
///
/// ```rust
/// extern crate googleprojection;
///
/// let pixel = googleprojection::from_ll_to_pixel(&(13.2, 55.9), 2).unwrap();
///
/// assert_eq!(pixel.0, 550.0);
/// assert_eq!(pixel.1, 319.0);
/// ```
pub fn from_ll_to_pixel<T: Coord>(ll: &T, zoom: usize) -> Option<T> {
    Mercator::with_size(256).from_ll_to_pixel(&ll, zoom)
}

/// Projects a given pixel position at a specific zoom level into LL world-coordinates using a
/// default tile size of 256.
///
/// Zoom level is between 0 and 29 (inclusive). Every other zoom level will return a `None`.
///
/// ```rust
/// extern crate googleprojection;
///
/// let ll = googleprojection::from_pixel_to_ll(&(78.0, 78.0), 12).unwrap();
///
/// assert!((ll.0 - -179.9732208251953).abs() < 1e-10);
/// assert!((ll.1 - 85.04881808980566).abs() < 1e-10);
/// ```
pub fn from_pixel_to_ll<T: Coord>(px: &T, zoom: usize) -> Option<T> {
    Mercator::with_size(256).from_pixel_to_ll(&px, zoom)
}

/// A trait for everything that can be treated as a coordinate for a projection.
///
/// Implement this trait if you have a custom type to be able to project to and from it directly.
///
/// There exist an impl for this for `(f64, f64)` out of the box (formatted as `(x, y)`, or `(lon,
/// lat)`).
pub trait Coord {
    /// Return the first of the `f64` pair.
    fn x(&self) -> f64;

    /// Return the second of the `f64` pair.
    fn y(&self) -> f64;

    /// Construct a new Coord implementation from two `f64`.
    fn with_xy(f64, f64) -> Self;
}

impl Coord for (f64, f64) {
    fn x(&self) -> f64 {
        self.0
    }
    fn y(&self) -> f64 {
        self.1
    }

    fn with_xy(x: f64, y: f64) -> (f64, f64) {
        (x, y)
    }
}

#[cfg(test)]
mod test {
    use Coord;

    const EPSILON: f64 = 1e-10;

    fn float_pair_close(pair: &(f64, f64), expected: &(f64, f64)) -> bool {
        ((pair.0 - expected.0).abs() < EPSILON) && ((pair.1 - expected.1).abs() < EPSILON)
    }

    #[test]
    fn it_maps_coords_for_f64_tuple() {
        let coord: (f64, f64) = Coord::with_xy(45.0, 33.0);
        assert_eq!(coord.x(), 45.0);
        assert_eq!(coord.y(), 33.0);
    }

    #[test]
    fn it_projects_to_pixels() {
        let answers = vec![((0.0, 0.0), 0, (128.0, 128.0)),
                           ((0.0, 0.0), 1, (256.0, 256.0)),
                           ((0.0, 0.0), 29, (6.8719476736e10, 6.8719476736e10)),

                           ((0.0, 1.0), 0, (128.0, 127.0)),
                           ((1.0, 0.0), 0, (129.0, 128.0)),
                           ((1.0, 1.0), 0, (129.0, 127.0)),

                           ((5.5, 5.5), 5, (4221.0, 3971.0)),

                           ((100.0, 54.0), 12, (815559.0, 336679.0)),

                           ((-45.0, 12.0), 6, (6144.0, 7642.0))];

        for answer in answers {
            let ll = answer.0;
            let zoom = answer.1;
            let expected = answer.2;

            let actual = super::from_ll_to_pixel(&ll, zoom).unwrap();

            assert!(float_pair_close(&actual, &expected),
                    format!("Expected {:?} at zoom {} to be {:?} but was {:?}",
                            &ll,
                            zoom,
                            &expected,
                            &actual));
        }
    }

    #[test]
    fn it_projects_to_longlat() {
        let answers = vec![((128.0, 128.0), 0, (0.0, 0.0)),
                           ((256.0, 256.0), 1, (0.0, 0.0)),
                           ((6.8719476736e10, 6.8719476736e10), 29, (0.0, 0.0)),

                           ((128.0, 127.0), 0, (0.0, 1.4061088354351594)),
                           ((129.0, 128.0), 0, (1.40625, 0.0)),
                           ((129.0, 127.0), 0, (1.40625, 1.4061088354351594)),

                           ((20.0, 19.0), 0, (-151.875, 82.11838360691269)),

                           ((78.0, 78.0), 12, (-179.9732208251953, 85.04881808980566)),

                           ((-67.0, -100.0), 6, (-181.47216796875, 85.2371040233303))];

        for answer in answers {
            let pixel = answer.0;
            let zoom = answer.1;
            let expected = answer.2;

            let actual = super::from_pixel_to_ll(&pixel, zoom).unwrap();

            assert!(float_pair_close(&actual, &expected),
                    format!("Expected {:?} at zoom {} to be {:?} but was {:?}",
                            &pixel,
                            zoom,
                            &expected,
                            &actual));
        }
    }

    #[test]
    fn it_returns_none_when_zooming_too_far() {
        assert_eq!(super::from_ll_to_pixel(&(0.0, 0.0), 30), None);

        assert_eq!(super::from_pixel_to_ll(&(0.0, 0.0), 30), None);
    }

    #[test]
    fn it_projects_with_custom_size() {
        use super::Mercator;
        let mercator = Mercator::with_size(512);

        let ll = mercator.from_pixel_to_ll(&(512.0, 512.0), 1).unwrap();
        assert!(ll == (0.0, 0.0),
                format!("Pixels 512,512 is on LL 0,0 on zoom 1 on a mercator with 512 pixels \
                         per tile, but got result: {:?}",
                        ll));

        let px = mercator.from_ll_to_pixel(&(0.0, 0.0), 1).unwrap();
        assert!(px == (512.0, 512.0),
                format!("LL 0,0 is on pixels 512,512 on zoom 1 on a mercator with 512 pixels \
                         per tile, but got result: {:?}",
                        px));
    }
}