morecantile 3.2.5__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/models.py CHANGED
@@ -2,26 +2,29 @@
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
- from .commons import BoundingBox, Coords, Tile
13
- from .errors import (
13
+ from morecantile.commons import BoundingBox, Coords, Tile
14
+ from morecantile.errors import (
15
+ DeprecationError,
14
16
  InvalidZoomError,
15
17
  NoQuadkeySupport,
16
18
  PointOutsideTMSBounds,
17
19
  QuadKeyError,
18
20
  )
19
- from .utils import (
21
+ from morecantile.utils import (
20
22
  _parse_tile_arg,
21
23
  bbox_to_feature,
22
24
  check_quadkey_support,
23
25
  meters_per_unit,
24
26
  point_in_bbox,
27
+ to_rasterio_crs,
25
28
  )
26
29
 
27
30
  NumType = Union[float, int]
@@ -43,7 +46,8 @@ class CRSType(CRS, str):
43
46
  @classmethod
44
47
  def validate(cls, value: Union[CRS, str]) -> CRS:
45
48
  """Validate CRS."""
46
- # 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
47
51
  if not isinstance(value, CRS):
48
52
  return CRS.from_user_input(value)
49
53
 
@@ -89,6 +93,11 @@ def crs_axis_inverted(crs: CRS) -> bool:
89
93
  return crs.axis_info[0].abbrev.upper() in ["Y", "LAT", "N"]
90
94
 
91
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
+
92
101
  class TMSBoundingBox(BaseModel):
93
102
  """Bounding box"""
94
103
 
@@ -104,44 +113,124 @@ class TMSBoundingBox(BaseModel):
104
113
  json_encoders = {CRS: lambda v: CRS_to_uri(v)}
105
114
 
106
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
+
107
128
  class TileMatrix(BaseModel):
108
- """Tile matrix"""
109
-
110
- type: str = Field("TileMatrixType", const=True)
111
- title: Optional[str]
112
- abstract: Optional[str]
113
- keywords: Optional[List[str]]
114
- identifier: str = Field(..., regex=r"^[0-9]+$")
115
- scaleDenominator: float
116
- topLeftCorner: BoundsType
117
- tileWidth: int
118
- tileHeight: int
119
- matrixWidth: int
120
- 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")
121
186
 
122
187
  class Config:
123
- """Forbid additional items like variableMatrixWidth."""
188
+ """Forbid additional items like variableMatrixWidths."""
124
189
 
125
190
  extra = "forbid"
126
191
 
127
192
 
128
193
  class TileMatrixSet(BaseModel):
129
- """Tile matrix set"""
130
-
131
- type: str = Field("TileMatrixSetType", const=True)
132
- title: str
133
- abstract: Optional[str]
134
- keywords: Optional[List[str]]
135
- identifier: str = Field(..., regex=r"^[\w\d_\-]+$")
136
- supportedCRS: CRSType
137
- wellKnownScaleSet: Optional[AnyHttpUrl] = None
138
- boundingBox: Optional[TMSBoundingBox]
139
- 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
+ )
140
231
 
141
232
  # Private attributes
142
233
  _is_quadtree: bool = PrivateAttr()
143
-
144
- # CRS transformation attributes
145
234
  _geographic_crs: CRSType = PrivateAttr(default=WGS84_CRS)
146
235
  _to_geographic: Transformer = PrivateAttr()
147
236
  _from_geographic: Transformer = PrivateAttr()
@@ -154,18 +243,23 @@ class TileMatrixSet(BaseModel):
154
243
 
155
244
  def __init__(self, **data):
156
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
+
157
251
  super().__init__(**data)
158
252
 
159
- self._is_quadtree = check_quadkey_support(self.tileMatrix)
253
+ self._is_quadtree = check_quadkey_support(self.tileMatrices)
160
254
 
161
255
  self._geographic_crs = data.get("_geographic_crs", WGS84_CRS)
162
256
 
163
257
  try:
164
258
  self._to_geographic = Transformer.from_crs(
165
- self.supportedCRS, self._geographic_crs, always_xy=True
259
+ self.crs, self._geographic_crs, always_xy=True
166
260
  )
167
261
  self._from_geographic = Transformer.from_crs(
168
- self._geographic_crs, self.supportedCRS, always_xy=True
262
+ self._geographic_crs, self.crs, always_xy=True
169
263
  )
170
264
  except ProjError:
171
265
  warnings.warn(
@@ -176,51 +270,103 @@ class TileMatrixSet(BaseModel):
176
270
  self._to_geographic = None
177
271
  self._from_geographic = None
178
272
 
179
- @validator("tileMatrix")
273
+ @validator("tileMatrices")
180
274
  def sort_tile_matrices(cls, v):
181
275
  """Sort matrices by identifier"""
182
- return sorted(v, key=lambda m: int(m.identifier))
276
+ return sorted(v, key=lambda m: int(m.id))
183
277
 
184
278
  def __iter__(self):
185
279
  """Iterate over matrices"""
186
- for matrix in self.tileMatrix:
280
+ for matrix in self.tileMatrices:
187
281
  yield matrix
188
282
 
189
283
  def __repr__(self):
190
284
  """Simplify default pydantic model repr."""
191
- return f"<TileMatrixSet title='{self.title}' identifier='{self.identifier}'>"
285
+ return f"<TileMatrixSet title='{self.title}' id='{self.id}'>"
192
286
 
193
287
  @property
194
- def crs(self) -> CRS:
195
- """Fetch CRS from epsg"""
196
- return self.supportedCRS
288
+ def geographic_crs(self) -> CRSType:
289
+ """Return the TMS's geographic CRS."""
290
+ return self._geographic_crs
197
291
 
198
292
  @property
199
293
  def rasterio_crs(self):
200
294
  """Return rasterio CRS."""
295
+ return to_rasterio_crs(self.crs)
201
296
 
202
- import rasterio
203
- from rasterio.env import GDALVersion
204
-
205
- if GDALVersion.runtime().major < 3:
206
- return rasterio.crs.CRS.from_wkt(self.crs.to_wkt(WktVersion.WKT1_GDAL))
207
- else:
208
- 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)
209
301
 
210
302
  @property
211
303
  def minzoom(self) -> int:
212
304
  """TileMatrixSet minimum TileMatrix identifier"""
213
- return int(self.tileMatrix[0].identifier)
305
+ return int(self.tileMatrices[0].id)
214
306
 
215
307
  @property
216
308
  def maxzoom(self) -> int:
217
309
  """TileMatrixSet maximum TileMatrix identifier"""
218
- return int(self.tileMatrix[-1].identifier)
310
+ return int(self.tileMatrices[-1].id)
219
311
 
220
312
  @property
221
313
  def _invert_axis(self) -> bool:
222
314
  """Check if CRS has inverted AXIS (lat,lon) instead of (lon,lat)."""
223
- 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)
224
370
 
