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}