roaring_landmask/
lib.rs

1//! # The Roaring Landmask
2//!
3//! Have you ever needed to know whether you are in the ocean or on land? And you
4//! need to know it fast? And you need to know it without using too much memory or
5//! too much disk? Then try the _Roaring Landmask_!
6//!
7//! The _roaring landmask_ is a Rust + Python package for quickly determining
8//! whether a point given in latitude and longitude is on land or not. A landmask
9//! is stored in a tree of [Roaring Bitmaps](https://roaringbitmap.org/). Points
10//! close to the shore might still be in the ocean, so a positive
11//! value is then checked against the vector shapes of the coastline.
12//!
13//! <img src="https://raw.githubusercontent.com/gauteh/roaring-landmask/main/the_earth.png" width="50%" />
14//!
15//! ([source](https://github.com/gauteh/roaring-landmask/blob/main/src/devel/make_demo_plot.py))
16//!
17//! The landmask is generated from the [GSHHG shoreline database](https://www.soest.hawaii.edu/pwessel/gshhg/) (Wessel, P., and W. H. F. Smith, A Global Self-consistent, Hierarchical, High-resolution Shoreline Database, J. Geophys. Res., 101, 8741-8743, 1996).
18//!
19//! ## Usage
20//!
21//! ```
22//! # use std::io;
23//! # fn main() -> io::Result<()> {
24//! #
25//! # pyo3::prepare_freethreaded_python();
26//! # pyo3::Python::with_gil(|py| {
27//! use roaring_landmask::RoaringLandmask;
28//!
29//! let mask = RoaringLandmask::new(py).unwrap();
30//!
31//! // Check some points on land
32//! assert!(mask.contains(15., 65.6));
33//! assert!(mask.contains(10., 60.0));
34//!
35//! // Check a point in the ocean
36//! assert!(!mask.contains(5., 65.6));
37//! # });
38//! #
39//! # Ok(())
40//! # }
41//! ```
42//!
43//! or in Python:
44//!
45//! ```python
46//! from roaring_landmask import RoaringLandmask
47//!
48//! l = RoaringLandmask.new()
49//! x = np.arange(-180, 180, .5)
50//! y = np.arange(-90, 90, .5)
51//!
52//! xx, yy = np.meshgrid(x,y)
53//!
54//! print ("points:", len(xx.ravel()))
55//! on_land = l.contains_many(xx.ravel(), yy.ravel())
56//! ```
57
58#![cfg_attr(feature = "nightly", feature(test))]
59#[cfg(feature = "nightly")]
60extern crate test;
61
62#[macro_use]
63extern crate lazy_static;
64
65// geos-sys needs libc++, probably libstdc++. On Windows Conda builds this hopefully adds the
66// correct flags to the linker.
67// extern crate link_cplusplus;
68
69use numpy::{PyArray, PyReadonlyArrayDyn};
70use pyo3::prelude::*;
71use std::io;
72
73pub mod mask;
74pub mod providers;
75pub mod shapes;
76
77pub use mask::RoaringMask;
78pub use providers::LandmaskProvider;
79pub use shapes::Shapes;
80
81include!(concat!(env!("OUT_DIR"), "/source_data.rs"));
82
83#[pymodule]
84fn roaring_landmask(_py: Python, m: &PyModule) -> PyResult<()> {
85    m.add_class::<mask::Affine>()?;
86    m.add_class::<RoaringMask>()?;
87    m.add_class::<Shapes>()?;
88    m.add_class::<RoaringLandmask>()?;
89    m.add_class::<LandmaskProvider>()?;
90
91    Ok(())
92}
93
94#[pyclass]
95pub struct RoaringLandmask {
96    #[pyo3(get)]
97    pub mask: RoaringMask,
98    #[pyo3(get)]
99    pub shapes: Shapes,
100}
101
102#[pymethods]
103impl RoaringLandmask {
104    #[staticmethod]
105    pub fn new(py: Python) -> io::Result<RoaringLandmask> {
106        Self::new_with_provider(py, LandmaskProvider::Gshhg)
107    }
108
109    #[staticmethod]
110    pub fn new_with_provider(
111        py: Python,
112        landmask_provider: LandmaskProvider,
113    ) -> io::Result<RoaringLandmask> {
114        let mask = RoaringMask::new(landmask_provider)?;
115        let shapes = Shapes::new(py, landmask_provider)?;
116
117        Ok(RoaringLandmask { mask, shapes })
118    }
119
120    #[getter]
121    pub fn dx(&self) -> f64 {
122        self.mask.dx()
123    }
124
125    #[getter]
126    pub fn dy(&self) -> f64 {
127        self.mask.dy()
128    }
129
130    /// Check if point (x, y) is on land.
131    ///
132    /// `x` is longitude, [-180, 180] east
133    /// `y` is latitude,  [- 90,  90] north
134    ///
135    ///
136    /// Returns `true` if the point is on land or close to the shore.
137    pub fn contains(&self, x: f64, y: f64) -> bool {
138        assert!(y >= -90. && y <= 90.);
139
140        let x = modulate_longitude(x);
141
142        self.mask.contains_unchecked(x, y) && self.shapes.contains_unchecked(x, y)
143    }
144
145    fn contains_many(
146        &self,
147        py: Python,
148        x: PyReadonlyArrayDyn<f64>,
149        y: PyReadonlyArrayDyn<f64>,
150    ) -> Py<PyArray<bool, numpy::Ix1>> {
151        let x = x.as_array();
152        let y = y.as_array();
153
154        PyArray::from_iter(
155            py,
156            x.iter().zip(y.iter()).map(|(x, y)| self.contains(*x, *y)),
157        )
158        .to_owned()
159    }
160
161    pub fn contains_many_par(
162        &self,
163        py: Python,
164        x: PyReadonlyArrayDyn<f64>,
165        y: PyReadonlyArrayDyn<f64>,
166    ) -> Py<PyArray<bool, numpy::IxDyn>> {
167        let x = x.as_array();
168        let y = y.as_array();
169
170        use ndarray::Zip;
171        let contains = Zip::from(&x)
172            .and(&y)
173            .par_map_collect(|x, y| self.contains(*x, *y));
174        PyArray::from_owned_array(py, contains).to_owned()
175    }
176}
177
178/// Move longitude into -180 to 180 domain.
179fn modulate_longitude(lon: f64) -> f64 {
180    ((lon + 180.) % 360.) - 180.
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn load_ms() {
189        pyo3::prepare_freethreaded_python();
190        pyo3::Python::with_gil(|py| {
191            let _ms = RoaringLandmask::new_with_provider(py, LandmaskProvider::Gshhg).unwrap();
192            let _ms = RoaringLandmask::new_with_provider(py, LandmaskProvider::Osm).unwrap();
193        })
194    }
195
196    #[test]
197    fn test_np() {
198        pyo3::prepare_freethreaded_python();
199        pyo3::Python::with_gil(|py| {
200            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Gshhg).unwrap();
201            assert!(!mask.contains(5., 90.));
202
203            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Osm).unwrap();
204            assert!(!mask.contains(5., 90.));
205        })
206    }
207
208    #[test]
209    fn test_sp() {
210        pyo3::prepare_freethreaded_python();
211        pyo3::Python::with_gil(|py| {
212            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Gshhg).unwrap();
213            assert!(mask.contains(5., -89.99));
214
215            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Osm).unwrap();
216            assert!(mask.contains(5., -89.99));
217        })
218    }
219
220    #[test]
221    #[should_panic]
222    fn test_sp_oob() {
223        pyo3::prepare_freethreaded_python();
224        pyo3::Python::with_gil(|py| {
225            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Gshhg).unwrap();
226            assert!(mask.contains(5., -90.));
227
228            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Osm).unwrap();
229            assert!(mask.contains(5., -90.));
230        })
231    }
232
233    #[test]
234    fn test_dateline_wrap() {
235        pyo3::prepare_freethreaded_python();
236        pyo3::Python::with_gil(|py| {
237            for provider in [LandmaskProvider::Gshhg, LandmaskProvider::Osm] {
238                let mask = RoaringLandmask::new_with_provider(py, provider).unwrap();
239                // Close to NP
240                assert!(!mask.contains(5., 89.));
241                // Close to SP
242                assert!(mask.contains(5., -89.));
243                // Within bounds
244                let x = (-180..180).map(f64::from).collect::<Vec<_>>();
245                let m = x.iter().map(|x| mask.contains(*x, 65.)).collect::<Vec<_>>();
246                // Wrapped bounds
247                let x = (180..540).map(f64::from).collect::<Vec<_>>();
248                let mm = x.iter().map(|x| mask.contains(*x, 65.)).collect::<Vec<_>>();
249                assert_eq!(m, mm);
250            }
251        })
252    }
253
254    #[test]
255    #[should_panic]
256    fn test_not_on_earth_north() {
257        pyo3::prepare_freethreaded_python();
258        pyo3::Python::with_gil(|py| {
259            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Gshhg).unwrap();
260            assert!(!mask.contains(5., 95.));
261
262            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Osm).unwrap();
263            assert!(!mask.contains(5., 95.));
264        })
265    }
266
267    #[test]
268    #[should_panic]
269    fn test_not_on_earth_south() {
270        pyo3::prepare_freethreaded_python();
271        pyo3::Python::with_gil(|py| {
272            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Gshhg).unwrap();
273            assert!(!mask.contains(5., -95.));
274
275            let mask = RoaringLandmask::new_with_provider(py, LandmaskProvider::Osm).unwrap();
276            assert!(!mask.contains(5., -95.));
277        })
278    }
279
280    #[cfg(feature = "nightly")]
281    mod benches {
282        use super::*;
283        use test::Bencher;
284
285        #[bench]
286        fn test_contains_on_land(b: &mut Bencher) {
287            pyo3::prepare_freethreaded_python();
288            pyo3::Python::with_gil(|py| {
289                for provider in [LandmaskProvider::Gshhg, LandmaskProvider::Osm] {
290                    let mask = RoaringLandmask::new_with_provider(py, provider).unwrap();
291                    assert!(mask.contains(15., 65.6));
292                    assert!(mask.contains(10., 60.0));
293                    b.iter(|| mask.contains(15., 65.6));
294                }
295            })
296        }
297
298        #[bench]
299        fn test_contains_in_ocean(b: &mut Bencher) {
300            pyo3::prepare_freethreaded_python();
301            pyo3::Python::with_gil(|py| {
302                for provider in [LandmaskProvider::Gshhg, LandmaskProvider::Osm] {
303                    let mask = RoaringLandmask::new_with_provider(py, provider).unwrap();
304                    assert!(!mask.contains(5., 65.6));
305                    b.iter(|| mask.contains(5., 65.6));
306                }
307            });
308        }
309
310        #[bench]
311        fn test_contains_many(b: &mut Bencher) {
312            pyo3::prepare_freethreaded_python();
313            pyo3::Python::with_gil(|py| {
314                for provider in [LandmaskProvider::Gshhg, LandmaskProvider::Osm] {
315                    let mask = RoaringLandmask::new_with_provider(py, provider).unwrap();
316                    let (x, y): (Vec<f64>, Vec<f64>) = (0..360 * 2)
317                        .map(|v| v as f64 * 0.5 - 180.)
318                        .map(|x| {
319                            (0..180 * 2)
320                                .map(|y| y as f64 * 0.5 - 90.)
321                                .map(move |y| (x, y))
322                        })
323                        .flatten()
324                        .unzip();
325                    let (x, y): (Vec<f64>, Vec<f64>) = (0..360 * 2)
326                        .map(|v| v as f64 * 0.5 - 180.)
327                        .map(|x| {
328                            (0..180 * 2)
329                                .map(|y| y as f64 * 0.5 - 90.)
330                                .map(move |y| (x, y))
331                        })
332                        .flatten()
333                        .unzip();
334                    let x = PyArray::from_vec(py, x);
335                    let y = PyArray::from_vec(py, y);
336                    println!("testing {} points..", x.len());
337                    b.iter(|| {
338                        let len = x.len();
339
340                        let x = x.to_dyn().readonly();
341                        let y = y.to_dyn().readonly();
342
343                        let onland = mask.contains_many(py, x, y);
344                        assert!(onland.as_ref(py).len() == len);
345                    });
346                }
347            })
348        }
349
350        #[bench]
351        #[ignore]
352        fn test_contains_many_par(b: &mut Bencher) {
353            pyo3::prepare_freethreaded_python();
354            pyo3::Python::with_gil(|py| {
355                for provider in [LandmaskProvider::Gshhg, LandmaskProvider::Osm] {
356                    let mask = RoaringLandmask::new_with_provider(py, provider).unwrap();
357                    let (x, y): (Vec<f64>, Vec<f64>) = (0..360 * 2)
358                        .map(|v| v as f64 * 0.5 - 180.)
359                        .map(|x| {
360                            (0..180 * 2)
361                                .map(|y| y as f64 * 0.5 - 90.)
362                                .map(move |y| (x, y))
363                        })
364                        .flatten()
365                        .unzip();
366                    let (x, y): (Vec<f64>, Vec<f64>) = (0..360 * 2)
367                        .map(|v| v as f64 * 0.5 - 180.)
368                        .map(|x| {
369                            (0..180 * 2)
370                                .map(|y| y as f64 * 0.5 - 90.)
371                                .map(move |y| (x, y))
372                        })
373                        .flatten()
374                        .unzip();
375                    let x = PyArray::from_vec(py, x);
376                    let y = PyArray::from_vec(py, y);
377                    println!("testing {} points..", x.len());
378                    b.iter(|| {
379                        let len = x.len();
380
381                        let x = x.to_dyn().readonly();
382                        let y = y.to_dyn().readonly();
383
384                        let onland = mask.contains_many_par(py, x, y);
385                        assert!(onland.as_ref(py).len() == len);
386                    });
387                }
388            })
389        }
390    }
391}