225
371
  @classmethod
226
372
  def custom(
@@ -229,13 +375,15 @@ class TileMatrixSet(BaseModel):
229
375
  crs: CRS,
230
376
  tile_width: int = 256,
231
377
  tile_height: int = 256,
232
- matrix_scale: List = [1, 1],
378
+ matrix_scale: Optional[List] = None,
233
379
  extent_crs: Optional[CRS] = None,
234
380
  minzoom: int = 0,
235
381
  maxzoom: int = 24,
236
- title: str = "Custom TileMatrixSet",
237
- identifier: str = "Custom",
382
+ title: Optional[str] = None,
383
+ id: Optional[str] = None,
384
+ ordered_axes: Optional[List[str]] = None,
238
385
  geographic_crs: CRS = WGS84_CRS,
386
+ **kwargs: Any,
239
387
  ):
240
388
  """
241
389
  Construct a custom TileMatrixSet.
@@ -261,118 +409,109 @@ class TileMatrixSet(BaseModel):
261
409
  Tile Matrix Set minimum zoom level (default is 0).
262
410
  maxzoom: int
263
411
  Tile Matrix Set maximum zoom level (default is 24).
264
- title: str
265
- Tile Matrix Set title (default is 'Custom TileMatrixSet')
266
- identifier: str
267
- 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
268
416
  geographic_crs: pyproj.CRS
269
417
  Geographic (lat,lon) coordinate reference system (default is EPSG:4326)
418
+ kwargs: Any
419
+ Attributes to forward to the TileMatrixSet
270
420
 
271
421
  Returns:
272
422
  --------
273
423
  TileMatrixSet
274
424
 
275
425
  """
