maptiler_cloud/
lib.rs

1use std::{fmt::Display, sync::Arc};
2
3/// Rust bindings for the [Maptiler Cloud API](https://cloud.maptiler.com/maps/)
4///
5/// The Maptiler Cloud API allows for simple access to images that allow people
6/// to make simple maps using satellite imagery, contour maps, or street maps.
7///
8/// # Example
9///
10/// ```no_run
11/// #[tokio::main]
12/// async fn main() {
13///     // Create a new Maptiler Cloud session
14///     // Use your own API key from Maptiler Cloud
15///     let maptiler = maptiler_cloud::Maptiler::new("placeholder api key").unwrap();
16///
17///     // Create a new tile request
18///     let x = 2;
19///     let y = 1;
20///     let zoom = 2;
21///
22///     let tile_request = maptiler_cloud::TileRequest::new(
23///         maptiler_cloud::TileSet::Satellite,
24///         x,
25///         y,
26///         zoom
27///     ).unwrap();
28///
29///     // Create the request using the Maptiler session
30///     let constructed = maptiler.create_request(tile_request);
31///
32///     // Actually perform the request to get the data
33///     let satellite_jpg = constructed.execute().await.unwrap();
34///
35///     // Check for JPEG file magic to make sure we got an image
36///     assert_eq!(&satellite_jpg[0..3], &[0xFF, 0xD8, 0xFF]);
37/// }
38/// ```
39///
40/// From there, most users will write those bytes to a file, or load them into another function
41/// that will be able to display the image from the raw JPEG bytes.
42///
43pub mod errors;
44
45/// The different types of tilesets that Maptiler Cloud supports
46#[derive(Debug, Copy, Clone, PartialEq, Eq)]
47pub enum TileSet {
48    /// A contour map of the world
49    /// Bytes returned will be a .pbf file
50    Contours,
51    /// A (beta) map of the countries of the world
52    /// Bytes returned will be a .pbf file
53    Countries,
54    /// Shows hills as a transparent shaded relief
55    /// Bytes returned will be a .png file
56    Hillshading,
57    /// A map of land vs. not land
58    /// Bytes returned will be a .pbf file
59    Land,
60    /// Land cover which stores what kinds of plants grow in specific areas
61    /// Bytes returned will be a .pbf file
62    Landcover,
63    /// General purpose map format
64    /// Bytes returned will be a .pbf file
65    MaptilerPlanet,
66    /// Like MaptilerPlanet, but with extra data in only upper-level zooms
67    /// Bytes returned will be a .pbf file
68    MaptilerPlanetLite,
69    /// OpenMapTiles format
70    /// Bytes returned will be a .pbf file
71    OpenMapTiles,
72    /// Same as OpenMapTiles, but in the WGS84 format
73    /// Bytes returned will be a .pbf file
74    OpenMapTilesWGS84,
75    /// Maps for outdoor life like hiking, cycling, etc.
76    /// Bytes returned will be a .pbf file
77    Outdoor,
78    /// Satellite images
79    /// Bytes returned will be a .jpg file
80    Satellite,
81    /// Satellite images but medium resolution from 2016
82    /// Bytes returned will be a .jpg file
83    SatelliteMediumRes2016,
84    /// Satellite images but medium resolution from 2018
85    /// Bytes returned will be a .jpg file
86    SatelliteMediumRes2018,
87    /// Contains terrain elevation data encoded into vector TIN polygons
88    /// Bytes returned will be a quantized mesh file
89    Terrain3D,
90    /// Contains terrain elevation data encoded into RGB color model
91    /// height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)
92    /// Bytes returned will be a .png file
93    TerrainRGB,
94    /// Specify your own custom TileSet
95    Custom {
96        /// The Maptiler Cloud tile endpoint, for satellite imagery: "satellite"
97        endpoint: &'static str,
98        /// The file extension that this endpoint returns, ex: "png"
99        extension: &'static str,
100    },
101}
102
103impl TileSet {
104    /// Returns the endpoint that this tileset requires on the API request
105    ///
106    /// For the satellite data tileset, the endpoint would be "satellite"
107    pub fn endpoint(&self) -> &'static str {
108        match self {
109            TileSet::Contours => "contours",
110            TileSet::Countries => "countries",
111            TileSet::Hillshading => "hillshades",
112            TileSet::Land => "land",
113            TileSet::Landcover => "landcover",
114            TileSet::MaptilerPlanet => "v3",
115            TileSet::MaptilerPlanetLite => "v3-lite",
116            TileSet::OpenMapTiles => "v3-openmaptiles",
117            TileSet::OpenMapTilesWGS84 => "v3-4326",
118            TileSet::Outdoor => "outdoor",
119            TileSet::Satellite => "satellite",
120            TileSet::SatelliteMediumRes2016 => "satellite-mediumres",
121            TileSet::SatelliteMediumRes2018 => "satellite-mediumres-2018",
122            TileSet::Terrain3D => "terrain-quantized-mesh",
123            TileSet::TerrainRGB => "terrain-rgb",
124            TileSet::Custom {
125                endpoint,
126                extension: _,
127            } => endpoint,
128        }
129    }
130
131    /// Returns the maximum zoom level that this tileset supports
132    ///
133    /// The custom tileset variant has a maximum of 20 here, but it may be lower than that. Take
134    /// care when using a custom tileset variant.
135    ///
136    pub fn max_zoom(&self) -> u32 {
137        match self {
138            TileSet::Contours => 14,
139            TileSet::Countries => 11,
140            TileSet::Hillshading => 12,
141            TileSet::Land => 14,
142            TileSet::Landcover => 9,
143            TileSet::MaptilerPlanet => 14,
144            TileSet::MaptilerPlanetLite => 10,
145            TileSet::OpenMapTiles => 14,
146            TileSet::OpenMapTilesWGS84 => 13,
147            TileSet::Outdoor => 14,
148            TileSet::Satellite => 20,
149            TileSet::SatelliteMediumRes2016 => 13,
150            TileSet::SatelliteMediumRes2018 => 13,
151            TileSet::Terrain3D => 13,
152            TileSet::TerrainRGB => 12,
153            // For the custom
154            TileSet::Custom {
155                endpoint: _,
156                extension: _,
157            } => 20,
158        }
159    }
160
161    /// Returns the minimum zoom level that this tileset supports
162    ///
163    /// The custom tileset variant has a minimum of 0 here, but it may be higher than that. Take
164    /// care when using a custom tileset variant.
165    ///
166    pub fn min_zoom(&self) -> u32 {
167        match self {
168            TileSet::Contours => 9,
169            TileSet::Countries => 0,
170            TileSet::Hillshading => 0,
171            TileSet::Land => 0,
172            TileSet::Landcover => 0,
173            TileSet::MaptilerPlanet => 0,
174            TileSet::MaptilerPlanetLite => 0,
175            TileSet::OpenMapTiles => 0,
176            TileSet::OpenMapTilesWGS84 => 0,
177            TileSet::Outdoor => 5,
178            TileSet::Satellite => 0,
179            TileSet::SatelliteMediumRes2016 => 0,
180            TileSet::SatelliteMediumRes2018 => 0,
181            TileSet::Terrain3D => 0,
182            TileSet::TerrainRGB => 0,
183            // For the custom
184            TileSet::Custom {
185                endpoint: _,
186                extension: _,
187            } => 0,
188        }
189    }
190
191    /// Returns the file extension that this tileset returns as a static &str
192    ///
193    /// Example outputs are: "png", "jpg", "pbf"
194    pub fn file_extension(&self) -> &'static str {
195        match self {
196            TileSet::Contours
197            | TileSet::Countries
198            | TileSet::Land
199            | TileSet::Landcover
200            | TileSet::MaptilerPlanet
201            | TileSet::MaptilerPlanetLite
202            | TileSet::OpenMapTiles
203            | TileSet::OpenMapTilesWGS84
204            | TileSet::Outdoor => "pbf",
205            TileSet::Hillshading | TileSet::TerrainRGB => "png",
206            TileSet::Satellite
207            | TileSet::SatelliteMediumRes2016
208            | TileSet::SatelliteMediumRes2018 => "jpg",
209            TileSet::Terrain3D => "quantized-mesh-1.0",
210            TileSet::Custom {
211                endpoint: _,
212                extension,
213            } => extension,
214        }
215    }
216}
217
218impl Display for TileSet {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        write!(
221            f,
222            "{}",
223            match self {
224                TileSet::Contours => "Contours",
225                TileSet::Countries => "Countries",
226                TileSet::Hillshading => "Hillshades",
227                TileSet::Land => "Land",
228                TileSet::Landcover => "Landcover",
229                TileSet::MaptilerPlanet => "MaptilerPlanet",
230                TileSet::MaptilerPlanetLite => "MaptilerPlanetLite",
231                TileSet::OpenMapTiles => "OpenMapTiles",
232                TileSet::OpenMapTilesWGS84 => "OpenMapTilesWGS84",
233                TileSet::Outdoor => "Outdoor",
234                TileSet::Satellite => "Satellite",
235                TileSet::SatelliteMediumRes2016 => "SatelliteMediumRes2016",
236                TileSet::SatelliteMediumRes2018 => "SatelliteMediumRes2018",
237                TileSet::Terrain3D => "Terrain3D",
238                TileSet::TerrainRGB => "TerrainRGB",
239                TileSet::Custom {
240                    endpoint,
241                    extension: _,
242                } => endpoint,
243            }
244        )
245    }
246}
247
248/// A struct containing the arguments required to make a request for a tile
249#[derive(Debug, PartialEq, Eq, Clone, Copy)]
250pub struct TileRequest {
251    set: TileSet,
252    zoom: u32,
253    tile_x: u32,
254    tile_y: u32,
255}
256
257impl TileRequest {
258    /// Creates a new TileRequest with the given parameters
259    ///
260    /// set: A TileSet representing which tileset to get the tile from. See https://cloud.maptiler.com/tiles/
261    ///
262    /// x: The x-coordinate of the tile in the [Tiled Web Map format](https://en.wikipedia.org/wiki/Tiled_web_map)
263    /// y: The y-coordinate of the tile
264    /// zoom: The zoom level of the tile in the Tile Web Map format
265    ///
266    /// The x and y positions must be in bounds
267    ///
268    pub fn new(set: TileSet, x: u32, y: u32, zoom: u32) -> Result<Self, errors::ArgumentError> {
269        // Check if the zoom is valid
270        if zoom > set.max_zoom() {
271            return Err(errors::ArgumentError::ZoomTooLarge(
272                zoom,
273                set,
274                set.max_zoom(),
275            ));
276        } else if zoom < set.min_zoom() {
277            return Err(errors::ArgumentError::ZoomTooSmall(
278                zoom,
279                set,
280                set.min_zoom(),
281            ));
282        }
283
284        // Check if the coordinates are valid
285        let max_coordinate = Self::max_coordinate_with_zoom(zoom);
286
287        if x > max_coordinate {
288            return Err(errors::ArgumentError::XTooLarge(x, zoom, max_coordinate));
289        }
290
291        if y > max_coordinate {
292            return Err(errors::ArgumentError::YTooLarge(y, zoom, max_coordinate));
293        }
294
295        Ok(Self {
296            set,
297            zoom,
298            tile_x: x,
299            tile_y: y,
300        })
301    }
302
303    // Calculates the maximum x or y coordinate for a given zoom level
304    fn max_coordinate_with_zoom(zoom: u32) -> u32 {
305        // This special case is if zoom == 0
306        //
307        // Then there is only one tile, so the max x and y are 0
308        if zoom == 0 {
309            0
310        } else {
311            // This does 2^zoom level
312            //
313            // zoom = 0:
314            //      2^0 = 1
315            // zoom = 1:
316            //      2^1 = 2
317
318            1 << zoom
319        }
320    }
321
322    /// Returns the x coordinate of this tile request
323    pub fn x(&self) -> u32 {
324        self.tile_x
325    }
326
327    /// Returns the y coordinate of this tile request
328    pub fn y(&self) -> u32 {
329        self.tile_y
330    }
331
332    /// Returns the zoom level of this tile request
333    pub fn zoom(&self) -> u32 {
334        self.zoom
335    }
336}
337
338impl From<TileRequest> for RequestType {
339    fn from(tile_request: TileRequest) -> Self {
340        RequestType::TileRequest(tile_request)
341    }
342}
343
344/// The type of request to the Maptiler Cloud API
345#[derive(Debug, Copy, Clone)]
346pub enum RequestType {
347    TileRequest(TileRequest),
348}
349
350/// Represents a request that has already been constructed using the Maptiler that created it. This
351/// can be directly await-ed using execute()
352#[derive(Debug, Clone)]
353pub struct ConstructedRequest {
354    api_key: Arc<String>,
355    inner: RequestType,
356    client: Arc<reqwest::Client>,
357}
358
359impl ConstructedRequest {
360    /// Actually performs the API call to the Maptiler Cloud API
361    pub async fn execute(&self) -> Result<Vec<u8>, errors::Error> {
362        match self.inner {
363            RequestType::TileRequest(tile_request) => self.execute_tile(tile_request).await,
364        }
365    }
366
367    async fn execute_tile(&self, tile_request: TileRequest) -> Result<Vec<u8>, errors::Error> {
368        let tileset = &tile_request.set;
369        let endpoint = tileset.endpoint();
370        let extension = tileset.file_extension();
371        let zoom = tile_request.zoom;
372        let x = tile_request.tile_x;
373        let y = tile_request.tile_y;
374
375        // https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=AAAAAAAAAAAAAAAAAA
376        let url = format!(
377            "https://api.maptiler.com/tiles/{}/{}/{}/{}.{}?key={}",
378            endpoint, zoom, x, y, extension, &self.api_key
379        );
380
381        // Perform the actual request
382        let res = self.client.get(url).send().await?;
383
384        match res.status() {
385            reqwest::StatusCode::OK => Ok(res.bytes().await?.to_vec()),
386            status => Err(errors::Error::Http(status)),
387        }
388    }
389}
390
391/// A struct that serves as a Maptiler "session", which stores the API key and is used to create
392/// requests
393pub struct Maptiler {
394    api_key: Arc<String>,
395    client: Arc<reqwest::Client>,
396}
397
398impl Maptiler {
399    /// Initializes this Maptiler Cloud API session
400    pub fn new<S>(api_key: S) -> Result<Self, errors::Error>
401    where
402        S: Into<String>,
403    {
404        Ok(Self {
405            api_key: Arc::new(api_key.into()),
406            client: Arc::new(reqwest::Client::builder().build()?),
407        })
408    }
409
410    /// Initializes this Maptiler Cloud API session, with a user provided [`reqwest::Client`]
411    pub fn new_with_client<S>(
412        api_key: S,
413        client: Arc<reqwest::Client>,
414    ) -> Result<Self, errors::Error>
415    where
416        S: Into<String>,
417    {
418        Ok(Self {
419            api_key: Arc::new(api_key.into()),
420            client,
421        })
422    }
423
424    /// Performs a generic request to the Maptiler Cloud API
425    ///
426    /// This may be a little simpler to use so that any type of request can be passed into this
427    /// function
428    ///
429    pub fn create_request(&self, request: impl Into<RequestType>) -> ConstructedRequest {
430        ConstructedRequest {
431            api_key: Arc::clone(&self.api_key),
432            inner: request.into(),
433            client: self.client.clone(),
434        }
435    }
436
437    /// Performs a tile request to the Maptiler Cloud API
438    pub fn create_tile_request(&self, tile_request: TileRequest) -> ConstructedRequest {
439        ConstructedRequest {
440            api_key: Arc::clone(&self.api_key),
441            inner: RequestType::TileRequest(tile_request),
442            client: self.client.clone(),
443        }
444    }
445}