maplibre/
coords.rs

1//! Provides utilities related to coordinates.
2
3use std::{
4    f64::consts::PI,
5    fmt,
6    fmt::{Display, Formatter},
7};
8
9use bytemuck_derive::{Pod, Zeroable};
10use cgmath::{AbsDiffEq, Matrix4, Point3, Vector3, Vector4};
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    style::source::TileAddressingScheme,
15    util::{
16        math::{div_floor, Aabb2},
17        SignificantlyDifferent,
18    },
19};
20
21pub const EXTENT_UINT: u32 = 4096;
22pub const EXTENT_SINT: i32 = EXTENT_UINT as i32;
23pub const EXTENT: f64 = EXTENT_UINT as f64;
24const TOP_LEFT_EXTENT: Vector4<f64> = Vector4::new(0.0, 0.0, 0.0, 1.0);
25const BOTTOM_RIGHT_EXTENT: Vector4<f64> = Vector4::new(EXTENT, EXTENT, 0.0, 1.0);
26
27pub const TILE_SIZE: f64 = 512.0;
28pub const MAX_ZOOM: usize = 32;
29
30// FIXME: MAX_ZOOM is 32, which means max bound is 2^32, which wouldn't fit in u32 or i32
31// Bounds are generated 0..=31
32pub const ZOOM_BOUNDS: [u32; MAX_ZOOM] = create_zoom_bounds::<MAX_ZOOM>();
33
34const fn create_zoom_bounds<const DIM: usize>() -> [u32; DIM] {
35    let mut result: [u32; DIM] = [0; DIM];
36    let mut i = 0;
37    while i < DIM {
38        result[i] = 2u32.pow(i as u32);
39        i += 1;
40    }
41    result
42}
43
44/// Represents the position of a node within a quad tree. The first u8 defines the `ZoomLevel` of the node.
45/// The remaining bytes define which part (north west, south west, south east, north east) of each
46/// subdivision of the quadtree is concerned.
47///
48/// TODO: We can optimize the quadkey and store the keys on 2 bits instead of 8
49#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy)]
50pub struct Quadkey([ZoomLevel; MAX_ZOOM]);
51
52impl Quadkey {
53    pub fn new(quad_encoded: &[ZoomLevel]) -> Self {
54        let mut key = [ZoomLevel::default(); MAX_ZOOM];
55        key[0] = (quad_encoded.len() as u8).into();
56        for (i, part) in quad_encoded.iter().enumerate() {
57            key[i + 1] = *part;
58        }
59        Self(key)
60    }
61}
62
63impl fmt::Debug for Quadkey {
64    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
65        let key = self.0;
66        let ZoomLevel(level) = key[0];
67        let len = level as usize;
68        for part in &self.0[0..len] {
69            write!(f, "{part:?}")?;
70        }
71        Ok(())
72    }
73}
74
75// FIXME: does Pod and Zeroable make sense?
76#[derive(
77    Ord,
78    PartialOrd,
79    Eq,
80    PartialEq,
81    Hash,
82    Copy,
83    Clone,
84    Debug,
85    Default,
86    Serialize,
87    Deserialize,
88    Pod,
89    Zeroable,
90)]
91#[repr(C)]
92pub struct ZoomLevel(u8);
93
94impl ZoomLevel {
95    pub const fn new(z: u8) -> Self {
96        ZoomLevel(z)
97    }
98    pub fn is_root(self) -> bool {
99        self.0 == 0
100    }
101}
102
103impl std::ops::Add<u8> for ZoomLevel {
104    type Output = ZoomLevel;
105
106    fn add(self, rhs: u8) -> Self::Output {
107        let zoom_level = self.0.checked_add(rhs).expect("zoom level overflowed");
108        ZoomLevel(zoom_level)
109    }
110}
111
112impl std::ops::Sub<u8> for ZoomLevel {
113    type Output = ZoomLevel;
114
115    fn sub(self, rhs: u8) -> Self::Output {
116        let zoom_level = self.0.checked_sub(rhs).expect("zoom level underflowed");
117        ZoomLevel(zoom_level)
118    }
119}
120
121impl Display for ZoomLevel {
122    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
123        write!(f, "{}", self.0)
124    }
125}
126
127impl From<u8> for ZoomLevel {
128    fn from(zoom_level: u8) -> Self {
129        ZoomLevel(zoom_level)
130    }
131}
132
133impl From<ZoomLevel> for u8 {
134    fn from(val: ZoomLevel) -> Self {
135        val.0
136    }
137}
138
139#[derive(Copy, Clone, Debug)]
140pub struct LatLon {
141    pub latitude: f64,
142    pub longitude: f64,
143}
144
145impl LatLon {
146    pub fn new(latitude: f64, longitude: f64) -> Self {
147        LatLon {
148            latitude,
149            longitude,
150        }
151    }
152
153    /// Approximate radius of the earth in meters.
154    /// Uses the WGS-84 approximation. The radius at the equator is ~6378137 and at the poles is ~6356752. https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84
155    /// 6371008.8 is one published "average radius" see https://en.wikipedia.org/wiki/Earth_radius#Mean_radius, or ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf p.4
156    const EARTH_RADIUS: f64 = 6371008.8;
157
158    /// The average circumference of the world in meters.
159
160    const EARTH_CIRCUMFRENCE: f64 = 2.0 * PI * Self::EARTH_RADIUS; // meters
161
162    /// The circumference at a line of latitude in meters.
163    fn circumference_at_latitude(&self) -> f64 {
164        Self::EARTH_CIRCUMFRENCE * (self.latitude * PI / 180.0).cos()
165    }
166
167    fn mercator_x_from_lng(&self) -> f64 {
168        (180.0 + self.longitude) / 360.0
169    }
170
171    fn mercator_y_from_lat(&self) -> f64 {
172        (180.0 - (180.0 / PI * ((PI / 4.0 + self.latitude * PI / 360.0).tan()).ln())) / 360.0
173    }
174
175    fn mercator_z_from_altitude(&self, altitude: f64) -> f64 {
176        altitude / self.circumference_at_latitude()
177    }
178}
179
180impl Default for LatLon {
181    fn default() -> Self {
182        LatLon {
183            latitude: 0.0,
184            longitude: 0.0,
185        }
186    }
187}
188
189impl Display for LatLon {
190    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
191        write!(f, "{},{}", self.latitude, self.longitude)
192    }
193}
194
195/// `Zoom` is an exponential scale that defines the zoom of the camera on the map.
196/// We can derive the `ZoomLevel` from `Zoom` by using the `[crate::coords::ZOOM_BOUNDS]`.
197#[derive(Copy, Clone, Debug)]
198pub struct Zoom(f64);
199
200impl Zoom {
201    pub fn new(zoom: f64) -> Self {
202        Zoom(zoom)
203    }
204
205    pub fn level(&self) -> f32 {
206        self.0 as f32
207    }
208}
209
210impl Zoom {
211    pub fn from(zoom_level: ZoomLevel) -> Self {
212        Zoom(zoom_level.0 as f64)
213    }
214}
215
216impl Default for Zoom {
217    fn default() -> Self {
218        Zoom(0.0)
219    }
220}
221
222impl Display for Zoom {
223    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
224        write!(f, "{}", (self.0 * 100.0).round() / 100.0)
225    }
226}
227
228impl std::ops::Add for Zoom {
229    type Output = Zoom;
230
231    fn add(self, rhs: Self) -> Self::Output {
232        Zoom(self.0 + rhs.0)
233    }
234}
235
236impl std::ops::Sub for Zoom {
237    type Output = Zoom;
238
239    fn sub(self, rhs: Self) -> Self::Output {
240        Zoom(self.0 - rhs.0)
241    }
242}
243
244impl Zoom {
245    pub fn scale_to_tile(&self, coords: &WorldTileCoords) -> f64 {
246        2.0_f64.powf(coords.z.0 as f64 - self.0)
247    }
248
249    pub fn scale_to_zoom_level(&self, z: ZoomLevel) -> f64 {
250        2.0_f64.powf(z.0 as f64 - self.0)
251    }
252
253    pub fn scale_delta(&self, zoom: &Zoom) -> f64 {
254        2.0_f64.powf(zoom.0 - self.0)
255    }
256
257    /// Adopted from
258    /// [Transform::coveringZoomLevel](https://github.com/maplibre/maplibre-gl-js/blob/80e232a64716779bfff841dbc18fddc1f51535ad/src/geo/transform.ts#L279-L288)
259    ///
260    /// This function calculates which ZoomLevel to show at this zoom.
261    ///
262    /// The `tile_size` is the size of the tile like specified in the source definition,
263    /// For example raster tiles can be 512px or 256px. If it is 256px, then 2x as many tiles are
264    /// displayed. If the raster tile is 512px then exactly as many raster tiles like vector
265    /// tiles would be displayed.
266    pub fn zoom_level(&self, tile_size: f64) -> ZoomLevel {
267        // TODO: Also support round() instead of floor() here
268        let z = (self.0 + (TILE_SIZE / tile_size).ln() / 2.0_f64.ln()).floor() as u8;
269        return ZoomLevel(z.max(0));
270    }
271}
272
273impl SignificantlyDifferent for Zoom {
274    type Epsilon = f64;
275
276    fn ne(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
277        self.0.abs_diff_eq(&other.0, epsilon)
278    }
279}
280
281/// Within each tile there is a separate coordinate system. Usually this coordinate system is
282/// within [`EXTENT`]. Therefore, `x` and `y` must be within the bounds of [`EXTENT`].
283///
284/// # Coordinate System Origin
285///
286/// The origin is in the upper-left corner.
287#[derive(Clone, Copy, Debug, PartialEq, Default)]
288pub struct InnerCoords {
289    pub x: f64,
290    pub y: f64,
291}
292
293/// Every tile has tile coordinates. These tile coordinates are also called
294/// [Slippy map tile names](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames).
295///
296/// # Coordinate System Origin
297///
298/// For Web Mercator the origin of the coordinate system is in the upper-left corner.
299#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Default)]
300pub struct TileCoords {
301    pub x: u32,
302    pub y: u32,
303    pub z: ZoomLevel,
304}
305
306impl TileCoords {
307    /// Transforms the tile coordinates as defined by the tile grid addressing scheme into a
308    /// representation which is used in the 3d-world.
309    /// This is not possible if the coordinates of this [`TileCoords`] exceed their bounds.
310    ///
311    /// # Example
312    /// The [`TileCoords`] `T(x=5,y=5,z=0)` exceeds its bounds because there is no tile
313    /// `x=5,y=5` at zoom level `z=0`.
314    pub fn into_world_tile(self, scheme: TileAddressingScheme) -> Option<WorldTileCoords> {
315        // FIXME: MAX_ZOOM is 32, which means max bound is 2^32, which wouldn't fit in u32 or i32
316        // Note that unlike WorldTileCoords, values are signed (no idea why)
317        let bounds = ZOOM_BOUNDS[self.z.0 as usize] as i32;
318        let x = self.x as i32;
319        let y = self.y as i32;
320
321        if x >= bounds || y >= bounds {
322            return None;
323        }
324
325        Some(match scheme {
326            TileAddressingScheme::XYZ => WorldTileCoords { x, y, z: self.z },
327            TileAddressingScheme::TMS => WorldTileCoords {
328                x,
329                y: bounds - 1 - y,
330                z: self.z,
331            },
332        })
333    }
334}
335
336impl From<(u32, u32, ZoomLevel)> for TileCoords {
337    fn from(tuple: (u32, u32, ZoomLevel)) -> Self {
338        TileCoords {
339            x: tuple.0,
340            y: tuple.1,
341            z: tuple.2,
342        }
343    }
344}
345
346/// Every tile has tile coordinates. Every tile coordinate can be mapped to a coordinate within
347/// the world. This provides the freedom to map from [TMS](https://wiki.openstreetmap.org/wiki/TMS)
348/// to [Slippy map tile names](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames).
349///
350/// # Coordinate System Origin
351///
352/// The origin of the coordinate system is in the upper-left corner.
353// FIXME: does Zeroable make sense?
354#[derive(
355    Clone,
356    Copy,
357    Debug,
358    PartialEq,
359    Eq,
360    PartialOrd,
361    Ord,
362    Hash,
363    Default,
364    Serialize,
365    Deserialize,
366    Zeroable,
367)]
368#[repr(C)]
369pub struct WorldTileCoords {
370    pub x: i32,
371    pub y: i32,
372    pub z: ZoomLevel,
373}
374
375impl WorldTileCoords {
376    /// Returns the tile coords according to an addressing scheme. This is not possible if the
377    /// coordinates of this [`WorldTileCoords`] exceed their bounds.
378    ///
379    /// # Example
380    ///
381    /// The [`WorldTileCoords`] `WT(x=5,y=5,z=0)` exceeds its bounds because there is no tile
382    /// `x=5,y=5` at zoom level `z=0`.
383    pub fn into_tile(self, scheme: TileAddressingScheme) -> Option<TileCoords> {
384        // FIXME: MAX_ZOOM is 32, which means max bound is 2^32, which wouldn't fit in u32 or i32
385        let bounds = ZOOM_BOUNDS[self.z.0 as usize];
386        let x = self.x as u32;
387        let y = self.y as u32;
388
389        if x >= bounds || y >= bounds {
390            return None;
391        }
392
393        Some(match scheme {
394            TileAddressingScheme::XYZ => TileCoords { x, y, z: self.z },
395            TileAddressingScheme::TMS => TileCoords {
396                x,
397                y: bounds - 1 - y,
398                z: self.z,
399            },
400        })
401    }
402
403    /// Adopted from
404    /// [Transform::calculatePosMatrix](https://github.com/maplibre/maplibre-gl-js/blob/80e232a64716779bfff841dbc18fddc1f51535ad/src/geo/transform.ts#L719-L731)
405    #[tracing::instrument(skip_all)]
406    pub fn transform_for_zoom(&self, zoom: Zoom) -> Matrix4<f64> {
407        /*
408           For tile.z = zoom:
409               => scale = 512
410           If tile.z < zoom:
411               => scale > 512
412           If tile.z > zoom:
413               => scale < 512
414        */
415        let tile_scale = TILE_SIZE * Zoom::from(self.z).scale_delta(&zoom);
416
417        let translate = Matrix4::from_translation(Vector3::new(
418            self.x as f64 * tile_scale,
419            self.y as f64 * tile_scale,
420            0.0,
421        ));
422
423        // Divide by EXTENT to normalize tile
424        // Scale tiles where zoom level = self.z to 512x512
425        let normalize_and_scale =
426            Matrix4::from_nonuniform_scale(tile_scale / EXTENT, tile_scale / EXTENT, 1.0);
427        translate * normalize_and_scale
428    }
429
430    pub fn into_aligned(self) -> AlignedWorldTileCoords {
431        AlignedWorldTileCoords(WorldTileCoords {
432            x: div_floor(self.x, 2) * 2,
433            y: div_floor(self.y, 2) * 2,
434            z: self.z,
435        })
436    }
437
438    /// Adopted from [tilebelt](https://github.com/mapbox/tilebelt)
439    pub fn build_quad_key(&self) -> Option<Quadkey> {
440        let bounds = ZOOM_BOUNDS[self.z.0 as usize];
441        let x = self.x as u32;
442        let y = self.y as u32;
443
444        if x >= bounds || y >= bounds {
445            return None;
446        }
447
448        let mut key = [ZoomLevel::default(); MAX_ZOOM];
449
450        key[0] = self.z;
451
452        for z in 1..self.z.0 + 1 {
453            let mut b = 0;
454            let mask: i32 = 1 << (z - 1);
455            if (self.x & mask) != 0 {
456                b += 1u8;
457            }
458            if (self.y & mask) != 0 {
459                b += 2u8;
460            }
461            key[z as usize] = ZoomLevel::from(b);
462        }
463        Some(Quadkey(key))
464    }
465
466    /// Adopted from [tilebelt](https://github.com/mapbox/tilebelt)
467    pub fn get_children(&self) -> [WorldTileCoords; 4] {
468        [
469            WorldTileCoords {
470                x: self.x * 2,
471                y: self.y * 2,
472                z: self.z + 1,
473            },
474            WorldTileCoords {
475                x: self.x * 2 + 1,
476                y: self.y * 2,
477                z: self.z + 1,
478            },
479            WorldTileCoords {
480                x: self.x * 2 + 1,
481                y: self.y * 2 + 1,
482                z: self.z + 1,
483            },
484            WorldTileCoords {
485                x: self.x * 2,
486                y: self.y * 2 + 1,
487                z: self.z + 1,
488            },
489        ]
490    }
491
492    /// Get the tile which is one zoom level lower and contains this one
493    pub fn get_parent(&self) -> Option<WorldTileCoords> {
494        if self.z.is_root() {
495            return None;
496        }
497
498        Some(WorldTileCoords {
499            x: self.x >> 1,
500            y: self.y >> 1,
501            z: self.z - 1,
502        })
503    }
504
505    /// Returns unique stencil reference values for WorldTileCoords which are 3D.
506    /// Tiles from arbitrary `z` can lie next to each other, because we mix tiles from
507    /// different levels based on availability.
508    pub fn stencil_reference_value_3d(&self) -> u8 {
509        const CASES: u8 = 4;
510        let z = u8::from(self.z);
511        match (self.x % 2 == 0, self.y % 2 == 0) {
512            (true, true) => z * CASES,
513            (true, false) => 1 + z * CASES,
514            (false, true) => 2 + z * CASES,
515            (false, false) => 3 + z * CASES,
516        }
517    }
518}
519
520impl From<(i32, i32, ZoomLevel)> for WorldTileCoords {
521    fn from(tuple: (i32, i32, ZoomLevel)) -> Self {
522        WorldTileCoords {
523            x: tuple.0,
524            y: tuple.1,
525            z: tuple.2,
526        }
527    }
528}
529
530/// An aligned world tile coordinate aligns a world coordinate at a 4x4 tile raster within the
531/// world. The aligned coordinates is defined by the coordinates of the upper left tile in the 4x4
532/// tile raster divided by 2 and rounding to the ceiling.
533///
534///
535/// # Coordinate System Origin
536///
537/// The origin of the coordinate system is in the upper-left corner.
538pub struct AlignedWorldTileCoords(pub WorldTileCoords);
539
540impl AlignedWorldTileCoords {
541    pub fn upper_left(self) -> WorldTileCoords {
542        self.0
543    }
544
545    pub fn upper_right(&self) -> WorldTileCoords {
546        WorldTileCoords {
547            x: self.0.x + 1,
548            y: self.0.y,
549            z: self.0.z,
550        }
551    }
552
553    pub fn lower_left(&self) -> WorldTileCoords {
554        WorldTileCoords {
555            x: self.0.x,
556            y: self.0.y - 1,
557            z: self.0.z,
558        }
559    }
560
561    pub fn lower_right(&self) -> WorldTileCoords {
562        WorldTileCoords {
563            x: self.0.x + 1,
564            y: self.0.y + 1,
565            z: self.0.z,
566        }
567    }
568}
569
570/// Actual coordinates within the 3D world. The `z` value of the [`WorldCoors`] is not related to
571/// the `z` value of the [`WorldTileCoors`]. In the 3D world all tiles are rendered at `z` values
572/// which are determined only by the render engine and not by the zoom level.
573///
574/// # Coordinate System Origin
575///
576/// The origin of the coordinate system is in the upper-left corner.
577#[derive(Clone, Copy, Debug, PartialEq, Default)]
578pub struct WorldCoords {
579    pub x: f64,
580    pub y: f64,
581}
582
583impl WorldCoords {
584    pub fn from_lat_lon(lat_lon: LatLon, zoom: Zoom) -> WorldCoords {
585        let tile_size = TILE_SIZE * 2.0_f64.powf(zoom.0);
586        // Get x value
587        let x = (lat_lon.longitude + 180.0) * (tile_size / 360.0);
588
589        // Convert from degrees to radians
590        let lat_rad = (lat_lon.latitude * PI) / 180.0;
591
592        // get y value
593        let merc_n = f64::ln(f64::tan((PI / 4.0) + (lat_rad / 2.0)));
594        let y = (tile_size / 2.0) - (tile_size * merc_n / (2.0 * PI));
595
596        WorldCoords { x, y }
597    }
598
599    pub fn at_ground(x: f64, y: f64) -> Self {
600        Self { x, y }
601    }
602
603    pub fn into_world_tile(self, z: ZoomLevel, zoom: Zoom) -> WorldTileCoords {
604        let tile_scale = zoom.scale_to_zoom_level(z) / TILE_SIZE; // TODO: Deduplicate
605        let x = self.x * tile_scale;
606        let y = self.y * tile_scale;
607
608        WorldTileCoords {
609            x: x as i32,
610            y: y as i32,
611            z,
612        }
613    }
614}
615
616impl From<(f32, f32)> for WorldCoords {
617    fn from(tuple: (f32, f32)) -> Self {
618        WorldCoords {
619            x: tuple.0 as f64,
620            y: tuple.1 as f64,
621        }
622    }
623}
624
625impl From<(f64, f64)> for WorldCoords {
626    fn from(tuple: (f64, f64)) -> Self {
627        WorldCoords {
628            x: tuple.0,
629            y: tuple.1,
630        }
631    }
632}
633
634impl From<Point3<f64>> for WorldCoords {
635    fn from(point: Point3<f64>) -> Self {
636        WorldCoords {
637            x: point.x,
638            y: point.y,
639        }
640    }
641}
642
643/// Defines a bounding box on a tiled map with a [`ZoomLevel`] and a padding.
644#[derive(Debug)]
645pub struct ViewRegion {
646    min_tile: WorldTileCoords,
647    max_tile: WorldTileCoords,
648    /// At which zoom level does this region exist
649    zoom_level: ZoomLevel,
650    /// Padding around this view region
651    padding: i32,
652    /// The maximum amount of tiles this view region contains
653    max_n_tiles: usize,
654}
655
656impl ViewRegion {
657    pub fn new(
658        view_region: Aabb2<f64>,
659        padding: i32,
660        max_n_tiles: usize,
661        zoom: Zoom,
662        z: ZoomLevel,
663    ) -> Self {
664        let min_world: WorldCoords = WorldCoords::at_ground(view_region.min.x, view_region.min.y);
665        let min_world_tile: WorldTileCoords = min_world.into_world_tile(z, zoom);
666        let max_world: WorldCoords = WorldCoords::at_ground(view_region.max.x, view_region.max.y);
667        let max_world_tile: WorldTileCoords = max_world.into_world_tile(z, zoom);
668
669        Self {
670            min_tile: min_world_tile,
671            max_tile: max_world_tile,
672            zoom_level: z,
673            max_n_tiles,
674            padding,
675        }
676    }
677
678    pub fn zoom_level(&self) -> ZoomLevel {
679        self.zoom_level
680    }
681
682    pub fn is_in_view(&self, &world_coords: &WorldTileCoords) -> bool {
683        world_coords.x <= self.max_tile.x + self.padding
684            && world_coords.y <= self.max_tile.y + self.padding
685            && world_coords.x >= self.min_tile.x - self.padding
686            && world_coords.y >= self.min_tile.y - self.padding
687            && world_coords.z == self.zoom_level
688    }
689
690    pub fn iter(&self) -> impl Iterator<Item = WorldTileCoords> + '_ {
691        (self.min_tile.x - self.padding..self.max_tile.x + 1 + self.padding)
692            .flat_map(move |x| {
693                (self.min_tile.y - self.padding..self.max_tile.y + 1 + self.padding).map(move |y| {
694                    let tile_coord: WorldTileCoords = (x, y, self.zoom_level).into();
695                    tile_coord
696                })
697            })
698            .take(self.max_n_tiles)
699    }
700}
701
702impl Display for TileCoords {
703    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
704        write!(
705            f,
706            "T(x={x},y={y},z={z})",
707            x = self.x,
708            y = self.y,
709            z = self.z
710        )
711    }
712}
713
714impl Display for WorldTileCoords {
715    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
716        write!(
717            f,
718            "WT(x={x},y={y},z={z})",
719            x = self.x,
720            y = self.y,
721            z = self.z
722        )
723    }
724}
725impl Display for WorldCoords {
726    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
727        write!(f, "W(x={x},y={y})", x = self.x, y = self.y,)
728    }
729}
730
731#[cfg(test)]
732mod tests {
733    use cgmath::Point2;
734
735    use crate::{
736        coords::{
737            Quadkey, TileCoords, ViewRegion, WorldCoords, WorldTileCoords, Zoom, ZoomLevel,
738            BOTTOM_RIGHT_EXTENT, TOP_LEFT_EXTENT,
739        },
740        render::tile_view_pattern::DEFAULT_TILE_SIZE,
741        style::source::TileAddressingScheme,
742        util::math::Aabb2,
743    };
744
745    fn to_from_world(tile: (i32, i32, ZoomLevel), zoom: Zoom) {
746        let tile = WorldTileCoords::from(tile);
747        let p1 = tile.transform_for_zoom(zoom) * TOP_LEFT_EXTENT;
748        let p2 = tile.transform_for_zoom(zoom) * BOTTOM_RIGHT_EXTENT;
749        println!("{p1:?}\n{p2:?}");
750
751        assert_eq!(
752            WorldCoords::from((p1.x, p1.y))
753                .into_world_tile(zoom.zoom_level(DEFAULT_TILE_SIZE), zoom),
754            tile
755        );
756    }
757
758    #[test]
759    fn world_coords_tests() {
760        to_from_world((1, 0, ZoomLevel::from(1)), Zoom::new(1.0));
761        to_from_world((67, 42, ZoomLevel::from(7)), Zoom::new(7.0));
762        to_from_world((17421, 11360, ZoomLevel::from(15)), Zoom::new(15.0));
763    }
764
765    #[test]
766    fn test_quad_key() {
767        assert_eq!(
768            TileCoords {
769                x: 0,
770                y: 0,
771                z: ZoomLevel::from(1)
772            }
773            .into_world_tile(TileAddressingScheme::TMS)
774            .unwrap()
775            .build_quad_key(),
776            Some(Quadkey::new(&[ZoomLevel::from(2)]))
777        );
778        assert_eq!(
779            TileCoords {
780                x: 0,
781                y: 1,
782                z: ZoomLevel::from(1)
783            }
784            .into_world_tile(TileAddressingScheme::TMS)
785            .unwrap()
786            .build_quad_key(),
787            Some(Quadkey::new(&[ZoomLevel::from(0)]))
788        );
789        assert_eq!(
790            TileCoords {
791                x: 1,
792                y: 1,
793                z: ZoomLevel::from(1)
794            }
795            .into_world_tile(TileAddressingScheme::TMS)
796            .unwrap()
797            .build_quad_key(),
798            Some(Quadkey::new(&[ZoomLevel::from(1)]))
799        );
800        assert_eq!(
801            TileCoords {
802                x: 1,
803                y: 0,
804                z: ZoomLevel::from(1)
805            }
806            .into_world_tile(TileAddressingScheme::TMS)
807            .unwrap()
808            .build_quad_key(),
809            Some(Quadkey::new(&[ZoomLevel::from(3)]))
810        );
811    }
812
813    #[test]
814    fn test_view_region() {
815        for tile_coords in ViewRegion::new(
816            Aabb2::new(Point2::new(0.0, 0.0), Point2::new(2000.0, 2000.0)),
817            1,
818            32,
819            Zoom::default(),
820            ZoomLevel::default(),
821        )
822        .iter()
823        {
824            println!("{tile_coords}");
825        }
826    }
827}