276
- tms: Dict[str, Any] = {
277
- "title": title,
278
- "identifier": identifier,
279
- "supportedCRS": crs,
280
- "tileMatrix": [],
281
- "_geographic_crs": geographic_crs,
282
- }
283
-
284
- is_inverted = crs_axis_inverted(crs)
426
+ matrix_scale = matrix_scale or [1, 1]
285
427
 
286
- if is_inverted:
287
- tms["boundingBox"] = TMSBoundingBox(
288
- crs=extent_crs or crs,
289
- lowerCorner=[extent[1], extent[0]],
290
- upperCorner=[extent[3], extent[2]],
291
- )
428
+ if ordered_axes:
429
+ is_inverted = ordered_axis_inverted(ordered_axes)
292
430
  else:
293
- tms["boundingBox"] = TMSBoundingBox(
294
- crs=extent_crs or crs,
295
- lowerCorner=[extent[0], extent[1]],
296
- upperCorner=[extent[2], extent[3]],
297
- )
431
+ is_inverted = crs_axis_inverted(crs)
298
432
 
299
433
  if extent_crs:
300
434
  transform = Transformer.from_crs(extent_crs, crs, always_xy=True)
301
- left, bottom, right, top = extent
302
- bbox = BoundingBox(
303
- *transform.transform_bounds(left, bottom, right, top, densify_pts=21)
304
- )
305
- else:
306
- bbox = BoundingBox(*extent)
435
+ extent = transform.transform_bounds(*extent, densify_pts=21)
307
436
 
437
+ bbox = BoundingBox(*extent)
308
438
  x_origin = bbox.left if not is_inverted else bbox.top
309
439
  y_origin = bbox.top if not is_inverted else bbox.left
310
-
311
440
  width = abs(bbox.right - bbox.left)
312
441
  height = abs(bbox.top - bbox.bottom)
313
442
  mpu = meters_per_unit(crs)
443
+
444
+ tile_matrices: List[TileMatrix] = []
314
445
  for zoom in range(minzoom, maxzoom + 1):
315
446
  res = max(
316
447
  width / (tile_width * matrix_scale[0]) / 2.0**zoom,
317
448
  height / (tile_height * matrix_scale[1]) / 2.0**zoom,
318
449
  )
319
- tms["tileMatrix"].append(
450
+ tile_matrices.append(
320
451
  TileMatrix(
321
- **dict(
322
- identifier=str(zoom),
323
- scaleDenominator=res * mpu / 0.00028,
324
- topLeftCorner=[x_origin, y_origin],
325
- tileWidth=tile_width,
326
- tileHeight=tile_height,
327
- matrixWidth=matrix_scale[0] * 2**zoom,
328
- matrixHeight=matrix_scale[1] * 2**zoom,
329
- )
452
+ **{
453
+ "id": str(zoom),
454
+ "scaleDenominator": res * mpu / 0.00028,
455
+ "cellSize": res,
456
+ "pointOfOrigin": [x_origin, y_origin],
457
+ "tileWidth": tile_width,
458
+ "tileHeight": tile_height,
459
+ "matrixWidth": matrix_scale[0] * 2**zoom,
460
+ "matrixHeight": matrix_scale[1] * 2**zoom,
461
+ }
330
462
  )
331
463
  )
332
464
 
333
- 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
+ )
334
473
 
335
474
  def matrix(self, zoom: int) -> TileMatrix:
336
475
  """Return the TileMatrix for a specific zoom."""
