morecantile 3.3.0__py3-none-any.whl → 4.0.0a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
morecantile/errors.py CHANGED
@@ -31,3 +31,7 @@ class QuadKeyError(MorecantileError):
31
31
 
32
32
  class InvalidZoomError(MorecantileError):
33
33
  """Raised when input zoom is invalid."""
34
+
35
+
36
+ class DeprecationError(MorecantileError):
37
+ """Raised when TMS version is not 2.0"""
morecantile/models.py CHANGED
@@ -2,17 +2,18 @@
2
2
 
3
3
  import math
4
4
  import warnings
5
- from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
5
+ from typing import Any, Dict, Iterator, List, Literal, Optional, Sequence, Tuple, Union
6
6
 
7
+ from cachetools import LRUCache, cached
8
+ from cachetools.keys import hashkey
7
9
  from pydantic import AnyHttpUrl, BaseModel, Field, PrivateAttr, validator
8
10
  from pyproj import CRS, Transformer
9
- from pyproj.enums import WktVersion
10
11
  from pyproj.exceptions import ProjError
11
12
 
12
13
  from morecantile.commons import BoundingBox, Coords, Tile
13
14
  from morecantile.errors import (
15
+ DeprecationError,
14
16
  InvalidZoomError,
15
- MorecantileError,
16
17
  NoQuadkeySupport,
17
18
  PointOutsideTMSBounds,
18
19
  QuadKeyError,
@@ -23,6 +24,7 @@ from morecantile.utils import (
23
24
  check_quadkey_support,
24
25
  meters_per_unit,
25
26
  point_in_bbox,
27
+ to_rasterio_crs,
26
28
  )
27
29
 
28
30
  NumType = Union[float, int]
@@ -44,7 +46,8 @@ class CRSType(CRS, str):
44
46
  @classmethod
45
47
  def validate(cls, value: Union[CRS, str]) -> CRS:
46
48
  """Validate CRS."""
47
- # If input is a string we tranlate it to CRS
49
+ # If input is a string we translate it to CRS
50
+ # TODO: add NotImplementedError for ISO 19115
48
51
  if not isinstance(value, CRS):
49
52
  return CRS.from_user_input(value)
50
53
 
@@ -90,6 +93,11 @@ def crs_axis_inverted(crs: CRS) -> bool:
90
93
  return crs.axis_info[0].abbrev.upper() in ["Y", "LAT", "N"]
91
94
 
92
95
 
96
+ def ordered_axis_inverted(ordered_axes: List[str]) -> bool:
97
+ """Check if ordered axes have inverted AXIS (lat,lon) instead of (lon,lat)."""
98
+ return ordered_axes[0].upper() in ["Y", "LAT", "N"]
99
+
100
+
93
101
  class TMSBoundingBox(BaseModel):
94
102
  """Bounding box"""
95
103
 
@@ -105,44 +113,124 @@ class TMSBoundingBox(BaseModel):
105
113
  json_encoders = {CRS: lambda v: CRS_to_uri(v)}
106
114
 
107
115
 
116
+ # class variableMatrixWidth(BaseModel):
117
+ # """Variable Matrix Width Definition
118
+
119
+
120
+ # ref: https://github.com/opengeospatial/2D-Tile-Matrix-Set/blob/master/schemas/tms/2.0/json/variableMatrixWidth.json
121
+ # """
122
+
123
+ # coalesce: int = Field(..., ge=2, multiple_of=1, description="Number of tiles in width that coalesce in a single tile for these rows")
124
+ # minTileRow: int = Field(..., ge=0, multiple_of=1, description="First tile row where the coalescence factor applies for this tilematrix")
125
+ # maxTileRow: int = Field(..., ge=0, multiple_of=1, description="Last tile row where the coalescence factor applies for this tilematrix")
126
+
127
+
108
128
  class TileMatrix(BaseModel):
109
- """Tile matrix"""
110
-
111
- type: str = Field("TileMatrixType", const=True)
112
- title: Optional[str]
113
- abstract: Optional[str]
114
- keywords: Optional[List[str]]
115
- identifier: str = Field(..., regex=r"^[0-9]+$")
116
- scaleDenominator: float
117
- topLeftCorner: BoundsType
118
- tileWidth: int
119
- tileHeight: int
120
- matrixWidth: int
121
- matrixHeight: int
129
+ """Tile Matrix Definition
130
+
131
+ A tile matrix, usually corresponding to a particular zoom level of a TileMatrixSet.
132
+
133
+ ref: https://github.com/opengeospatial/2D-Tile-Matrix-Set/blob/master/schemas/tms/2.0/json/tileMatrix.json
134
+ """
135
+
136
+ title: Optional[str] = Field(
137
+ description="Title of this tile matrix, normally used for display to a human"
138
+ )
139
+ description: Optional[str] = Field(
140
+ description="Brief narrative description of this tile matrix set, normally available for display to a human"
141
+ )
142
+ keywords: Optional[List[str]] = Field(
143
+ description="Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this dataset"
144
+ )
145
+ id: str = Field(
146
+ ...,
147
+ regex=r"^[0-9]+$",
148
+ description="Identifier selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile. Implementation of 'identifier'",
149
+ )
150
+ scaleDenominator: float = Field(
151
+ ..., description="Scale denominator of this tile matrix"
152
+ )
153
+ cellSize: float = Field(..., description="Cell size of this tile matrix")
154
+ cornerOfOrigin: Optional[Literal["topLeft", "bottomLeft"]] = Field(
155
+ description="The corner of the tile matrix (_topLeft_ or _bottomLeft_) used as the origin for numbering tile rows and columns. This corner is also a corner of the (0, 0) tile."
156
+ )
157
+ pointOfOrigin: BoundsType = Field(
158
+ ...,
159
+ description="Precise position in CRS coordinates of the corner of origin (e.g. the top-left corner) for this tile matrix. This position is also a corner of the (0, 0) tile. In previous version, this was 'topLeftCorner' and 'cornerOfOrigin' did not exist.",
160
+ )
161
+ tileWidth: int = Field(
162
+ ...,
163
+ ge=1,
164
+ multiple_of=1,
165
+ description="Width of each tile of this tile matrix in pixels",
166
+ )
167
+ tileHeight: int = Field(
168
+ ...,
169
+ ge=1,
170
+ multiple_of=1,
171
+ description="Height of each tile of this tile matrix in pixels",
172
+ )
173
+ matrixWidth: int = Field(
174
+ ...,
175
+ ge=1,
176
+ multiple_of=1,
177
+ description="Width of the matrix (number of tiles in width)",
178
+ )
179
+ matrixHeight: int = Field(
180
+ ...,
181
+ ge=1,
182
+ multiple_of=1,
183
+ description="Height of the matrix (number of tiles in height)",
184
+ )
185
+ # variableMatrixWidths: Optional[List[variableMatrixWidth]] = Field(description="Describes the rows that has variable matrix width")
122
186
 
123
187
  class Config:
124
- """Forbid additional items like variableMatrixWidth."""
188
+ """Forbid additional items like variableMatrixWidths."""
125
189
 
126
190
  extra = "forbid"
127
191
 
128
192
 
129
193
  class TileMatrixSet(BaseModel):
130
- """Tile matrix set"""
131
-
132
- type: str = Field("TileMatrixSetType", const=True)
133
- title: str
134
- abstract: Optional[str]
135
- keywords: Optional[List[str]]
136
- identifier: str = Field(..., regex=r"^[\w\d_\-]+$")
137
- supportedCRS: CRSType
138
- wellKnownScaleSet: Optional[AnyHttpUrl] = None
139
- boundingBox: Optional[TMSBoundingBox]
140
- tileMatrix: List[TileMatrix]
194
+ """Tile Matrix Set Definition
195
+
196
+ A definition of a tile matrix set following the Tile Matrix Set standard.
197
+ For tileset metadata, such a description (in `tileMatrixSet` property) is only required for offline use,
198
+ as an alternative to a link with a `http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme` relation type.
199
+
200
+ ref: https://github.com/opengeospatial/2D-Tile-Matrix-Set/blob/master/schemas/tms/2.0/json/tileMatrixSet.json
201
+
202
+ """
203
+
204
+ title: Optional[str] = Field(
205
+ description="Title of this tile matrix set, normally used for display to a human"
206
+ )
207
+ description: Optional[str] = Field(
208
+ description="Brief narrative description of this tile matrix set, normally available for display to a human"
209
+ )
210
+ keywords: Optional[List[str]] = Field(
211
+ description="Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this tile matrix set"
212
+ )
213
+ id: Optional[str] = Field(
214
+ regex=r"^[\w\d_\-]+$",
215
+ description="Tile matrix set identifier. Implementation of 'identifier'",
216
+ )
217
+ uri: Optional[str] = Field(
218
+ description="Reference to an official source for this tileMatrixSet"
219
+ )
220
+ orderedAxes: Optional[List[str]]
221
+ crs: CRSType = Field(..., description="Coordinate Reference System (CRS)")
222
+ wellKnownScaleSet: Optional[AnyHttpUrl] = Field(
223
+ description="Reference to a well-known scale set"
224
+ )
225
+ boundingBox: Optional[TMSBoundingBox] = Field(
226
+ description="Minimum bounding rectangle surrounding the tile matrix set, in the supported CRS"
227
+ )
228
+ tileMatrices: List[TileMatrix] = Field(
229
+ ..., description="Describes scale levels and its tile matrices"
230
+ )
141
231
 
142
232
  # Private attributes
143
233
  _is_quadtree: bool = PrivateAttr()
144
-
145
- # CRS transformation attributes
146
234
  _geographic_crs: CRSType = PrivateAttr(default=WGS84_CRS)
147
235
  _to_geographic: Transformer = PrivateAttr()
148
236
  _from_geographic: Transformer = PrivateAttr()
@@ -155,18 +243,23 @@ class TileMatrixSet(BaseModel):
155
243
 
156
244
  def __init__(self, **data):
157
245
  """Create PyProj transforms and check if TileMatrixSet supports quadkeys."""
246
+ if {"supportedCRS", "topLeftCorner"}.intersection(data):
247
+ raise DeprecationError(
248
+ "Tile Matrix Set must be version 2.0. Use morecantile <4.0 for TMS 1.0 support"
249
+ )
250
+
158
251
  super().__init__(**data)
159
252
 
160
- self._is_quadtree = check_quadkey_support(self.tileMatrix)
253
+ self._is_quadtree = check_quadkey_support(self.tileMatrices)
161
254
 
162
255
  self._geographic_crs = data.get("_geographic_crs", WGS84_CRS)
163
256
 
164
257
  try:
165
258
  self._to_geographic = Transformer.from_crs(
166
- self.supportedCRS, self._geographic_crs, always_xy=True
259
+ self.crs, self._geographic_crs, always_xy=True
167
260
  )
168
261
  self._from_geographic = Transformer.from_crs(
169
- self._geographic_crs, self.supportedCRS, always_xy=True
262
+ self._geographic_crs, self.crs, always_xy=True
170
263
  )
171
264
  except ProjError:
172
265
  warnings.warn(
@@ -177,51 +270,103 @@ class TileMatrixSet(BaseModel):
177
270
  self._to_geographic = None
178
271
  self._from_geographic = None
179
272
 
180
- @validator("tileMatrix")
273
+ @validator("tileMatrices")
181
274
  def sort_tile_matrices(cls, v):
182
275
  """Sort matrices by identifier"""
183
- return sorted(v, key=lambda m: int(m.identifier))
276
+ return sorted(v, key=lambda m: int(m.id))
184
277
 
185
278
  def __iter__(self):
186
279
  """Iterate over matrices"""
187
- for matrix in self.tileMatrix:
280
+ for matrix in self.tileMatrices:
188
281
  yield matrix
189
282
 
190
283
  def __repr__(self):
191
284
  """Simplify default pydantic model repr."""
192
- return f"<TileMatrixSet title='{self.title}' identifier='{self.identifier}'>"
285
+ return f"<TileMatrixSet title='{self.title}' id='{self.id}'>"
193
286
 
194
287
  @property
195
- def crs(self) -> CRS:
196
- """Fetch CRS from epsg"""
197
- return self.supportedCRS
288
+ def geographic_crs(self) -> CRSType:
289
+ """Return the TMS's geographic CRS."""
290
+ return self._geographic_crs
198
291
 
199
292
  @property
200
293
  def rasterio_crs(self):
201
294
  """Return rasterio CRS."""
295
+ return to_rasterio_crs(self.crs)
202
296
 
203
- import rasterio
204
- from rasterio.env import GDALVersion
205
-
206
- if GDALVersion.runtime().major < 3:
207
- return rasterio.crs.CRS.from_wkt(self.crs.to_wkt(WktVersion.WKT1_GDAL))
208
- else:
209
- return rasterio.crs.CRS.from_wkt(self.crs.to_wkt())
297
+ @property
298
+ def rasterio_geographic_crs(self):
299
+ """Return the geographic CRS as a rasterio CRS."""
300
+ return to_rasterio_crs(self._geographic_crs)
210
301
 
211
302
  @property
212
303
  def minzoom(self) -> int:
213
304
  """TileMatrixSet minimum TileMatrix identifier"""
214
- return int(self.tileMatrix[0].identifier)
305
+ return int(self.tileMatrices[0].id)
215
306
 
216
307
  @property
217
308
  def maxzoom(self) -> int:
218
309
  """TileMatrixSet maximum TileMatrix identifier"""
219
- return int(self.tileMatrix[-1].identifier)
310
+ return int(self.tileMatrices[-1].id)
220
311
 
221
312
  @property
222
313
  def _invert_axis(self) -> bool:
223
314
  """Check if CRS has inverted AXIS (lat,lon) instead of (lon,lat)."""
224
- return crs_axis_inverted(self.crs)
315
+ return (
316
+ ordered_axis_inverted(self.orderedAxes)
317
+ if self.orderedAxes
318
+ else crs_axis_inverted(self.crs)
319
+ )
320
+
321
+ @classmethod
322
+ def from_v1(cls, tms: Dict) -> "TileMatrixSet":
323
+ """
324
+ Makes a TMS from a v1 TMS definition
325
+
326
+ Attributes
327
+ ----------
328
+ supportedCRS: CRSType
329
+ Tile Matrix Set coordinate reference system
330
+ title: str
331
+ Title of TMS
332
+ abstract: str (optional)
333
+ Abstract of CRS
334
+ keywords: str (optional)
335
+ Keywords
336
+ identifier: str
337
+ TMS Identifier
338
+ wellKnownScaleSet: AnyHttpUrl (optional)
339
+ WKSS URL
340
+ boundingBox: TMSBoundingBox (optional)
341
+ Bounding box of TMS
342
+ tileMatrix: List[TileMatrix]
343
+ List of Tile Matrices
344
+
345
+ Returns:
346
+ --------
347
+ TileMatrixSet
348
+ """
349
+ v2_tms = tms.copy()
350
+
351
+ del v2_tms["type"]
352
+
353
+ v2_tms["crs"] = v2_tms.pop("supportedCRS")
354
+ v2_tms["tileMatrices"] = v2_tms.pop("tileMatrix")
355
+ v2_tms["id"] = v2_tms.pop("identifier")
356
+ mpu = meters_per_unit(CRS.from_user_input(v2_tms["crs"]))
357
+ for i in range(len(v2_tms["tileMatrices"])):
358
+ v2_tms["tileMatrices"][i]["cellSize"] = (
359
+ v2_tms["tileMatrices"][i]["scaleDenominator"] * 0.00028 / mpu
360
+ )
361
+ v2_tms["tileMatrices"][i]["pointOfOrigin"] = v2_tms["tileMatrices"][i].pop(
362
+ "topLeftCorner"
363
+ )
364
+ v2_tms["tileMatrices"][i]["id"] = v2_tms["tileMatrices"][i].pop(
365
+ "identifier"
366
+ )
367
+ del v2_tms["tileMatrices"][i]["type"]
368
+
369
+ return TileMatrixSet(**v2_tms)
225
370
 
226
371
  @classmethod
227
372
  def custom(
@@ -234,9 +379,11 @@ class TileMatrixSet(BaseModel):
234
379
  extent_crs: Optional[CRS] = None,
235
380
  minzoom: int = 0,
236
381
  maxzoom: int = 24,
237
- title: str = "Custom TileMatrixSet",
238
- identifier: str = "Custom",
382
+ title: Optional[str] = None,
383
+ id: Optional[str] = None,
384
+ ordered_axes: Optional[List[str]] = None,
239
385
  geographic_crs: CRS = WGS84_CRS,
386
+ **kwargs: Any,
240
387
  ):
241
388
  """
242
389
  Construct a custom TileMatrixSet.
@@ -262,12 +409,14 @@ class TileMatrixSet(BaseModel):
262
409
  Tile Matrix Set minimum zoom level (default is 0).
263
410
  maxzoom: int
264
411
  Tile Matrix Set maximum zoom level (default is 24).
265
- title: str
266
- Tile Matrix Set title (default is 'Custom TileMatrixSet')
267
- identifier: str
268
- Tile Matrix Set identifier (default is 'Custom')
412
+ title: str, optional
413
+ Tile Matrix Set title
414
+ id: str, optional
415
+ Tile Matrix Set identifier
269
416
  geographic_crs: pyproj.CRS
270
417
  Geographic (lat,lon) coordinate reference system (default is EPSG:4326)
418
+ kwargs: Any
419
+ Attributes to forward to the TileMatrixSet
271
420
 
272
421
  Returns:
273
422
  --------
@@ -276,55 +425,35 @@ class TileMatrixSet(BaseModel):
276
425
  """
277
426
  matrix_scale = matrix_scale or [1, 1]
278
427
 
279
- tms: Dict[str, Any] = {
280
- "title": title,
281
- "identifier": identifier,
282
- "supportedCRS": crs,
283
- "tileMatrix": [],
284
- "_geographic_crs": geographic_crs,
285
- }
286
-
287
- is_inverted = crs_axis_inverted(crs)
288
-
289
- if is_inverted:
290
- tms["boundingBox"] = TMSBoundingBox(
291
- crs=extent_crs or crs,
292
- lowerCorner=[extent[1], extent[0]],
293
- upperCorner=[extent[3], extent[2]],
294
- )
428
+ if ordered_axes:
429
+ is_inverted = ordered_axis_inverted(ordered_axes)
295
430
  else:
296
- tms["boundingBox"] = TMSBoundingBox(
297
- crs=extent_crs or crs,
298
- lowerCorner=[extent[0], extent[1]],
299
- upperCorner=[extent[2], extent[3]],
300
- )
431
+ is_inverted = crs_axis_inverted(crs)
301
432
 
302
433
  if extent_crs:
303
434
  transform = Transformer.from_crs(extent_crs, crs, always_xy=True)
304
- left, bottom, right, top = extent
305
- bbox = BoundingBox(
306
- *transform.transform_bounds(left, bottom, right, top, densify_pts=21)
307
- )
308
- else:
309
- bbox = BoundingBox(*extent)
435
+ extent = transform.transform_bounds(*extent, densify_pts=21)
310
436
 
437
+ bbox = BoundingBox(*extent)
311
438
  x_origin = bbox.left if not is_inverted else bbox.top
312
439
  y_origin = bbox.top if not is_inverted else bbox.left
313
-
314
440
  width = abs(bbox.right - bbox.left)
315
441
  height = abs(bbox.top - bbox.bottom)
316
442
  mpu = meters_per_unit(crs)
443
+
444
+ tile_matrices: List[TileMatrix] = []
317
445
  for zoom in range(minzoom, maxzoom + 1):
318
446
  res = max(
319
447
  width / (tile_width * matrix_scale[0]) / 2.0**zoom,
320
448
  height / (tile_height * matrix_scale[1]) / 2.0**zoom,
321
449
  )
322
- tms["tileMatrix"].append(
450
+ tile_matrices.append(
323
451
  TileMatrix(
324
452
  **{
325
- "identifier": str(zoom),
453
+ "id": str(zoom),
326
454
  "scaleDenominator": res * mpu / 0.00028,
327
- "topLeftCorner": [x_origin, y_origin],
455
+ "cellSize": res,
456
+ "pointOfOrigin": [x_origin, y_origin],
328
457
  "tileWidth": tile_width,
329
458
  "tileHeight": tile_height,
330
459
  "matrixWidth": matrix_scale[0] * 2**zoom,
@@ -333,22 +462,29 @@ class TileMatrixSet(BaseModel):
333
462
  )
334
463
  )
335
464
 
336
- return cls(**tms)
465
+ return cls(
466
+ crs=crs,
467
+ tileMatrices=tile_matrices,
468
+ id=id,
469
+ title=title,
470
+ _geographic_crs=geographic_crs,
471
+ **kwargs,
472
+ )
337
473
 
338
474
  def matrix(self, zoom: int) -> TileMatrix:
339
475
  """Return the TileMatrix for a specific zoom."""
340
- for m in self.tileMatrix:
341
- if m.identifier == str(zoom):
476
+ for m in self.tileMatrices:
477
+ if m.id == str(zoom):
342
478
  return m
343
479
 
344
480
  matrix_scale = list(
345
481
  {
346
482
  round(
347
- self.tileMatrix[idx].scaleDenominator
348
- / self.tileMatrix[idx - 1].scaleDenominator,
483
+ self.tileMatrices[idx].scaleDenominator
484
+ / self.tileMatrices[idx - 1].scaleDenominator,
349
485
  2,
350
486
  )
351
- for idx in range(1, len(self.tileMatrix))
487
+ for idx in range(1, len(self.tileMatrices))
352
488
  }
353
489
  )
354
490
  if len(matrix_scale) > 1:
@@ -361,14 +497,15 @@ class TileMatrixSet(BaseModel):
361
497
  UserWarning,
362
498
  )
363
499
 
364
- tile_matrix = self.tileMatrix[-1]
500
+ tile_matrix = self.tileMatrices[-1]
365
501
  factor = 1 / matrix_scale[0]
366
- while not str(zoom) == tile_matrix.identifier:
502
+ while not str(zoom) == tile_matrix.id:
367
503
  tile_matrix = TileMatrix(
368
504
  **{
369
- "identifier": str(int(tile_matrix.identifier) + 1),
505
+ "id": str(int(tile_matrix.id) + 1),
370
506
  "scaleDenominator": tile_matrix.scaleDenominator / factor,
371
- "topLeftCorner": tile_matrix.topLeftCorner,
507
+ "cellSize": tile_matrix.cellSize / factor,
508
+ "pointOfOrigin": tile_matrix.pointOfOrigin,
372
509
  "tileWidth": tile_matrix.tileWidth,
373
510
  "tileHeight": tile_matrix.tileHeight,
374
511
  "matrixWidth": int(tile_matrix.matrixWidth * factor),
@@ -515,10 +652,10 @@ class TileMatrixSet(BaseModel):
515
652
  res = self._resolution(matrix)
516
653
 
517
654
  origin_x = (
518
- matrix.topLeftCorner[1] if self._invert_axis else matrix.topLeftCorner[0]
655
+ matrix.pointOfOrigin[1] if self._invert_axis else matrix.pointOfOrigin[0]
519
656
  )
520
657
  origin_y = (
521
- matrix.topLeftCorner[0] if self._invert_axis else matrix.topLeftCorner[1]
658
+ matrix.pointOfOrigin[0] if self._invert_axis else matrix.pointOfOrigin[1]
522
659
  )
523
660
 
524
661
  xtile = (
@@ -587,10 +724,10 @@ class TileMatrixSet(BaseModel):
587
724
  res = self._resolution(matrix)
588
725
 
589
726
  origin_x = (
590
- matrix.topLeftCorner[1] if self._invert_axis else matrix.topLeftCorner[0]
727
+ matrix.pointOfOrigin[1] if self._invert_axis else matrix.pointOfOrigin[0]
591
728
  )
592
729
  origin_y = (
593
- matrix.topLeftCorner[0] if self._invert_axis else matrix.topLeftCorner[1]
730
+ matrix.pointOfOrigin[0] if self._invert_axis else matrix.pointOfOrigin[1]
594
731
  )
595
732
 
596
733
  xcoord = origin_x + t.x * res * matrix.tileWidth
@@ -656,50 +793,19 @@ class TileMatrixSet(BaseModel):
656
793
  @property
657
794
  def xy_bbox(self):
658
795
  """Return TMS bounding box in TileMatrixSet's CRS."""
659
- if self.boundingBox:
660
- left = (
661
- self.boundingBox.lowerCorner[1]
662
- if self._invert_axis
663
- else self.boundingBox.lowerCorner[0]
664
- )
665
- bottom = (
666
- self.boundingBox.lowerCorner[0]
667
- if self._invert_axis
668
- else self.boundingBox.lowerCorner[1]
669
- )
670
- right = (
671
- self.boundingBox.upperCorner[1]
672
- if self._invert_axis
673
- else self.boundingBox.upperCorner[0]
674
- )
675
- top = (
676
- self.boundingBox.upperCorner[0]
677
- if self._invert_axis
678
- else self.boundingBox.upperCorner[1]
679
- )
680
- if self.boundingBox.crs != self.crs:
681
- transform = Transformer.from_crs(
682
- self.boundingBox.crs, self.crs, always_xy=True
683
- )
684
- left, bottom, right, top = transform.transform_bounds(
685
- left,
686
- bottom,
687
- right,
688
- top,
689
- densify_pts=21,
690
- )
796
+ zoom = self.minzoom
797
+ matrix = self.matrix(zoom)
691
798
 
692
- else:
693
- zoom = self.minzoom
694
- matrix = self.matrix(zoom)
695
- left, top = self._ul(Tile(0, 0, zoom))
696
- right, bottom = self._ul(
697
- Tile(matrix.matrixWidth, matrix.matrixHeight, zoom)
698
- )
799
+ left, top = self._ul(Tile(0, 0, zoom))
800
+ right, bottom = self._ul(Tile(matrix.matrixWidth, matrix.matrixHeight, zoom))
699
801
 
700
802
  return BoundingBox(left, bottom, right, top)
701
803
 
702
804
  @property
805
+ @cached( # type: ignore
806
+ LRUCache(maxsize=512),
807
+ key=lambda self: hashkey(self.id, self.tileMatrices[0].pointOfOrigin),
808
+ )
703
809
  def bbox(self):
704
810
  """Return TMS bounding box in geographic coordinate reference system."""
705
811
  left, bottom, right, top = self.xy_bbox
@@ -744,7 +850,7 @@ class TileMatrixSet(BaseModel):
744
850
  zooms : int or sequence of int
745
851
  One or more zoom levels.
746
852
  truncate : bool, optional
747
- Whether or not to truncate inputs to web mercator limits.
853
+ Whether or not to truncate inputs to TMS limits.
748
854
 
749
855
  Yields
750
856
  ------
@@ -778,13 +884,18 @@ class TileMatrixSet(BaseModel):
778
884
  n = min(self.bbox.top, n)
779
885
 
780
886
  for z in zooms:
781
- ul_tile = self.tile(
887
+ nw_tile = self.tile(
782
888
  w + LL_EPSILON, n - LL_EPSILON, z
783
889
  ) # Not in mercantile
784
- lr_tile = self.tile(e - LL_EPSILON, s + LL_EPSILON, z)
890
+ se_tile = self.tile(e - LL_EPSILON, s + LL_EPSILON, z)
891
+
892
+ minx = min(nw_tile.x, se_tile.x)
893
+ maxx = max(nw_tile.x, se_tile.x)
894
+ miny = min(nw_tile.y, se_tile.y)
895
+ maxy = max(nw_tile.y, se_tile.y)
785
896
 
786
- for i in range(ul_tile.x, lr_tile.x + 1):
787
- for j in range(ul_tile.y, lr_tile.y + 1):
897
+ for i in range(minx, maxx + 1):
898
+ for j in range(miny, maxy + 1):
788
899
  yield Tile(i, j, z)
789
900
 
790
901
  def feature(
@@ -851,7 +962,7 @@ class TileMatrixSet(BaseModel):
851
962
  "geometry": geom,
852
963
  "properties": {
853
964
  "title": f"XYZ tile {xyz}",
854
- "grid_name": self.identifier,
965
+ "grid_name": self.id,
855
966
  "grid_crs": self.crs.to_string(),
856
967
  },
857
968
  }
@@ -1063,8 +1174,8 @@ class TileMatrixSet(BaseModel):
1063
1174
  tile : Tile or sequence of int
1064
1175
  May be be either an instance of Tile or 3 ints, X, Y, Z.
1065
1176
  zoom : int, optional
1066
- Determines the *zoom* level of the returned parent tile.
1067
- This defaults to one lower than the tile (the immediate parent).
1177
+ Determines the *zoom* level of the returned child tiles.
1178
+ This defaults to one higher than the tile (the immediate children).
1068
1179
 
1069
1180
  Returns
1070
1181
  -------
@@ -431,7 +431,7 @@ def custom(
431
431
  tms = morecantile.TileMatrixSet.custom(
432
432
  extent,
433
433
  CRS.from_epsg(epsg),
434
- identifier=name,
434
+ id=name,
435
435
  minzoom=minzoom,
436
436
  maxzoom=maxzoom,
437
437
  tile_width=tile_width,
morecantile/utils.py CHANGED
@@ -4,6 +4,7 @@ import math
4
4
  from typing import Dict, List
5
5
 
6
6
  from pyproj import CRS
7
+ from pyproj.enums import WktVersion
7
8
 
8
9
  from morecantile.commons import BoundingBox, Coords, Tile
9
10
  from morecantile.errors import TileArgParsingError
@@ -100,3 +101,14 @@ def check_quadkey_support(tms: List) -> bool:
100
101
  for i, t in enumerate(tms[:-1])
101
102
  ]
102
103
  )
104
+
105
+
106
+ def to_rasterio_crs(incrs: CRS):
107
+ """Convert a pyproj CRS to a rasterio CRS"""
108
+ from rasterio import crs
109
+ from rasterio.env import GDALVersion
110
+
111
+ if GDALVersion.runtime().major < 3:
112
+ return crs.CRS.from_wkt(incrs.to_wkt(WktVersion.WKT1_GDAL))
113
+ else:
114
+ return crs.CRS.from_wkt(incrs.to_wkt())