morecantile 6.2.0__py3-none-any.whl → 7.0.1__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/__init__.py +1 -1
- morecantile/data/NZTM2000Quad.json +0 -1
- morecantile/data/WebMercatorQuad.json +362 -260
- morecantile/defaults.py +15 -16
- morecantile/models.py +280 -171
- morecantile/scripts/cli.py +19 -5
- morecantile/utils.py +24 -3
- {morecantile-6.2.0.dist-info → morecantile-7.0.1.dist-info}/METADATA +38 -35
- {morecantile-6.2.0.dist-info → morecantile-7.0.1.dist-info}/RECORD +12 -12
- {morecantile-6.2.0.dist-info → morecantile-7.0.1.dist-info}/WHEEL +1 -1
- morecantile-7.0.1.dist-info/entry_points.txt +2 -0
- morecantile-6.2.0.dist-info/entry_points.txt +0 -3
- {morecantile-6.2.0.dist-info → morecantile-7.0.1.dist-info/licenses}/LICENSE +0 -0
morecantile/models.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Pydantic modules for OGC TileMatrixSets (https://www.ogc.org/standards/tms)"""
|
|
2
2
|
|
|
3
3
|
import math
|
|
4
|
+
import os
|
|
4
5
|
import warnings
|
|
5
|
-
from
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Iterator, Sequence
|
|
7
|
+
from functools import cached_property, lru_cache
|
|
8
|
+
from typing import Any, Literal
|
|
7
9
|
|
|
8
10
|
import pyproj
|
|
9
11
|
from pydantic import (
|
|
@@ -16,7 +18,7 @@ from pydantic import (
|
|
|
16
18
|
field_validator,
|
|
17
19
|
model_validator,
|
|
18
20
|
)
|
|
19
|
-
from pyproj.exceptions import CRSError
|
|
21
|
+
from pyproj.exceptions import CRSError
|
|
20
22
|
from typing_extensions import Annotated
|
|
21
23
|
|
|
22
24
|
from morecantile.commons import BoundingBox, Coords, Tile
|
|
@@ -35,12 +37,17 @@ from morecantile.utils import (
|
|
|
35
37
|
meters_per_unit,
|
|
36
38
|
point_in_bbox,
|
|
37
39
|
to_rasterio_crs,
|
|
40
|
+
truncate_coordinates,
|
|
38
41
|
)
|
|
39
42
|
|
|
40
|
-
NumType =
|
|
41
|
-
BoundsType =
|
|
43
|
+
NumType = float | int
|
|
44
|
+
BoundsType = tuple[NumType, NumType]
|
|
42
45
|
LL_EPSILON = 1e-11
|
|
43
|
-
axesInfo = Annotated[
|
|
46
|
+
axesInfo = Annotated[list[str], Field(min_length=2, max_length=2)]
|
|
47
|
+
WGS84_CRS = pyproj.CRS.from_epsg(4326)
|
|
48
|
+
DEFAULT_GEOGRAPHIC_CRS = os.environ.get("MORECANTILE_DEFAULT_GEOGRAPHIC_CRS")
|
|
49
|
+
|
|
50
|
+
TransformerFromCRS = lru_cache(pyproj.Transformer.from_crs)
|
|
44
51
|
|
|
45
52
|
|
|
46
53
|
class CRSUri(BaseModel):
|
|
@@ -64,7 +71,7 @@ class CRSWKT(BaseModel):
|
|
|
64
71
|
"""Coordinate Reference System (CRS) from WKT encoded as PROJJSON Object."""
|
|
65
72
|
|
|
66
73
|
wkt: Annotated[
|
|
67
|
-
|
|
74
|
+
dict,
|
|
68
75
|
Field(
|
|
69
76
|
json_schema_extra={
|
|
70
77
|
"description": "An object defining the CRS using the JSON encoding for Well-known text representation of coordinate reference systems 2.0",
|
|
@@ -77,7 +84,7 @@ class CRSRef(BaseModel):
|
|
|
77
84
|
"""CRS from referenceSystem."""
|
|
78
85
|
|
|
79
86
|
referenceSystem: Annotated[
|
|
80
|
-
|
|
87
|
+
dict[str, Any],
|
|
81
88
|
Field(
|
|
82
89
|
json_schema_extra={
|
|
83
90
|
"description": "A reference system data structure as defined in the MD_ReferenceSystem of the ISO 19115",
|
|
@@ -86,7 +93,7 @@ class CRSRef(BaseModel):
|
|
|
86
93
|
]
|
|
87
94
|
|
|
88
95
|
|
|
89
|
-
class CRS(RootModel[
|
|
96
|
+
class CRS(RootModel[str | CRSUri | CRSWKT | CRSRef]):
|
|
90
97
|
"""CRS model.
|
|
91
98
|
|
|
92
99
|
Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/common-geodata/crs.yaml
|
|
@@ -119,7 +126,7 @@ class CRS(RootModel[Union[str, Union[CRSUri, CRSWKT, CRSRef]]]):
|
|
|
119
126
|
"""return the string form of the user input used to create the CRS."""
|
|
120
127
|
return self._pyproj_crs.srs
|
|
121
128
|
|
|
122
|
-
def to_epsg(self, *args: Any, **kwargs: Any) ->
|
|
129
|
+
def to_epsg(self, *args: Any, **kwargs: Any) -> int | None:
|
|
123
130
|
"""return EPSG number of the CRS."""
|
|
124
131
|
return self._pyproj_crs.to_epsg(*args, **kwargs)
|
|
125
132
|
|
|
@@ -131,7 +138,7 @@ class CRS(RootModel[Union[str, Union[CRSUri, CRSWKT, CRSRef]]]):
|
|
|
131
138
|
"""return PROJ4 version of the CRS."""
|
|
132
139
|
return self._pyproj_crs.to_proj4(*args, **kwargs)
|
|
133
140
|
|
|
134
|
-
def to_dict(self) ->
|
|
141
|
+
def to_dict(self) -> dict:
|
|
135
142
|
"""return DICT version of the CRS."""
|
|
136
143
|
return self._pyproj_crs.to_dict()
|
|
137
144
|
|
|
@@ -165,7 +172,7 @@ def crs_axis_inverted(crs: pyproj.CRS) -> bool:
|
|
|
165
172
|
return crs.axis_info[0].abbrev.upper() in ["Y", "LAT", "N"]
|
|
166
173
|
|
|
167
174
|
|
|
168
|
-
def ordered_axis_inverted(ordered_axes:
|
|
175
|
+
def ordered_axis_inverted(ordered_axes: list[str]) -> bool:
|
|
169
176
|
"""Check if ordered axes have inverted AXIS (lat,lon) instead of (lon,lat)."""
|
|
170
177
|
return ordered_axes[0].upper() in ["Y", "LAT", "N"]
|
|
171
178
|
|
|
@@ -194,7 +201,7 @@ class TMSBoundingBox(BaseModel, arbitrary_types_allowed=True):
|
|
|
194
201
|
),
|
|
195
202
|
]
|
|
196
203
|
crs: Annotated[
|
|
197
|
-
|
|
204
|
+
CRS | None,
|
|
198
205
|
Field(
|
|
199
206
|
json_schema_extra={
|
|
200
207
|
"description": "Coordinate Reference System (CRS)",
|
|
@@ -202,7 +209,7 @@ class TMSBoundingBox(BaseModel, arbitrary_types_allowed=True):
|
|
|
202
209
|
),
|
|
203
210
|
] = None
|
|
204
211
|
orderedAxes: Annotated[
|
|
205
|
-
|
|
212
|
+
axesInfo | None,
|
|
206
213
|
Field(
|
|
207
214
|
json_schema_extra={
|
|
208
215
|
"description": "Ordered list of names of the dimensions defined in the CRS",
|
|
@@ -258,7 +265,7 @@ class TileMatrix(BaseModel, extra="forbid"):
|
|
|
258
265
|
"""
|
|
259
266
|
|
|
260
267
|
title: Annotated[
|
|
261
|
-
|
|
268
|
+
str | None,
|
|
262
269
|
Field(
|
|
263
270
|
json_schema_extra={
|
|
264
271
|
"description": "Title of this tile matrix, normally used for display to a human",
|
|
@@ -266,7 +273,7 @@ class TileMatrix(BaseModel, extra="forbid"):
|
|
|
266
273
|
),
|
|
267
274
|
] = None
|
|
268
275
|
description: Annotated[
|
|
269
|
-
|
|
276
|
+
str | None,
|
|
270
277
|
Field(
|
|
271
278
|
json_schema_extra={
|
|
272
279
|
"description": "Brief narrative description of this tile matrix set, normally available for display to a human",
|
|
@@ -274,7 +281,7 @@ class TileMatrix(BaseModel, extra="forbid"):
|
|
|
274
281
|
),
|
|
275
282
|
] = None
|
|
276
283
|
keywords: Annotated[
|
|
277
|
-
|
|
284
|
+
list[str] | None,
|
|
278
285
|
Field(
|
|
279
286
|
json_schema_extra={
|
|
280
287
|
"description": "Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this dataset",
|
|
@@ -363,7 +370,7 @@ class TileMatrix(BaseModel, extra="forbid"):
|
|
|
363
370
|
),
|
|
364
371
|
]
|
|
365
372
|
variableMatrixWidths: Annotated[
|
|
366
|
-
|
|
373
|
+
list[variableMatrixWidth] | None,
|
|
367
374
|
Field(
|
|
368
375
|
json_schema_extra={
|
|
369
376
|
"description": "Describes the rows that has variable matrix width",
|
|
@@ -391,7 +398,7 @@ class TileMatrix(BaseModel, extra="forbid"):
|
|
|
391
398
|
return 1
|
|
392
399
|
|
|
393
400
|
|
|
394
|
-
class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
401
|
+
class TileMatrixSet(BaseModel, arbitrary_types_allowed=True, extra="ignore"):
|
|
395
402
|
"""Tile Matrix Set Definition
|
|
396
403
|
|
|
397
404
|
A definition of a tile matrix set following the Tile Matrix Set standard.
|
|
@@ -403,52 +410,58 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
403
410
|
"""
|
|
404
411
|
|
|
405
412
|
title: Annotated[
|
|
406
|
-
|
|
413
|
+
str | None,
|
|
407
414
|
Field(
|
|
408
415
|
json_schema_extra={
|
|
409
416
|
"description": "Title of this tile matrix set, normally used for display to a human",
|
|
410
|
-
}
|
|
417
|
+
},
|
|
418
|
+
frozen=True,
|
|
411
419
|
),
|
|
412
420
|
] = None
|
|
413
421
|
description: Annotated[
|
|
414
|
-
|
|
422
|
+
str | None,
|
|
415
423
|
Field(
|
|
416
424
|
json_schema_extra={
|
|
417
425
|
"description": "Brief narrative description of this tile matrix set, normally available for display to a human",
|
|
418
|
-
}
|
|
426
|
+
},
|
|
427
|
+
frozen=True,
|
|
419
428
|
),
|
|
420
429
|
] = None
|
|
421
430
|
keywords: Annotated[
|
|
422
|
-
|
|
431
|
+
list[str] | None,
|
|
423
432
|
Field(
|
|
424
433
|
json_schema_extra={
|
|
425
434
|
"description": "Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this tile matrix set",
|
|
426
|
-
}
|
|
435
|
+
},
|
|
436
|
+
frozen=True,
|
|
427
437
|
),
|
|
428
438
|
] = None
|
|
429
439
|
id: Annotated[
|
|
430
|
-
|
|
440
|
+
str | None,
|
|
431
441
|
Field(
|
|
432
442
|
pattern=r"^[\w\d_\-]+$",
|
|
433
443
|
json_schema_extra={
|
|
434
444
|
"description": "Tile matrix set identifier. Implementation of 'identifier'",
|
|
435
445
|
},
|
|
446
|
+
frozen=True,
|
|
436
447
|
),
|
|
437
448
|
] = None
|
|
438
449
|
uri: Annotated[
|
|
439
|
-
|
|
450
|
+
str | None,
|
|
440
451
|
Field(
|
|
441
452
|
json_schema_extra={
|
|
442
453
|
"description": "Reference to an official source for this tileMatrixSet",
|
|
443
|
-
}
|
|
454
|
+
},
|
|
455
|
+
frozen=True,
|
|
444
456
|
),
|
|
445
457
|
] = None
|
|
446
458
|
orderedAxes: Annotated[
|
|
447
|
-
|
|
459
|
+
axesInfo | None,
|
|
448
460
|
Field(
|
|
449
461
|
json_schema_extra={
|
|
450
462
|
"description": "Ordered list of names of the dimensions defined in the CRS",
|
|
451
|
-
}
|
|
463
|
+
},
|
|
464
|
+
frozen=True,
|
|
452
465
|
),
|
|
453
466
|
] = None
|
|
454
467
|
crs: Annotated[
|
|
@@ -456,39 +469,41 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
456
469
|
Field(
|
|
457
470
|
json_schema_extra={
|
|
458
471
|
"description": "Coordinate Reference System (CRS)",
|
|
459
|
-
}
|
|
472
|
+
},
|
|
473
|
+
frozen=True,
|
|
460
474
|
),
|
|
461
475
|
]
|
|
462
476
|
wellKnownScaleSet: Annotated[
|
|
463
|
-
|
|
477
|
+
AnyHttpUrl | None,
|
|
464
478
|
Field(
|
|
465
479
|
json_schema_extra={
|
|
466
480
|
"description": "Reference to a well-known scale set",
|
|
467
|
-
}
|
|
481
|
+
},
|
|
482
|
+
frozen=True,
|
|
468
483
|
),
|
|
469
484
|
] = None
|
|
470
485
|
boundingBox: Annotated[
|
|
471
|
-
|
|
486
|
+
TMSBoundingBox | None,
|
|
472
487
|
Field(
|
|
473
488
|
json_schema_extra={
|
|
474
489
|
"description": "Minimum bounding rectangle surrounding the tile matrix set, in the supported CRS",
|
|
475
|
-
}
|
|
490
|
+
},
|
|
491
|
+
frozen=True,
|
|
476
492
|
),
|
|
477
493
|
] = None
|
|
478
494
|
tileMatrices: Annotated[
|
|
479
|
-
|
|
495
|
+
list[TileMatrix],
|
|
480
496
|
Field(
|
|
481
497
|
json_schema_extra={
|
|
482
498
|
"description": "Describes scale levels and its tile matrices",
|
|
483
|
-
}
|
|
499
|
+
},
|
|
500
|
+
frozen=True,
|
|
484
501
|
),
|
|
485
502
|
]
|
|
486
503
|
|
|
487
504
|
# Private attributes
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
_tile_matrices_idx: Dict[int, int] = PrivateAttr()
|
|
505
|
+
_geographic_crs: pyproj.CRS = PrivateAttr()
|
|
506
|
+
_tile_matrices_idx: dict[int, int] = PrivateAttr()
|
|
492
507
|
|
|
493
508
|
def __init__(self, **data):
|
|
494
509
|
"""Set private attributes."""
|
|
@@ -498,22 +513,12 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
498
513
|
int(mat.id): idx for idx, mat in enumerate(self.tileMatrices)
|
|
499
514
|
}
|
|
500
515
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
self.
|
|
506
|
-
|
|
507
|
-
)
|
|
508
|
-
except ProjError:
|
|
509
|
-
warnings.warn(
|
|
510
|
-
"Could not create coordinate Transformer from input CRS to the given geographic CRS"
|
|
511
|
-
"some methods might not be available.",
|
|
512
|
-
UserWarning,
|
|
513
|
-
stacklevel=1,
|
|
514
|
-
)
|
|
515
|
-
self._to_geographic = None
|
|
516
|
-
self._from_geographic = None
|
|
516
|
+
# Default Geographic CRS from TMS's CRS
|
|
517
|
+
self._geographic_crs = (
|
|
518
|
+
pyproj.CRS.from_user_input(DEFAULT_GEOGRAPHIC_CRS)
|
|
519
|
+
if DEFAULT_GEOGRAPHIC_CRS
|
|
520
|
+
else self.crs._pyproj_crs.geodetic_crs
|
|
521
|
+
)
|
|
517
522
|
|
|
518
523
|
@model_validator(mode="before")
|
|
519
524
|
def check_for_old_specification(cls, data):
|
|
@@ -551,20 +556,36 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
551
556
|
"""Simplify default pydantic model repr."""
|
|
552
557
|
return f"<TileMatrixSet title='{self.title}' id='{self.id}' crs='{CRS_to_uri(self.crs._pyproj_crs)}>"
|
|
553
558
|
|
|
554
|
-
@cached_property
|
|
555
|
-
def geographic_crs(self) -> pyproj.CRS:
|
|
556
|
-
"""Return the TMS's geographic CRS."""
|
|
557
|
-
return self.crs._pyproj_crs.geodetic_crs
|
|
558
|
-
|
|
559
559
|
@cached_property
|
|
560
560
|
def rasterio_crs(self):
|
|
561
561
|
"""Return rasterio CRS."""
|
|
562
562
|
return to_rasterio_crs(self.crs._pyproj_crs)
|
|
563
563
|
|
|
564
|
-
|
|
564
|
+
def set_geographic_crs(self, crs: pyproj.CRS) -> None:
|
|
565
|
+
"""Overwrite Geographic CRS for the TMS."""
|
|
566
|
+
self._geographic_crs = crs
|
|
567
|
+
|
|
568
|
+
@property
|
|
569
|
+
def _to_geographic(self) -> pyproj.Transformer:
|
|
570
|
+
return TransformerFromCRS(
|
|
571
|
+
self.crs._pyproj_crs, self.geographic_crs, always_xy=True
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def _from_geographic(self) -> pyproj.Transformer:
|
|
576
|
+
return TransformerFromCRS(
|
|
577
|
+
self.geographic_crs, self.crs._pyproj_crs, always_xy=True
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
@property
|
|
581
|
+
def geographic_crs(self) -> pyproj.CRS:
|
|
582
|
+
"""Return the TMS's geographic CRS."""
|
|
583
|
+
return self._geographic_crs
|
|
584
|
+
|
|
585
|
+
@property
|
|
565
586
|
def rasterio_geographic_crs(self):
|
|
566
587
|
"""Return the geographic CRS as a rasterio CRS."""
|
|
567
|
-
return to_rasterio_crs(self.
|
|
588
|
+
return to_rasterio_crs(self._geographic_crs)
|
|
568
589
|
|
|
569
590
|
@property
|
|
570
591
|
def minzoom(self) -> int:
|
|
@@ -586,7 +607,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
586
607
|
)
|
|
587
608
|
|
|
588
609
|
@classmethod
|
|
589
|
-
def from_v1(cls, tms:
|
|
610
|
+
def from_v1(cls, tms: dict) -> "TileMatrixSet":
|
|
590
611
|
"""
|
|
591
612
|
Makes a TMS from a v1 TMS definition
|
|
592
613
|
|
|
@@ -606,7 +627,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
606
627
|
WKSS URL
|
|
607
628
|
boundingBox: TMSBoundingBox (optional)
|
|
608
629
|
Bounding box of TMS
|
|
609
|
-
tileMatrix:
|
|
630
|
+
tileMatrix: list[TileMatrix]
|
|
610
631
|
List of Tile Matrices
|
|
611
632
|
|
|
612
633
|
Returns:
|
|
@@ -644,19 +665,21 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
644
665
|
@classmethod
|
|
645
666
|
def custom(
|
|
646
667
|
cls,
|
|
647
|
-
extent:
|
|
668
|
+
extent: list[float],
|
|
648
669
|
crs: pyproj.CRS,
|
|
649
670
|
tile_width: int = 256,
|
|
650
671
|
tile_height: int = 256,
|
|
651
|
-
matrix_scale:
|
|
652
|
-
extent_crs:
|
|
672
|
+
matrix_scale: list | None = None,
|
|
673
|
+
extent_crs: pyproj.CRS | None = None,
|
|
653
674
|
minzoom: int = 0,
|
|
654
675
|
maxzoom: int = 24,
|
|
655
|
-
title:
|
|
656
|
-
id:
|
|
657
|
-
ordered_axes:
|
|
676
|
+
title: str | None = None,
|
|
677
|
+
id: str | None = None,
|
|
678
|
+
ordered_axes: list[str] | None = None,
|
|
658
679
|
screen_pixel_size: float = 0.28e-3,
|
|
659
680
|
decimation_base: int = 2,
|
|
681
|
+
corner_of_origin: Literal["topLeft", "bottomLeft"] = "topLeft",
|
|
682
|
+
point_of_origin: list[float] = None,
|
|
660
683
|
**kwargs: Any,
|
|
661
684
|
):
|
|
662
685
|
"""
|
|
@@ -664,10 +687,10 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
664
687
|
|
|
665
688
|
Attributes
|
|
666
689
|
----------
|
|
667
|
-
crs: pyproj.CRS
|
|
668
|
-
Tile Matrix Set coordinate reference system
|
|
669
690
|
extent: list
|
|
670
691
|
Bounding box of the Tile Matrix Set, (left, bottom, right, top).
|
|
692
|
+
crs: pyproj.CRS
|
|
693
|
+
Tile Matrix Set coordinate reference system
|
|
671
694
|
tile_width: int
|
|
672
695
|
Width of each tile of this tile matrix in pixels (default is 256).
|
|
673
696
|
tile_height: int
|
|
@@ -693,6 +716,10 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
693
716
|
Rendering pixel size. 0.28 mm was the actual pixel size of a common display from 2005 and considered as standard by OGC.
|
|
694
717
|
decimation_base: int, optional
|
|
695
718
|
How tiles are divided at each zoom level (default is 2). Must be greater than 1.
|
|
719
|
+
corner_of_origin: str, optional
|
|
720
|
+
Corner of origin for the TMS, either 'topLeft' or 'bottomLeft'
|
|
721
|
+
point_of_origin: list, optional
|
|
722
|
+
Point of origin for the TMS, (x, y) coordinates in the TMS CRS.
|
|
696
723
|
kwargs: Any
|
|
697
724
|
Attributes to forward to the TileMatrixSet
|
|
698
725
|
|
|
@@ -710,7 +737,10 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
710
737
|
|
|
711
738
|
if extent_crs:
|
|
712
739
|
transform = pyproj.Transformer.from_crs(extent_crs, crs, always_xy=True)
|
|
713
|
-
|
|
740
|
+
left, bottom, right, top = extent
|
|
741
|
+
extent = list(
|
|
742
|
+
transform.transform_bounds(left, bottom, right, top, densify_pts=21)
|
|
743
|
+
)
|
|
714
744
|
|
|
715
745
|
if decimation_base <= 1:
|
|
716
746
|
raise ValueError(
|
|
@@ -718,13 +748,25 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
718
748
|
)
|
|
719
749
|
|
|
720
750
|
bbox = BoundingBox(*extent)
|
|
721
|
-
|
|
722
|
-
|
|
751
|
+
if not point_of_origin:
|
|
752
|
+
if corner_of_origin == "topLeft":
|
|
753
|
+
x_origin = bbox.left if not is_inverted else bbox.top
|
|
754
|
+
y_origin = bbox.top if not is_inverted else bbox.left
|
|
755
|
+
point_of_origin = [x_origin, y_origin]
|
|
756
|
+
elif corner_of_origin == "bottomLeft":
|
|
757
|
+
x_origin = bbox.left if not is_inverted else bbox.bottom
|
|
758
|
+
y_origin = bbox.bottom if not is_inverted else bbox.left
|
|
759
|
+
point_of_origin = [x_origin, y_origin]
|
|
760
|
+
else:
|
|
761
|
+
raise ValueError(
|
|
762
|
+
f"Invalid `corner_of_origin` value: {corner_of_origin}, must be either 'topLeft' or 'bottomLeft'"
|
|
763
|
+
)
|
|
764
|
+
|
|
723
765
|
width = abs(bbox.right - bbox.left)
|
|
724
766
|
height = abs(bbox.top - bbox.bottom)
|
|
725
767
|
mpu = meters_per_unit(crs)
|
|
726
768
|
|
|
727
|
-
tile_matrices:
|
|
769
|
+
tile_matrices: list[TileMatrix] = []
|
|
728
770
|
for zoom in range(minzoom, maxzoom + 1):
|
|
729
771
|
res = max(
|
|
730
772
|
width / (tile_width * matrix_scale[0]) / float(decimation_base) ** zoom,
|
|
@@ -738,7 +780,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
738
780
|
"id": str(zoom),
|
|
739
781
|
"scaleDenominator": res * mpu / screen_pixel_size,
|
|
740
782
|
"cellSize": res,
|
|
741
|
-
"
|
|
783
|
+
"cornerOfOrigin": corner_of_origin,
|
|
784
|
+
"pointOfOrigin": point_of_origin,
|
|
742
785
|
"tileWidth": tile_width,
|
|
743
786
|
"tileHeight": tile_height,
|
|
744
787
|
"matrixWidth": matrix_scale[0] * decimation_base**zoom,
|
|
@@ -810,6 +853,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
810
853
|
id=str(int(tile_matrix.id) + 1),
|
|
811
854
|
scaleDenominator=tile_matrix.scaleDenominator / factor,
|
|
812
855
|
cellSize=tile_matrix.cellSize / factor,
|
|
856
|
+
cornerOfOrigin=tile_matrix.cornerOfOrigin,
|
|
813
857
|
pointOfOrigin=tile_matrix.pointOfOrigin,
|
|
814
858
|
tileWidth=tile_matrix.tileWidth,
|
|
815
859
|
tileHeight=tile_matrix.tileHeight,
|
|
@@ -832,9 +876,9 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
832
876
|
def zoom_for_res(
|
|
833
877
|
self,
|
|
834
878
|
res: float,
|
|
835
|
-
max_z:
|
|
879
|
+
max_z: int | None = None,
|
|
836
880
|
zoom_level_strategy: str = "auto",
|
|
837
|
-
min_z:
|
|
881
|
+
min_z: int | None = None,
|
|
838
882
|
) -> int:
|
|
839
883
|
"""Get TMS zoom level corresponding to a specific resolution.
|
|
840
884
|
|
|
@@ -887,7 +931,17 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
887
931
|
|
|
888
932
|
return zoom_level
|
|
889
933
|
|
|
890
|
-
def
|
|
934
|
+
def intersect_tms(self, bbox: BoundingBox) -> bool:
|
|
935
|
+
"""Check if a bounds intersects with the TMS bounds."""
|
|
936
|
+
tms_bounds = self.xy_bbox
|
|
937
|
+
return (
|
|
938
|
+
(bbox[0] < tms_bounds[2])
|
|
939
|
+
and (bbox[2] > tms_bounds[0])
|
|
940
|
+
and (bbox[3] > tms_bounds[1])
|
|
941
|
+
and (bbox[1] < tms_bounds[3])
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
def lnglat(self, x: float, y: float, truncate: bool = False) -> Coords:
|
|
891
945
|
"""Transform point(x,y) to geographic longitude and latitude."""
|
|
892
946
|
inside = point_in_bbox(Coords(x, y), self.xy_bbox)
|
|
893
947
|
if not inside:
|
|
@@ -900,14 +954,14 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
900
954
|
lng, lat = self._to_geographic.transform(x, y)
|
|
901
955
|
|
|
902
956
|
if truncate:
|
|
903
|
-
lng, lat =
|
|
957
|
+
lng, lat = truncate_coordinates(lng, lat, self.bbox)
|
|
904
958
|
|
|
905
959
|
return Coords(lng, lat)
|
|
906
960
|
|
|
907
|
-
def xy(self, lng: float, lat: float, truncate=False) -> Coords:
|
|
961
|
+
def xy(self, lng: float, lat: float, truncate: bool = False) -> Coords:
|
|
908
962
|
"""Transform geographic longitude and latitude coordinates to TMS CRS."""
|
|
909
963
|
if truncate:
|
|
910
|
-
lng, lat =
|
|
964
|
+
lng, lat = truncate_coordinates(lng, lat, self.bbox)
|
|
911
965
|
|
|
912
966
|
inside = point_in_bbox(Coords(lng, lat), self.bbox)
|
|
913
967
|
if not inside:
|
|
@@ -921,25 +975,6 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
921
975
|
|
|
922
976
|
return Coords(x, y)
|
|
923
977
|
|
|
924
|
-
def truncate_lnglat(self, lng: float, lat: float) -> Tuple[float, float]:
|
|
925
|
-
"""
|
|
926
|
-
Truncate geographic coordinates to TMS geographic bbox.
|
|
927
|
-
|
|
928
|
-
Adapted from https://github.com/mapbox/mercantile/blob/master/mercantile/__init__.py
|
|
929
|
-
|
|
930
|
-
"""
|
|
931
|
-
if lng > self.bbox.right:
|
|
932
|
-
lng = self.bbox.right
|
|
933
|
-
elif lng < self.bbox.left:
|
|
934
|
-
lng = self.bbox.left
|
|
935
|
-
|
|
936
|
-
if lat > self.bbox.top:
|
|
937
|
-
lat = self.bbox.top
|
|
938
|
-
elif lat < self.bbox.bottom:
|
|
939
|
-
lat = self.bbox.bottom
|
|
940
|
-
|
|
941
|
-
return lng, lat
|
|
942
|
-
|
|
943
978
|
def _tile(
|
|
944
979
|
self,
|
|
945
980
|
xcoord: float,
|
|
@@ -970,8 +1005,14 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
970
1005
|
if not math.isinf(xcoord)
|
|
971
1006
|
else 0
|
|
972
1007
|
)
|
|
1008
|
+
|
|
1009
|
+
coord = (
|
|
1010
|
+
(origin_y - ycoord)
|
|
1011
|
+
if matrix.cornerOfOrigin == "topLeft"
|
|
1012
|
+
else (ycoord - origin_y)
|
|
1013
|
+
)
|
|
973
1014
|
ytile = (
|
|
974
|
-
math.floor(
|
|
1015
|
+
math.floor(coord / float(matrix.cellSize * matrix.tileHeight))
|
|
975
1016
|
if not math.isinf(ycoord)
|
|
976
1017
|
else 0
|
|
977
1018
|
)
|
|
@@ -1007,6 +1048,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1007
1048
|
zoom: int,
|
|
1008
1049
|
truncate=False,
|
|
1009
1050
|
ignore_coalescence: bool = False,
|
|
1051
|
+
geographic_crs: pyproj.CRS | None = None,
|
|
1010
1052
|
) -> Tile:
|
|
1011
1053
|
"""
|
|
1012
1054
|
Get the tile for a given geographic longitude and latitude pair.
|
|
@@ -1019,13 +1061,41 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1019
1061
|
The zoom level.
|
|
1020
1062
|
truncate : bool
|
|
1021
1063
|
Whether or not to truncate inputs to limits of TMS geographic bounds.
|
|
1064
|
+
ignore_coalescence : bool
|
|
1065
|
+
Whether or not to ignore coalescence factor for TMS with variable matrix width.
|
|
1066
|
+
geographic_crs: pyproj.CRS, optional
|
|
1067
|
+
Geographic CRS of the given coordinates. Default to TMS's Geographic CRS.
|
|
1022
1068
|
|
|
1023
1069
|
Returns
|
|
1024
1070
|
-------
|
|
1025
1071
|
Tile
|
|
1026
1072
|
|
|
1027
1073
|
"""
|
|
1028
|
-
|
|
1074
|
+
geographic_crs = geographic_crs or self.geographic_crs or WGS84_CRS
|
|
1075
|
+
_from_geographic = TransformerFromCRS(
|
|
1076
|
+
geographic_crs, self.crs._pyproj_crs, always_xy=True
|
|
1077
|
+
)
|
|
1078
|
+
_to_geographic = TransformerFromCRS(
|
|
1079
|
+
self.crs._pyproj_crs, geographic_crs, always_xy=True
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
if truncate:
|
|
1083
|
+
left, bottom, right, top = self.xy_bbox
|
|
1084
|
+
bbox = BoundingBox(
|
|
1085
|
+
*_to_geographic.transform_bounds(
|
|
1086
|
+
left, bottom, right, top, densify_pts=21
|
|
1087
|
+
),
|
|
1088
|
+
)
|
|
1089
|
+
lng, lat = truncate_coordinates(lng, lat, bbox)
|
|
1090
|
+
|
|
1091
|
+
x, y = _from_geographic.transform(lng, lat)
|
|
1092
|
+
if not point_in_bbox(Coords(x, y), self.xy_bbox):
|
|
1093
|
+
warnings.warn(
|
|
1094
|
+
f"Point ({lng}, {lat}) is outside TMS bounds.",
|
|
1095
|
+
PointOutsideTMSBounds,
|
|
1096
|
+
stacklevel=1,
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1029
1099
|
return self._tile(x, y, zoom, ignore_coalescence=ignore_coalescence)
|
|
1030
1100
|
|
|
1031
1101
|
def _ul(self, *tile: Tile) -> Coords:
|
|
@@ -1051,10 +1121,16 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1051
1121
|
if matrix.variableMatrixWidths is not None
|
|
1052
1122
|
else 1
|
|
1053
1123
|
)
|
|
1054
|
-
|
|
1055
|
-
origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1056
|
-
origin_y - t.y * matrix.cellSize * matrix.tileHeight,
|
|
1124
|
+
x_coord = (
|
|
1125
|
+
origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1057
1126
|
)
|
|
1127
|
+
y_coord = (
|
|
1128
|
+
origin_y - t.y * matrix.cellSize * matrix.tileHeight
|
|
1129
|
+
if matrix.cornerOfOrigin == "topLeft"
|
|
1130
|
+
else origin_y + t.y * matrix.cellSize * matrix.tileHeight
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
return Coords(x_coord, y_coord)
|
|
1058
1134
|
|
|
1059
1135
|
def _lr(self, *tile: Tile) -> Coords:
|
|
1060
1136
|
"""
|
|
@@ -1079,12 +1155,18 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1079
1155
|
if matrix.variableMatrixWidths is not None
|
|
1080
1156
|
else 1
|
|
1081
1157
|
)
|
|
1082
|
-
|
|
1158
|
+
x_coord = (
|
|
1083
1159
|
origin_x
|
|
1084
|
-
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1085
|
-
|
|
1160
|
+
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1161
|
+
)
|
|
1162
|
+
y_coord = (
|
|
1163
|
+
origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1164
|
+
if matrix.cornerOfOrigin == "topLeft"
|
|
1165
|
+
else origin_y + (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1086
1166
|
)
|
|
1087
1167
|
|
|
1168
|
+
return Coords(x_coord, y_coord)
|
|
1169
|
+
|
|
1088
1170
|
def xy_bounds(self, *tile: Tile) -> BoundingBox:
|
|
1089
1171
|
"""
|
|
1090
1172
|
Return the bounding box of the tile in TMS coordinate reference system.
|
|
@@ -1110,12 +1192,16 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1110
1192
|
)
|
|
1111
1193
|
|
|
1112
1194
|
left = origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1113
|
-
top = origin_y - t.y * matrix.cellSize * matrix.tileHeight
|
|
1114
1195
|
right = (
|
|
1115
1196
|
origin_x
|
|
1116
1197
|
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1117
1198
|
)
|
|
1118
|
-
|
|
1199
|
+
if matrix.cornerOfOrigin == "topLeft":
|
|
1200
|
+
top = origin_y - t.y * matrix.cellSize * matrix.tileHeight
|
|
1201
|
+
bottom = origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1202
|
+
else:
|
|
1203
|
+
bottom = origin_y + t.y * matrix.cellSize * matrix.tileHeight
|
|
1204
|
+
top = origin_y + (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1119
1205
|
|
|
1120
1206
|
return BoundingBox(left, bottom, right, top)
|
|
1121
1207
|
|
|
@@ -1174,7 +1260,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1174
1260
|
|
|
1175
1261
|
return BoundingBox(left, bottom, right, top)
|
|
1176
1262
|
|
|
1177
|
-
@
|
|
1263
|
+
@cached_property
|
|
1178
1264
|
def xy_bbox(self):
|
|
1179
1265
|
"""Return TMS bounding box in TileMatrixSet's CRS."""
|
|
1180
1266
|
zoom = self.minzoom
|
|
@@ -1186,7 +1272,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1186
1272
|
)
|
|
1187
1273
|
return BoundingBox(left, bottom, right, top)
|
|
1188
1274
|
|
|
1189
|
-
@
|
|
1275
|
+
@property
|
|
1190
1276
|
def bbox(self):
|
|
1191
1277
|
"""Return TMS bounding box in geographic coordinate reference system."""
|
|
1192
1278
|
left, bottom, right, top = self.xy_bbox
|
|
@@ -1200,16 +1286,6 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1200
1286
|
)
|
|
1201
1287
|
)
|
|
1202
1288
|
|
|
1203
|
-
def intersect_tms(self, bbox: BoundingBox) -> bool:
|
|
1204
|
-
"""Check if a bounds intersects with the TMS bounds."""
|
|
1205
|
-
tms_bounds = self.xy_bbox
|
|
1206
|
-
return (
|
|
1207
|
-
(bbox[0] < tms_bounds[2])
|
|
1208
|
-
and (bbox[2] > tms_bounds[0])
|
|
1209
|
-
and (bbox[3] > tms_bounds[1])
|
|
1210
|
-
and (bbox[1] < tms_bounds[3])
|
|
1211
|
-
)
|
|
1212
|
-
|
|
1213
1289
|
def tiles( # noqa: C901
|
|
1214
1290
|
self,
|
|
1215
1291
|
west: float,
|
|
@@ -1218,6 +1294,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1218
1294
|
north: float,
|
|
1219
1295
|
zooms: Sequence[int],
|
|
1220
1296
|
truncate: bool = False,
|
|
1297
|
+
geographic_crs: pyproj.CRS | None = None,
|
|
1221
1298
|
) -> Iterator[Tile]:
|
|
1222
1299
|
"""
|
|
1223
1300
|
Get the tiles overlapped by a geographic bounding box
|
|
@@ -1232,6 +1309,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1232
1309
|
One or more zoom levels.
|
|
1233
1310
|
truncate : bool, optional
|
|
1234
1311
|
Whether or not to truncate inputs to TMS limits.
|
|
1312
|
+
geographic_crs: pyproj.CRS, optional
|
|
1313
|
+
Geographic CRS of the given coordinates. Default to TMS's Geographic CRS
|
|
1235
1314
|
|
|
1236
1315
|
Yields
|
|
1237
1316
|
------
|
|
@@ -1249,39 +1328,44 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1249
1328
|
if isinstance(zooms, int):
|
|
1250
1329
|
zooms = (zooms,)
|
|
1251
1330
|
|
|
1331
|
+
geographic_crs = geographic_crs or self.geographic_crs or WGS84_CRS
|
|
1332
|
+
_from_geographic = TransformerFromCRS(
|
|
1333
|
+
geographic_crs, self.crs._pyproj_crs, always_xy=True
|
|
1334
|
+
)
|
|
1335
|
+
_to_geographic = TransformerFromCRS(
|
|
1336
|
+
self.crs._pyproj_crs, geographic_crs, always_xy=True
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
# TMS bbox
|
|
1340
|
+
left, bottom, right, top = self.xy_bbox
|
|
1341
|
+
bbox = BoundingBox(
|
|
1342
|
+
*_to_geographic.transform_bounds(left, bottom, right, top, densify_pts=21),
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1252
1345
|
if truncate:
|
|
1253
|
-
west, south =
|
|
1254
|
-
east, north =
|
|
1346
|
+
west, south = truncate_coordinates(west, south, bbox)
|
|
1347
|
+
east, north = truncate_coordinates(east, north, bbox)
|
|
1255
1348
|
|
|
1256
1349
|
if west > east:
|
|
1257
|
-
bbox_west = (
|
|
1258
|
-
bbox_east = (west, south,
|
|
1350
|
+
bbox_west = (bbox.left, south, east, north)
|
|
1351
|
+
bbox_east = (west, south, bbox.right, north)
|
|
1259
1352
|
bboxes = [bbox_west, bbox_east]
|
|
1260
1353
|
else:
|
|
1261
1354
|
bboxes = [(west, south, east, north)]
|
|
1262
1355
|
|
|
1263
1356
|
for w, s, e, n in bboxes:
|
|
1264
1357
|
# Clamp bounding values.
|
|
1265
|
-
es_contain_180th = lons_contain_antimeridian(e,
|
|
1266
|
-
w = max(
|
|
1267
|
-
s = max(
|
|
1268
|
-
e = max(
|
|
1269
|
-
n = min(
|
|
1270
|
-
|
|
1358
|
+
es_contain_180th = lons_contain_antimeridian(e, bbox.right)
|
|
1359
|
+
w = max(bbox.left, w)
|
|
1360
|
+
s = max(bbox.bottom, s)
|
|
1361
|
+
e = max(bbox.right, e) if es_contain_180th else min(bbox.right, e)
|
|
1362
|
+
n = min(bbox.top, n)
|
|
1363
|
+
|
|
1364
|
+
w, n = _from_geographic.transform(w + LL_EPSILON, n - LL_EPSILON)
|
|
1365
|
+
e, s = _from_geographic.transform(e - LL_EPSILON, s + LL_EPSILON)
|
|
1271
1366
|
for z in zooms:
|
|
1272
|
-
nw_tile = self.
|
|
1273
|
-
|
|
1274
|
-
n - LL_EPSILON,
|
|
1275
|
-
z,
|
|
1276
|
-
ignore_coalescence=True,
|
|
1277
|
-
) # Not in mercantile
|
|
1278
|
-
se_tile = self.tile(
|
|
1279
|
-
e - LL_EPSILON,
|
|
1280
|
-
s + LL_EPSILON,
|
|
1281
|
-
z,
|
|
1282
|
-
ignore_coalescence=True,
|
|
1283
|
-
)
|
|
1284
|
-
|
|
1367
|
+
nw_tile = self._tile(w, n, z, ignore_coalescence=True)
|
|
1368
|
+
se_tile = self._tile(e, s, z, ignore_coalescence=True)
|
|
1285
1369
|
minx = min(nw_tile.x, se_tile.x)
|
|
1286
1370
|
maxx = max(nw_tile.x, se_tile.x)
|
|
1287
1371
|
miny = min(nw_tile.y, se_tile.y)
|
|
@@ -1303,12 +1387,13 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1303
1387
|
def feature(
|
|
1304
1388
|
self,
|
|
1305
1389
|
tile: Tile,
|
|
1306
|
-
fid:
|
|
1307
|
-
props:
|
|
1308
|
-
buffer:
|
|
1309
|
-
precision:
|
|
1390
|
+
fid: str | None = None,
|
|
1391
|
+
props: dict | None = None,
|
|
1392
|
+
buffer: NumType | None = None,
|
|
1393
|
+
precision: int | None = None,
|
|
1310
1394
|
projected: bool = False,
|
|
1311
|
-
|
|
1395
|
+
geographic_crs: pyproj.CRS | None = None,
|
|
1396
|
+
) -> dict:
|
|
1312
1397
|
"""
|
|
1313
1398
|
Get the GeoJSON feature corresponding to a tile.
|
|
1314
1399
|
|
|
@@ -1329,16 +1414,27 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1329
1414
|
otherwise original coordinate values will be preserved (default).
|
|
1330
1415
|
projected : bool, optional
|
|
1331
1416
|
Return coordinates in TMS projection. Default is false.
|
|
1417
|
+
geographic_crs: pyproj.CRS, optional
|
|
1418
|
+
Geographic CRS to use when `projected=False`. Default to 'EPSG:4326' as per GeoJSON specification.
|
|
1419
|
+
.
|
|
1332
1420
|
|
|
1333
1421
|
Returns
|
|
1334
1422
|
-------
|
|
1335
1423
|
dict
|
|
1336
1424
|
|
|
1337
1425
|
"""
|
|
1426
|
+
geographic_crs = geographic_crs or WGS84_CRS
|
|
1427
|
+
|
|
1428
|
+
feature_crs = self.crs._pyproj_crs
|
|
1338
1429
|
west, south, east, north = self.xy_bounds(tile)
|
|
1339
1430
|
|
|
1340
1431
|
if not projected:
|
|
1341
|
-
|
|
1432
|
+
feature_crs = geographic_crs
|
|
1433
|
+
tr = pyproj.Transformer.from_crs(
|
|
1434
|
+
self.crs._pyproj_crs, geographic_crs, always_xy=True
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
west, south, east, north = tr.transform_bounds(
|
|
1342
1438
|
west, south, east, north, densify_pts=21
|
|
1343
1439
|
)
|
|
1344
1440
|
|
|
@@ -1357,33 +1453,45 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1357
1453
|
geom = bbox_to_feature(west, south, east, north)
|
|
1358
1454
|
|
|
1359
1455
|
xyz = str(tile)
|
|
1360
|
-
feat:
|
|
1456
|
+
feat: dict[str, Any] = {
|
|
1361
1457
|
"type": "Feature",
|
|
1362
1458
|
"bbox": bbox,
|
|
1363
1459
|
"id": xyz,
|
|
1364
1460
|
"geometry": geom,
|
|
1365
1461
|
"properties": {
|
|
1366
1462
|
"title": f"XYZ tile {xyz}",
|
|
1367
|
-
"
|
|
1368
|
-
"
|
|
1463
|
+
"tms": self.id,
|
|
1464
|
+
"tms_crs": CRS_to_uri(self.crs._pyproj_crs),
|
|
1369
1465
|
},
|
|
1370
1466
|
}
|
|
1371
1467
|
|
|
1372
|
-
if
|
|
1468
|
+
if feature_crs != WGS84_CRS:
|
|
1373
1469
|
warnings.warn(
|
|
1374
1470
|
"CRS is no longer part of the GeoJSON specification."
|
|
1375
1471
|
"Other projection than EPSG:4326 might not be supported.",
|
|
1376
1472
|
UserWarning,
|
|
1377
1473
|
stacklevel=1,
|
|
1378
1474
|
)
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1475
|
+
|
|
1476
|
+
if authority_code := feature_crs.to_authority(min_confidence=20):
|
|
1477
|
+
authority, code = authority_code
|
|
1478
|
+
feat.update(
|
|
1479
|
+
{
|
|
1480
|
+
"crs": {
|
|
1481
|
+
"type": "name",
|
|
1482
|
+
"properties": {"name": CRS_to_uri(feature_crs)},
|
|
1483
|
+
}
|
|
1384
1484
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1485
|
+
)
|
|
1486
|
+
else:
|
|
1487
|
+
feat.update(
|
|
1488
|
+
{
|
|
1489
|
+
"crs": {
|
|
1490
|
+
"type": "wkt",
|
|
1491
|
+
"properties": {"wkt": feature_crs.to_wkt()},
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
)
|
|
1387
1495
|
|
|
1388
1496
|
if props:
|
|
1389
1497
|
feat["properties"].update(props)
|
|
@@ -1461,7 +1569,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1461
1569
|
|
|
1462
1570
|
return Tile(xtile, ytile, i + 1)
|
|
1463
1571
|
|
|
1464
|
-
def minmax(self, zoom: int) ->
|
|
1572
|
+
def minmax(self, zoom: int) -> dict:
|
|
1465
1573
|
"""Return TileMatrix Extrema.
|
|
1466
1574
|
|
|
1467
1575
|
Parameters
|
|
@@ -1471,7 +1579,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1471
1579
|
|
|
1472
1580
|
Returns
|
|
1473
1581
|
-------
|
|
1474
|
-
|
|
1582
|
+
dict
|
|
1475
1583
|
|
|
1476
1584
|
"""
|
|
1477
1585
|
m = self.matrix(zoom)
|
|
@@ -1480,11 +1588,12 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1480
1588
|
"y": {"min": 0, "max": m.matrixHeight - 1},
|
|
1481
1589
|
}
|
|
1482
1590
|
|
|
1483
|
-
def is_valid(self, *tile: Tile) -> bool:
|
|
1591
|
+
def is_valid(self, *tile: Tile, strict: bool = True) -> bool:
|
|
1484
1592
|
"""Check if a tile is valid."""
|
|
1485
1593
|
t = _parse_tile_arg(*tile)
|
|
1486
1594
|
|
|
1487
|
-
|
|
1595
|
+
disable_overzoom = self.is_variable or strict
|
|
1596
|
+
if t.z < self.minzoom or (disable_overzoom and t.z > self.maxzoom):
|
|
1488
1597
|
return False
|
|
1489
1598
|
|
|
1490
1599
|
matrix = self.matrix(t.z)
|
|
@@ -1493,7 +1602,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1493
1602
|
|
|
1494
1603
|
return validx and validy
|
|
1495
1604
|
|
|
1496
|
-
def neighbors(self, *tile: Tile) ->
|
|
1605
|
+
def neighbors(self, *tile: Tile) -> list[Tile]:
|
|
1497
1606
|
"""The neighbors of a tile
|
|
1498
1607
|
|
|
1499
1608
|
The neighbors function makes no guarantees regarding neighbor tile
|