map_engine/raster/
mod.rs

1//! Types and helpers to work with raster images.
2pub mod pixels;
3
4use crate::{
5    affine::GeoTransform,
6    errors::MapEngineError,
7    tiles::{Tile, TILE_SIZE},
8    windows::intersection,
9    windows::Window,
10};
11use gdal::{
12    raster::{GdalType, RasterBand, ResampleAlg},
13    spatial_ref::SpatialRef,
14    Dataset,
15};
16use ndarray::{s, Array, Array2, Array3};
17use num_traits::{Num, NumCast};
18pub use pixels::{driver::MBTILES, RawPixels, StyledPixels};
19use std::cmp;
20use std::path::PathBuf;
21
22/// A Raster image.
23#[derive(Debug, Clone)]
24pub struct Raster {
25    path: PathBuf,
26    geo: GeoTransform,
27    epsg_code: i32,
28    driver_name: String,
29    raster_count: isize,
30    raster_size: (usize, usize),
31}
32
33impl Raster {
34    /// Crete a new `Raster`.
35    ///
36    /// This will open a [`Dataset`] and store some metadata into the `Raster` struct. This serves
37    /// as a cache to avoid constantly reading from the file.
38    pub fn new(path: PathBuf) -> Result<Self, MapEngineError> {
39        let src = Dataset::open(&path)?;
40        let geo = src.geo_transform()?;
41        let geo = GeoTransform::from_gdal(&geo);
42        Ok(Self {
43            path,
44            geo,
45            epsg_code: src.spatial_ref()?.auth_code()?,
46            driver_name: src.driver().short_name(),
47            raster_count: src.raster_count(),
48            raster_size: src.raster_size(),
49        })
50    }
51
52    /// Create a new `Raster` from an open [`Dataset`].
53    ///
54    /// Usually, you would want to use `Raster::new` but this method is available in case you
55    /// already opened a `Dataset`.
56    pub fn from_src(path: PathBuf, src: &Dataset) -> Result<Self, MapEngineError> {
57        let geo = src.geo_transform()?;
58        let geo = GeoTransform::from_gdal(&geo);
59        Ok(Self {
60            path,
61            geo,
62            epsg_code: src.spatial_ref()?.auth_code()?,
63            driver_name: src.driver().short_name(),
64            raster_count: src.raster_count(),
65            raster_size: src.raster_size(),
66        })
67    }
68
69    /// Read a tile from raster file.
70    ///
71    /// # Arguments
72    ///
73    /// * `tile` - Tile to read.
74    /// * `bands` - Bands to read (1-indexed).
75    /// * `e_resample_alg` - Resample algorith to use in case interpolations are needed.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use gdal::raster::ResampleAlg;
81    /// use map_engine::{errors::MapEngineError, raster::{Raster, RawPixels}, tiles::Tile};
82    /// use std::path::PathBuf;
83    ///
84    /// fn main() -> Result<(), MapEngineError> {
85    ///     let path = PathBuf::from("src/tests/data/chile_optimised.tif");
86    ///     let raster = Raster::new(path)?;
87    ///     let tile = Tile::new(304, 624, 10);
88    ///
89    ///     let arr: RawPixels<f64> = raster.read_tile(&tile, Some(&[1]), Some(ResampleAlg::Average))?;
90    ///     assert_eq!(arr.as_array().shape(), &[1, 256, 256]);
91    ///     Ok(())
92    /// }
93    /// ```
94    pub fn read_tile<P>(
95        &self,
96        tile: &Tile,
97        bands: Option<&[isize]>,
98        e_resample_alg: Option<ResampleAlg>,
99    ) -> Result<RawPixels<P>, MapEngineError>
100    where
101        P: GdalType + Copy + Num + NumCast,
102        P: ndarray::ScalarOperand,
103    {
104        let src = Dataset::open(&self.path)?;
105        let driver_name = self.driver_name();
106        // let tile_size = TILE_SIZE as usize;
107        let geo = self.geo();
108        let epsg_code = self.epsg_code;
109        let (mut win, is_skewed) = tile.to_window(self)?;
110
111        let tile_bounds_xy = tile.bounds_xy();
112        if is_skewed {
113            win = win * 2f64.sqrt();
114        };
115
116        let all_bands: Vec<_> = (1..=self.raster_count()).collect();
117        let mut bands = bands.unwrap_or(&all_bands);
118        if driver_name == MBTILES {
119            bands = &all_bands;
120        }
121
122        let mut container_arr = Array3::<P>::zeros((bands.len(), TILE_SIZE, TILE_SIZE));
123
124        for (out_idx, band_index) in bands.iter().enumerate() {
125            let band = src.rasterband(*band_index)?;
126
127            let band_data = try_boundless(
128                &src,
129                &band,
130                &win,
131                geo,
132                epsg_code,
133                tile_bounds_xy,
134                is_skewed,
135                e_resample_alg,
136            );
137            let band_data = if let Some(d) = band_data {
138                d
139            } else {
140                try_overview(
141                    &band,
142                    &win,
143                    // req_overview as f64,
144                    geo,
145                    epsg_code,
146                    tile_bounds_xy,
147                    is_skewed,
148                    e_resample_alg,
149                )?
150            };
151
152            container_arr
153                .slice_mut(s![out_idx, .., ..])
154                .assign(&band_data);
155        }
156
157        // TODO: evaluate if we have to read this every time
158        if driver_name == MBTILES {
159            container_arr.swap_axes(0, 1);
160            container_arr.swap_axes(1, 2);
161        }
162        Ok(RawPixels::new(container_arr, driver_name))
163    }
164
165    pub fn geo(&self) -> &GeoTransform {
166        &self.geo
167    }
168
169    pub fn spatial_ref(&self) -> Result<SpatialRef, MapEngineError> {
170        Ok(SpatialRef::from_epsg(self.epsg_code as u32)?)
171    }
172
173    pub fn driver_name(&self) -> &str {
174        &self.driver_name
175    }
176
177    /// Get number of bands available in the file.
178    pub fn raster_count(&self) -> isize {
179        self.raster_count
180    }
181
182    pub fn raster_size(&self) -> (usize, usize) {
183        self.raster_size
184    }
185
186    pub fn intersects(&self, tile: &Tile) -> Result<bool, MapEngineError> {
187        let (raster_w, raster_h) = self.raster_size();
188        let raster_win = Window::new(0, 0, raster_w, raster_h);
189        Ok(intersection(&[raster_win, tile.to_window(self)?.0]).is_some())
190    }
191}
192
193fn array_to_mem_dataset<N>(
194    arr: Array2<N>,
195    transform: &GeoTransform,
196    crs: i32,
197    fname: &str,
198) -> Result<Dataset, MapEngineError>
199where
200    N: Clone + GdalType + Copy,
201{
202    let shape = arr.shape();
203    let count = 1;
204    let height = shape[0];
205    let width = shape[1];
206
207    let driver = gdal::Driver::get("MEM")?;
208    // let driver = gdal::Driver::get("GTiff")?;
209
210    let mut dataset = driver
211        .create_with_band_type::<N, _>(fname, height as isize, width as isize, count as isize)
212        .unwrap();
213    let gt = transform.to_tuple();
214    let gt = [gt.0, gt.1, gt.2, gt.3, gt.4, gt.5];
215    dataset.set_geo_transform(&gt)?;
216    dataset.set_spatial_ref(&SpatialRef::from_epsg(crs as u32)?)?;
217
218    let mut band = dataset.rasterband(1)?;
219
220    let v: Vec<N> = Array::from_iter(arr.into_iter()).to_vec();
221    let buff = gdal::raster::Buffer::<N>::new((height, width), v);
222    band.write((0, 0), (height, width), &buff)?;
223    drop(band);
224    Ok(dataset)
225}
226
227fn reproject<N>(
228    source: Array2<N>,
229    src_transform: &GeoTransform,
230    src_crs: i32,
231    destination: Array2<N>,
232    dst_transform: &GeoTransform,
233    dst_crs: i32,
234) -> Result<Array2<N>, MapEngineError>
235where
236    N: GdalType + Copy,
237{
238    let dst_shape = &destination.shape();
239    let dst_shape = (dst_shape[0], dst_shape[1]);
240    let src_dataset = array_to_mem_dataset(source, src_transform, src_crs, "/tmp/src.tif")?;
241    let dst_dataset = array_to_mem_dataset(destination, dst_transform, dst_crs, "/tmp/dst.tif")?;
242    gdal::raster::reproject(&src_dataset, &dst_dataset)?;
243
244    let dst_band = dst_dataset.rasterband(1)?;
245
246    dst_band
247        .read_as_array::<N>(
248            (0, 0),
249            dst_shape,
250            (TILE_SIZE, TILE_SIZE),
251            Some(gdal::raster::ResampleAlg::NearestNeighbour),
252        )
253        .map_err(From::from)
254}
255
256fn read_and_reproject<N>(
257    band: &RasterBand,
258    win: &Window,
259    geo: &GeoTransform,
260    epsg_code: i32,
261    tile_bounds_xy: (f64, f64, f64, f64),
262    e_resample_alg: Option<ResampleAlg>,
263) -> Result<Array2<N>, MapEngineError>
264where
265    N: GdalType + Copy + Num,
266{
267    let win_geo = win.geotransform(geo).to_gdal();
268
269    let d = band.read_as::<N>(
270        (win.col_off, win.row_off),
271        (win.width as usize, win.height as usize),
272        (win.width as usize, win.height as usize),
273        e_resample_alg,
274    )?;
275
276    let arr = Array::from_iter(d.data).into_shape((win.width as usize, win.height as usize))?;
277
278    let res_x = win_geo.geo[1];
279    let res_y = win_geo.geo[5];
280    let (min_x, max_y, max_x, min_y) = tile_bounds_xy;
281    let mercator_geo = &GeoTransform::new(&[min_x, res_x, 0.0, max_y, 0.0, res_y]);
282    let dst_cols = ((max_x - min_x) / res_x) as usize;
283    let dst_rows = ((max_y - min_y) / -res_y) as usize;
284    let dst_shape = (dst_cols, dst_rows);
285    let dst_arr = Array2::<N>::zeros(dst_shape);
286    reproject(arr, &win_geo, epsg_code, dst_arr, mercator_geo, 3857)
287}
288
289fn try_overview<N>(
290    band: &RasterBand,
291    win: &Window,
292    // factor: f64,
293    geo: &GeoTransform,
294    epsg_code: i32,
295    tile_bounds_xy: (f64, f64, f64, f64),
296    is_skewed: bool,
297    e_resample_alg: Option<ResampleAlg>,
298) -> Result<Array2<N>, MapEngineError>
299where
300    N: GdalType + Copy + Num,
301{
302    if is_skewed {
303        read_and_reproject(band, win, geo, epsg_code, tile_bounds_xy, e_resample_alg)
304    } else {
305        band.read_as_array::<N>(
306            // (new_win.col_off, new_win.row_off),
307            // (new_win.width as usize, new_win.height as usize),
308            (win.col_off, win.row_off),
309            (win.width as usize, win.height as usize),
310            (TILE_SIZE, TILE_SIZE),
311            e_resample_alg,
312        )
313        .map_err(From::from)
314    }
315}
316
317// Read pixels within a Window
318#[allow(clippy::too_many_arguments)]
319fn try_boundless<N>(
320    src: &Dataset,
321    band: &RasterBand,
322    win: &Window,
323    geo: &GeoTransform,
324    epsg_code: i32,
325    tile_bounds_xy: (f64, f64, f64, f64),
326    is_skewed: bool,
327    e_resample_alg: Option<ResampleAlg>,
328) -> Option<Array2<N>>
329where
330    N: GdalType + Copy + Num + NumCast,
331    N: ndarray::ScalarOperand,
332{
333    let (raster_w, raster_h) = src.raster_size();
334    let raster_win = Window::new(0, 0, raster_w, raster_h);
335    let inter = intersection(&[raster_win, *win]);
336    if let Some(inter) = inter {
337        if (inter.height >= win.height || inter.width >= win.width)
338            && (win.col_off >= 0 && win.row_off >= 0)
339            && (win.row_off + win.height as isize) < raster_win.height as isize
340            && (win.col_off + win.width as isize) < raster_win.width as isize
341        {
342            // The image is larger than the tile. Return None to proceed normally (try_overview)
343            return None;
344        }
345    };
346
347    let mut container_arr = match band.no_data_value() {
348        None => Array2::<N>::zeros((TILE_SIZE, TILE_SIZE)),
349        Some(val) => {
350            let val = N::from(val)?;
351            Array2::<N>::ones((TILE_SIZE, TILE_SIZE)) * val
352        }
353    };
354
355    if inter.is_none() {
356        return Some(container_arr);
357    };
358
359    let inter = inter.unwrap();
360    // The image is smaller than the tile
361    let factor = (
362        win.width as f64 / inter.width as f64,
363        win.height as f64 / inter.height as f64,
364    );
365    let data = if is_skewed {
366        read_and_reproject(band, &inter, geo, epsg_code, tile_bounds_xy, e_resample_alg)
367    } else {
368        let into_shape = (
369            (TILE_SIZE as f64 / factor.0).floor() as usize,
370            (TILE_SIZE as f64 / factor.1).floor() as usize,
371        );
372        band.read_as_array::<N>(
373            (inter.col_off, inter.row_off),
374            (inter.width, inter.height),
375            into_shape,
376            e_resample_alg,
377        )
378        .map_err(From::from)
379    }
380    .unwrap_or_else(|_| panic!("Cannot read window {:?} from {:?}", inter, src));
381
382    let col_off = if win.col_off < 0 {
383        (TILE_SIZE as f64 * (win.col_off as f64 / win.width as f64)).trunc() as isize
384    } else {
385        0
386    };
387    let row_off = if win.row_off < 0 {
388        (TILE_SIZE as f64 * (win.row_off as f64 / win.height as f64)).trunc() as isize
389    } else {
390        0
391    };
392    let (row_range, col_range) = if is_skewed {
393        let row_range = ((TILE_SIZE - data.shape()[0]) as isize)
394            ..cmp::min(row_off.abs() + data.shape()[0] as isize, TILE_SIZE as isize);
395        let col_range = ((TILE_SIZE - data.shape()[1]) as isize)
396            ..cmp::min(col_off.abs() + data.shape()[1] as isize, TILE_SIZE as isize);
397        (row_range, col_range)
398    } else {
399        let row_range =
400            row_off.abs()..cmp::min(row_off.abs() + data.shape()[0] as isize, TILE_SIZE as isize);
401        let col_range =
402            col_off.abs()..cmp::min(col_off.abs() + data.shape()[1] as isize, TILE_SIZE as isize);
403        (row_range, col_range)
404    };
405    container_arr
406        .slice_mut(s![row_range, col_range])
407        .assign(&data);
408    Some(container_arr)
409}
410
411#[cfg(test)]
412mod test {
413    use super::*;
414    use std::path::Path;
415
416    #[test]
417    fn test_try_boundless() {
418        let path = Path::new("src/tests/data/chile_optimised.tif");
419        let src = Dataset::open(path).unwrap();
420        let epsg_code = src.spatial_ref().unwrap().auth_code().unwrap();
421        let geo = src.geo_transform().unwrap();
422        let geo = GeoTransform::from_gdal(&geo);
423        let band = src.rasterband(1).unwrap();
424        // The whole image is 1/16 of the window size
425        let win = Window::new(0, 0, 2048, 2048); // quad row=0,col=0
426        let tile_bounds_xy = win.bounds(&geo);
427        let arr: Array2<f64> = try_boundless(
428            &src,
429            &band,
430            &win,
431            &geo,
432            epsg_code,
433            tile_bounds_xy,
434            false,
435            Some(ResampleAlg::Average),
436        )
437        .unwrap();
438        assert_eq!(arr.shape(), &[256, 256]);
439        let expected = Array::from_iter([2251., 2242., 0., 2251., 2259., 0., 0., 0., 0.])
440            .into_shape((3, 3))
441            .unwrap();
442        assert_eq!(arr.slice(s![62..65, 62..65]), expected);
443        let win = Window::new(-1024, -512, 2048, 2048); // quad row=1,col=2
444        let tile_bounds_xy = win.bounds(&geo);
445        let arr: Array2<f64> = try_boundless(
446            &src,
447            &band,
448            &win,
449            &geo,
450            epsg_code,
451            tile_bounds_xy,
452            false,
453            Some(ResampleAlg::Average),
454        )
455        .unwrap();
456        assert_eq!(arr.shape(), &[256, 256]);
457        assert_eq!(arr.slice(s![126..129, 190..193]), expected);
458        // Read bottom-right 1/4 of the image
459        let win = Window::new(256, 256, 2048, 2048);
460        let tile_bounds_xy = win.bounds(&geo);
461        let arr: Array2<f64> = try_boundless(
462            &src,
463            &band,
464            &win,
465            &geo,
466            epsg_code,
467            tile_bounds_xy,
468            false,
469            Some(ResampleAlg::Average),
470        )
471        .unwrap();
472        assert_eq!(arr.shape(), &[256, 256]);
473        let expected = Array::from_iter([2251., 2242., 0., 2251., 2259., 0., 0., 0., 0.])
474            .into_shape((3, 3))
475            .unwrap();
476        assert_eq!(arr.slice(s![30..33, 30..33]), expected);
477        // Read bottom-left 1/4 of the image
478        let win = Window::new(-1792, 256, 2048, 2048);
479        let tile_bounds_xy = win.bounds(&geo);
480        let arr: Array2<f64> = try_boundless(
481            &src,
482            &band,
483            &win,
484            &geo,
485            epsg_code,
486            tile_bounds_xy,
487            false,
488            Some(ResampleAlg::Average),
489        )
490        .unwrap();
491        assert_eq!(arr.shape(), &[256, 256]);
492        let expected = Array::from_iter([0., 4645., 4527., 0., 4706., 4297., 0., 0., 0.])
493            .into_shape((3, 3))
494            .unwrap();
495        assert_eq!(arr.slice(s![30..33, 223..226]), expected);
496
497        // These tiles don't exist but they simulate some cases
498        let win = Window::new(0, -512, 512, 512 * 2);
499        let tile_bounds_xy = win.bounds(&geo);
500        let arr: Option<Array2<f64>> = try_boundless(
501            &src,
502            &band,
503            &win,
504            &geo,
505            epsg_code,
506            tile_bounds_xy,
507            false,
508            Some(ResampleAlg::Average),
509        );
510        assert!(arr.is_some());
511
512        let win = Window::new(0, 256, 512, 512 * 2);
513        let tile_bounds_xy = win.bounds(&geo);
514        let arr: Option<Array2<f64>> = try_boundless(
515            &src,
516            &band,
517            &win,
518            &geo,
519            epsg_code,
520            tile_bounds_xy,
521            false,
522            Some(ResampleAlg::Average),
523        );
524        assert!(arr.is_some());
525    }
526
527    #[test]
528    fn test_non_intersecting_returns_no_data_tile() {
529        let path = Path::new("src/tests/data/categorical_optimised.tif");
530        let src = Dataset::open(path).unwrap();
531        let epsg_code = src.spatial_ref().unwrap().auth_code().unwrap();
532        let geo = src.geo_transform().unwrap();
533        let geo = GeoTransform::from_gdal(&geo);
534        let mut band = src.rasterband(1).unwrap();
535
536        // A window with no intersecting area, should return an array of no_data_value
537        let win = Window::new(2048, 2048, 4096, 4096);
538        let tile_bounds_xy = win.bounds(&geo);
539
540        band.set_no_data_value(2f64).unwrap();
541        // Setting the return type to `Array2<f32>` makes the generic algorithm
542        // return the expected type.
543        let arr: Array2<f32> = try_boundless(
544            &src,
545            &band,
546            &win,
547            &geo,
548            epsg_code,
549            tile_bounds_xy,
550            false,
551            None,
552        )
553        .unwrap();
554
555        // Simulate an array of the same dimensions and values
556        let expected: Array2<f32> = Array2::ones((256, 256)) * 2f32;
557        assert_eq!(arr, expected);
558
559        // TODO: what's the story here? what do we want to achieve?
560        // let _arr2 = arr.mapv(|v| {
561        //     v.to_u8()
562        //         .unwrap_or(band.no_data_value().unwrap_or(0.0) as u8)
563        // });
564    }
565
566    #[test]
567    fn test_generic_function_returns_expected_type() {
568        let path = Path::new("src/tests/data/chile_optimised.tif");
569        let src = Dataset::open(path).unwrap();
570        let epsg_code = src.spatial_ref().unwrap().auth_code().unwrap();
571        let geo = src.geo_transform().unwrap();
572        let geo = GeoTransform::from_gdal(&geo);
573        let band = src.rasterband(1).unwrap();
574        let win = Window::new(-1792, 256, 2048, 2048);
575        let tile_bounds_xy = win.bounds(&geo);
576
577        let arr: Array2<f32> = try_boundless(
578            &src,
579            &band,
580            &win,
581            &geo,
582            epsg_code,
583            tile_bounds_xy,
584            false,
585            Some(ResampleAlg::Average),
586        )
587        .unwrap();
588
589        // Return types should be f32
590        let expected = Array::from_iter([
591            0.0f32, 4645.0f32, 4527.0f32, 0.0f32, 4706.0f32, 4297.0f32, 0.0f32, 0.0f32, 0.0f32,
592        ])
593        .into_shape((3, 3))
594        .unwrap();
595        assert_eq!(arr.slice(s![30..33, 223..226]), expected);
596
597        let arr: Array2<u32> = try_boundless(
598            &src,
599            &band,
600            &win,
601            &geo,
602            epsg_code,
603            tile_bounds_xy,
604            false,
605            Some(ResampleAlg::Average),
606        )
607        .unwrap();
608
609        // Return types should be u32
610        let expected = Array::from_iter([
611            0u32, 4645u32, 4527u32, 0u32, 4706u32, 4297u32, 0u32, 0u32, 0u32,
612        ])
613        .into_shape((3, 3))
614        .unwrap();
615        assert_eq!(arr.slice(s![30..33, 223..226]), expected);
616
617        // Following code does not compile!, type annotations are needed!
618        // let arr: Array2<_> = try_boundless(&src, &band, &win, Some(ResampleAlg::Average)).unwrap();
619    }
620
621    #[test]
622    fn test_band_type_does_not_fit_in_resulting_type() {
623        let path = Path::new("src/tests/data/chile_optimised.tif");
624        let src = Dataset::open(path).unwrap();
625        let epsg_code = src.spatial_ref().unwrap().auth_code().unwrap();
626        let geo = src.geo_transform().unwrap();
627        let geo = GeoTransform::from_gdal(&geo);
628        let band = src.rasterband(1).unwrap();
629        let win = Window::new(-1792, 256, 2048, 2048);
630        let tile_bounds_xy = win.bounds(&geo);
631
632        let arr: Array2<u8> = try_boundless(
633            &src,
634            &band,
635            &win,
636            &geo,
637            epsg_code,
638            tile_bounds_xy,
639            false,
640            Some(ResampleAlg::Average),
641        )
642        .unwrap();
643
644        // Return types should be u8
645        // Using the `as` operator, and because particular u32 values do not fit in u8 they are
646        // made whatever the amount of bytes from the smalles type represent.
647        let expected = Array::from_iter([0u8, 255, 255, 0, 255, 255, 0, 0, 0])
648            .into_shape((3, 3))
649            .unwrap();
650
651        // values not fitting in u8 will be made std::u8::MAX
652        assert_eq!(arr.slice(s![30..33, 223..226]), expected);
653
654        // and not "<big-value><big-type> as <small-type>"
655        // returns any value, not necessarily <small-type>::MAX
656        let not_expected = Array::from_iter([
657            0u8,
658            4645u32 as u8,
659            4527u32 as u8,
660            0u8,
661            4706u32 as u8,
662            4297u32 as u8,
663            0u8,
664            0u8,
665            0u8,
666        ])
667        .into_shape((3, 3))
668        .unwrap();
669
670        assert_ne!(arr.slice(s![30..33, 223..226]), not_expected);
671    }
672
673    #[test]
674    fn test_intersects() {
675        let path = Path::new("src/tests/data/chile_optimised.tif");
676        let raster = Raster::new(path.into()).unwrap();
677        let tile1 = Tile::new(304, 624, 10);
678        assert!(raster.intersects(&tile1).unwrap());
679        let tile1 = Tile::new(303, 624, 10);
680        assert!(!raster.intersects(&tile1).unwrap());
681    }
682}