337
- try:
338
- tile_matrix = list(
339
- filter(lambda m: m.identifier == str(zoom), self.tileMatrix)
340
- )[0]
341
- except IndexError:
342
- matrix_scale = list(
343
- {
344
- round(
345
- self.tileMatrix[idx].scaleDenominator
346
- / self.tileMatrix[idx - 1].scaleDenominator,
347
- 2,
348
- )
349
- for idx in range(1, len(self.tileMatrix))
350
- }
351
- )
352
- if len(matrix_scale) > 1:
353
- raise Exception(
354
- f"TileMatrix not found for level: {zoom} - Unable to construct tileMatrix for TMS with variable scale"
476
+ for m in self.tileMatrices:
477
+ if m.id == str(zoom):
478
+ return m
479
+
480
+ matrix_scale = list(
481
+ {
482
+ round(
483
+ self.tileMatrices[idx].scaleDenominator
484
+ / self.tileMatrices[idx - 1].scaleDenominator,
485
+ 2,
355
486
  )
356
-
357
- warnings.warn(
358
- f"TileMatrix not found for level: {zoom} - Creating values from TMS Scale.",
359
- UserWarning,
487
+ for idx in range(1, len(self.tileMatrices))
488
+ }
489
+ )
490
+ if len(matrix_scale) > 1:
491
+ raise InvalidZoomError(
492
+ f"TileMatrix not found for level: {zoom} - Unable to construct tileMatrix for TMS with variable scale"
360
493
  )
361
494
 
362
- tile_matrix = self.tileMatrix[-1]
363
- factor = 1 / matrix_scale[0]
364
- while not str(zoom) == tile_matrix.identifier:
365
- tile_matrix = TileMatrix(
366
- **dict(
367
- identifier=str(int(tile_matrix.identifier) + 1),
368
- scaleDenominator=tile_matrix.scaleDenominator / factor,
369
- topLeftCorner=tile_matrix.topLeftCorner,
370
- tileWidth=tile_matrix.tileWidth,
371
- tileHeight=tile_matrix.tileHeight,
372
- matrixWidth=int(tile_matrix.matrixWidth * factor),
373
- matrixHeight=int(tile_matrix.matrixHeight * factor),
374
- )
375
- )
495
+ warnings.warn(
496
+ f"TileMatrix not found for level: {zoom} - Creating values from TMS Scale.",
497
+ UserWarning,
498
+ )
499
+
500
+ tile_matrix = self.tileMatrices[-1]
501
+ factor = 1 / matrix_scale[0]
502
+ while not str(zoom) == tile_matrix.id:
503
+ tile_matrix = TileMatrix(
504
+ **{
505
+ "id": str(int(tile_matrix.id) + 1),
506
+ "scaleDenominator": tile_matrix.scaleDenominator / factor,
507
+ "cellSize": tile_matrix.cellSize / factor,
508
+ "pointOfOrigin": tile_matrix.pointOfOrigin,
509
+ "tileWidth": tile_matrix.tileWidth,
510
+ "tileHeight": tile_matrix.tileHeight,
511
+ "matrixWidth": int(tile_matrix.matrixWidth * factor),
512
+ "matrixHeight": int(tile_matrix.matrixHeight * factor),
513
+ }
514
+ )
376
515
 
377
516
  return tile_matrix
378
517
 
@@ -513,10 +652,10 @@ class TileMatrixSet(BaseModel):
513
652
  res = self._resolution(matrix)
514
653
 
515
654
  origin_x = (
516
- matrix.topLeftCorner[1] if self._invert_axis else matrix.topLeftCorner[0]
655
+ matrix.pointOfOrigin[1] if self._invert_axis else matrix.pointOfOrigin[0]
517
656
  )
518
657
  origin_y = (
519
- matrix.topLeftCorner[0] if self._invert_axis else matrix.topLeftCorner[1]
658
+ matrix.pointOfOrigin[0] if self._invert_axis else matrix.pointOfOrigin[1]
520
659
  )
521
660
 
522
661
  xtile = (
@@ -585,10 +724,10 @@ class TileMatrixSet(BaseModel):
585
724
  res = self._resolution(matrix)
586
725
 
587
726
  origin_x = (
588
- matrix.topLeftCorner[1] if self._invert_axis else matrix.topLeftCorner[0]
727
+ matrix.pointOfOrigin[1] if self._invert_axis else matrix.pointOfOrigin[0]
589
728
  )
590
729
  origin_y = (
591
- matrix.topLeftCorner[0] if self._invert_axis else matrix.topLeftCorner[1]
730
+ matrix.pointOfOrigin[0] if self._invert_axis else matrix.pointOfOrigin[1]
592
731
  )
593
732
 
594
733
  xcoord = origin_x + t.x * res * matrix.tileWidth
@@ -654,50 +793,19 @@ class TileMatrixSet(BaseModel):
654
793
  @property
655
794
  def xy_bbox(self):
656
795
  """Return TMS bounding box in TileMatrixSet's CRS."""
657
- if self.boundingBox:
658
- left = (
659
- self.boundingBox.lowerCorner[1]
660
- if self._invert_axis
661
- else self.boundingBox.lowerCorner[0]
662
- )
663
- bottom = (
664
- self.boundingBox.lowerCorner[0]
665
- if self._invert_axis
666
- else self.boundingBox.lowerCorner[1]
667
- )
668
- right = (
669
- self.boundingBox.upperCorner[1]
670
- if self._invert_axis
671
- else self.boundingBox.upperCorner[0]
672
- )
673
- top = (
674
- self.boundingBox.upperCorner[0]
675
- if self._invert_axis
676
- else self.boundingBox.upperCorner[1]
677
- )
678
- if self.boundingBox.crs != self.crs:
679
- transform = Transformer.from_crs(
680
- self.boundingBox.crs, self.crs, always_xy=True
681
- )
682
- left, bottom, right, top = transform.transform_bounds(
683
- left,
684
- bottom,
685
- right,
686
- top,
687
- densify_pts=21,
688
- )
796
+ zoom = self.minzoom
797
+ matrix = self.matrix(zoom)
689
798
 
690
- else:
691
- zoom = self.minzoom
692
- matrix = self.matrix(zoom)
693
- left, top = self._ul(Tile(0, 0, zoom))
694
- right, bottom = self._ul(
695
- Tile(matrix.matrixWidth, matrix.matrixHeight, zoom)
696
- )
799
+ left, top = self._ul(Tile(0, 0, zoom))
800
+ right, bottom = self._ul(Tile(matrix.matrixWidth, matrix.matrixHeight, zoom))
697
801
 
698
802
  return BoundingBox(left, bottom, right, top)
699
803
 
700
804
  @property
805
+ @cached( # type: ignore
806
+ LRUCache(maxsize=512),
807
+ key=lambda self: hashkey(self.id, self.tileMatrices[0].pointOfOrigin),
808
+ )
701
809
  def bbox(self):
702
810
  """Return TMS bounding box in geographic coordinate reference system."""
703
811
  left, bottom, right, top = self.xy_bbox
@@ -742,7 +850,7 @@ class TileMatrixSet(BaseModel):
742
850
  zooms : int or sequence of int
743
851
  One or more zoom levels.
744
852
  truncate : bool, optional
745
- Whether or not to truncate inputs to web mercator limits.
853
+ Whether or not to truncate inputs to TMS limits.
746
854
 
747
855
  Yields
748
856
  ------
@@ -776,20 +884,25 @@ class TileMatrixSet(BaseModel):
776
884
  n = min(self.bbox.top, n)
777
885
 
778
886
  for z in zooms:
779
- ul_tile = self.tile(
887
+ nw_tile = self.tile(
780
888
  w + LL_EPSILON, n - LL_EPSILON, z
781
889
  ) # Not in mercantile
782
- 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)
783
896
 
784
- for i in range(ul_tile.x, lr_tile.x + 1):
785
- 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):
786
899
  yield Tile(i, j, z)
787
900
 
788
901
  def feature(
789
902
  self,
790
903
  tile: Tile,
791
904
  fid: Optional[str] = None,
792
- props: Dict = {},
905
+ props: Optional[Dict] = None,
793
906
  buffer: Optional[NumType] = None,
794
907
  precision: Optional[int] = None,
795
908
  projected: bool = False,
@@ -849,7 +962,7 @@ class TileMatrixSet(BaseModel):
849
962
  "geometry": geom,
850
963
  "properties": {
851
964
  "title": f"XYZ tile {xyz}",
852
- "grid_name": self.identifier,
965
+ "grid_name": self.id,
853
966
  "grid_crs": self.crs.to_string(),
854
967
  },
855
968
  }
@@ -1061,8 +1174,8 @@ class TileMatrixSet(BaseModel):
1061
1174
  tile : Tile or sequence of int
1062
1175
  May be be either an instance of Tile or 3 ints, X, Y, Z.
1063
1176
  zoom : int, optional
1064
- Determines the *zoom* level of the returned parent tile.
1065
- 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).
1066
1179
 
1067
1180
  Returns
1068
1181
  -------