morecantile 6.2.0__py3-none-any.whl → 7.0.0__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 +12 -12
- morecantile/models.py +225 -124
- morecantile/scripts/cli.py +19 -5
- morecantile/utils.py +23 -1
- {morecantile-6.2.0.dist-info → morecantile-7.0.0.dist-info}/METADATA +38 -35
- {morecantile-6.2.0.dist-info → morecantile-7.0.0.dist-info}/RECORD +12 -12
- {morecantile-6.2.0.dist-info → morecantile-7.0.0.dist-info}/WHEEL +1 -1
- morecantile-7.0.0.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.0.dist-info/licenses}/LICENSE +0 -0
morecantile/models.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 functools import cached_property
|
|
6
|
+
from functools import cached_property, lru_cache
|
|
6
7
|
from typing import Any, Dict, Iterator, List, Literal, Optional, Sequence, Tuple, Union
|
|
7
8
|
|
|
8
9
|
import pyproj
|
|
@@ -16,7 +17,7 @@ from pydantic import (
|
|
|
16
17
|
field_validator,
|
|
17
18
|
model_validator,
|
|
18
19
|
)
|
|
19
|
-
from pyproj.exceptions import CRSError
|
|
20
|
+
from pyproj.exceptions import CRSError
|
|
20
21
|
from typing_extensions import Annotated
|
|
21
22
|
|
|
22
23
|
from morecantile.commons import BoundingBox, Coords, Tile
|
|
@@ -35,12 +36,17 @@ from morecantile.utils import (
|
|
|
35
36
|
meters_per_unit,
|
|
36
37
|
point_in_bbox,
|
|
37
38
|
to_rasterio_crs,
|
|
39
|
+
truncate_coordinates,
|
|
38
40
|
)
|
|
39
41
|
|
|
40
42
|
NumType = Union[float, int]
|
|
41
43
|
BoundsType = Tuple[NumType, NumType]
|
|
42
44
|
LL_EPSILON = 1e-11
|
|
43
45
|
axesInfo = Annotated[List[str], Field(min_length=2, max_length=2)]
|
|
46
|
+
WGS84_CRS = pyproj.CRS.from_epsg(4326)
|
|
47
|
+
DEFAULT_GEOGRAPHIC_CRS = os.environ.get("MORECANTILE_DEFAULT_GEOGRAPHIC_CRS")
|
|
48
|
+
|
|
49
|
+
TransformerFromCRS = lru_cache(pyproj.Transformer.from_crs)
|
|
44
50
|
|
|
45
51
|
|
|
46
52
|
class CRSUri(BaseModel):
|
|
@@ -391,7 +397,7 @@ class TileMatrix(BaseModel, extra="forbid"):
|
|
|
391
397
|
return 1
|
|
392
398
|
|
|
393
399
|
|
|
394
|
-
class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
400
|
+
class TileMatrixSet(BaseModel, arbitrary_types_allowed=True, extra="ignore"):
|
|
395
401
|
"""Tile Matrix Set Definition
|
|
396
402
|
|
|
397
403
|
A definition of a tile matrix set following the Tile Matrix Set standard.
|
|
@@ -407,7 +413,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
407
413
|
Field(
|
|
408
414
|
json_schema_extra={
|
|
409
415
|
"description": "Title of this tile matrix set, normally used for display to a human",
|
|
410
|
-
}
|
|
416
|
+
},
|
|
417
|
+
frozen=True,
|
|
411
418
|
),
|
|
412
419
|
] = None
|
|
413
420
|
description: Annotated[
|
|
@@ -415,7 +422,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
415
422
|
Field(
|
|
416
423
|
json_schema_extra={
|
|
417
424
|
"description": "Brief narrative description of this tile matrix set, normally available for display to a human",
|
|
418
|
-
}
|
|
425
|
+
},
|
|
426
|
+
frozen=True,
|
|
419
427
|
),
|
|
420
428
|
] = None
|
|
421
429
|
keywords: Annotated[
|
|
@@ -423,7 +431,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
423
431
|
Field(
|
|
424
432
|
json_schema_extra={
|
|
425
433
|
"description": "Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this tile matrix set",
|
|
426
|
-
}
|
|
434
|
+
},
|
|
435
|
+
frozen=True,
|
|
427
436
|
),
|
|
428
437
|
] = None
|
|
429
438
|
id: Annotated[
|
|
@@ -433,6 +442,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
433
442
|
json_schema_extra={
|
|
434
443
|
"description": "Tile matrix set identifier. Implementation of 'identifier'",
|
|
435
444
|
},
|
|
445
|
+
frozen=True,
|
|
436
446
|
),
|
|
437
447
|
] = None
|
|
438
448
|
uri: Annotated[
|
|
@@ -440,7 +450,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
440
450
|
Field(
|
|
441
451
|
json_schema_extra={
|
|
442
452
|
"description": "Reference to an official source for this tileMatrixSet",
|
|
443
|
-
}
|
|
453
|
+
},
|
|
454
|
+
frozen=True,
|
|
444
455
|
),
|
|
445
456
|
] = None
|
|
446
457
|
orderedAxes: Annotated[
|
|
@@ -448,7 +459,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
448
459
|
Field(
|
|
449
460
|
json_schema_extra={
|
|
450
461
|
"description": "Ordered list of names of the dimensions defined in the CRS",
|
|
451
|
-
}
|
|
462
|
+
},
|
|
463
|
+
frozen=True,
|
|
452
464
|
),
|
|
453
465
|
] = None
|
|
454
466
|
crs: Annotated[
|
|
@@ -456,7 +468,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
456
468
|
Field(
|
|
457
469
|
json_schema_extra={
|
|
458
470
|
"description": "Coordinate Reference System (CRS)",
|
|
459
|
-
}
|
|
471
|
+
},
|
|
472
|
+
frozen=True,
|
|
460
473
|
),
|
|
461
474
|
]
|
|
462
475
|
wellKnownScaleSet: Annotated[
|
|
@@ -464,7 +477,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
464
477
|
Field(
|
|
465
478
|
json_schema_extra={
|
|
466
479
|
"description": "Reference to a well-known scale set",
|
|
467
|
-
}
|
|
480
|
+
},
|
|
481
|
+
frozen=True,
|
|
468
482
|
),
|
|
469
483
|
] = None
|
|
470
484
|
boundingBox: Annotated[
|
|
@@ -472,7 +486,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
472
486
|
Field(
|
|
473
487
|
json_schema_extra={
|
|
474
488
|
"description": "Minimum bounding rectangle surrounding the tile matrix set, in the supported CRS",
|
|
475
|
-
}
|
|
489
|
+
},
|
|
490
|
+
frozen=True,
|
|
476
491
|
),
|
|
477
492
|
] = None
|
|
478
493
|
tileMatrices: Annotated[
|
|
@@ -480,14 +495,13 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
480
495
|
Field(
|
|
481
496
|
json_schema_extra={
|
|
482
497
|
"description": "Describes scale levels and its tile matrices",
|
|
483
|
-
}
|
|
498
|
+
},
|
|
499
|
+
frozen=True,
|
|
484
500
|
),
|
|
485
501
|
]
|
|
486
502
|
|
|
487
503
|
# Private attributes
|
|
488
|
-
|
|
489
|
-
_from_geographic: pyproj.Transformer = PrivateAttr()
|
|
490
|
-
|
|
504
|
+
_geographic_crs: pyproj.CRS = PrivateAttr()
|
|
491
505
|
_tile_matrices_idx: Dict[int, int] = PrivateAttr()
|
|
492
506
|
|
|
493
507
|
def __init__(self, **data):
|
|
@@ -498,22 +512,12 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
498
512
|
int(mat.id): idx for idx, mat in enumerate(self.tileMatrices)
|
|
499
513
|
}
|
|
500
514
|
|
|
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
|
|
515
|
+
# Default Geographic CRS from TMS's CRS
|
|
516
|
+
self._geographic_crs = (
|
|
517
|
+
pyproj.CRS.from_user_input(DEFAULT_GEOGRAPHIC_CRS)
|
|
518
|
+
if DEFAULT_GEOGRAPHIC_CRS
|
|
519
|
+
else self.crs._pyproj_crs.geodetic_crs
|
|
520
|
+
)
|
|
517
521
|
|
|
518
522
|
@model_validator(mode="before")
|
|
519
523
|
def check_for_old_specification(cls, data):
|
|
@@ -551,20 +555,36 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
551
555
|
"""Simplify default pydantic model repr."""
|
|
552
556
|
return f"<TileMatrixSet title='{self.title}' id='{self.id}' crs='{CRS_to_uri(self.crs._pyproj_crs)}>"
|
|
553
557
|
|
|
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
558
|
@cached_property
|
|
560
559
|
def rasterio_crs(self):
|
|
561
560
|
"""Return rasterio CRS."""
|
|
562
561
|
return to_rasterio_crs(self.crs._pyproj_crs)
|
|
563
562
|
|
|
564
|
-
|
|
563
|
+
def set_geographic_crs(self, crs: CRS) -> None:
|
|
564
|
+
"""Overwrite Geographic CRS for the TMS."""
|
|
565
|
+
self._geographic_crs = crs
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def _to_geographic(self) -> pyproj.Transformer:
|
|
569
|
+
return TransformerFromCRS(
|
|
570
|
+
self.crs._pyproj_crs, self.geographic_crs, always_xy=True
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def _from_geographic(self) -> pyproj.Transformer:
|
|
575
|
+
return TransformerFromCRS(
|
|
576
|
+
self.geographic_crs, self.crs._pyproj_crs, always_xy=True
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def geographic_crs(self) -> pyproj.CRS:
|
|
581
|
+
"""Return the TMS's geographic CRS."""
|
|
582
|
+
return self._geographic_crs
|
|
583
|
+
|
|
584
|
+
@property
|
|
565
585
|
def rasterio_geographic_crs(self):
|
|
566
586
|
"""Return the geographic CRS as a rasterio CRS."""
|
|
567
|
-
return to_rasterio_crs(self.
|
|
587
|
+
return to_rasterio_crs(self._geographic_crs)
|
|
568
588
|
|
|
569
589
|
@property
|
|
570
590
|
def minzoom(self) -> int:
|
|
@@ -657,6 +677,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
657
677
|
ordered_axes: Optional[List[str]] = None,
|
|
658
678
|
screen_pixel_size: float = 0.28e-3,
|
|
659
679
|
decimation_base: int = 2,
|
|
680
|
+
corner_of_origin: Literal["topLeft", "bottomLeft"] = "topLeft",
|
|
681
|
+
point_of_origin: List[float] = None,
|
|
660
682
|
**kwargs: Any,
|
|
661
683
|
):
|
|
662
684
|
"""
|
|
@@ -664,10 +686,10 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
664
686
|
|
|
665
687
|
Attributes
|
|
666
688
|
----------
|
|
667
|
-
crs: pyproj.CRS
|
|
668
|
-
Tile Matrix Set coordinate reference system
|
|
669
689
|
extent: list
|
|
670
690
|
Bounding box of the Tile Matrix Set, (left, bottom, right, top).
|
|
691
|
+
crs: pyproj.CRS
|
|
692
|
+
Tile Matrix Set coordinate reference system
|
|
671
693
|
tile_width: int
|
|
672
694
|
Width of each tile of this tile matrix in pixels (default is 256).
|
|
673
695
|
tile_height: int
|
|
@@ -693,6 +715,10 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
693
715
|
Rendering pixel size. 0.28 mm was the actual pixel size of a common display from 2005 and considered as standard by OGC.
|
|
694
716
|
decimation_base: int, optional
|
|
695
717
|
How tiles are divided at each zoom level (default is 2). Must be greater than 1.
|
|
718
|
+
corner_of_origin: str, optional
|
|
719
|
+
Corner of origin for the TMS, either 'topLeft' or 'bottomLeft'
|
|
720
|
+
point_of_origin: list, optional
|
|
721
|
+
Point of origin for the TMS, (x, y) coordinates in the TMS CRS.
|
|
696
722
|
kwargs: Any
|
|
697
723
|
Attributes to forward to the TileMatrixSet
|
|
698
724
|
|
|
@@ -718,8 +744,20 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
718
744
|
)
|
|
719
745
|
|
|
720
746
|
bbox = BoundingBox(*extent)
|
|
721
|
-
|
|
722
|
-
|
|
747
|
+
if not point_of_origin:
|
|
748
|
+
if corner_of_origin == "topLeft":
|
|
749
|
+
x_origin = bbox.left if not is_inverted else bbox.top
|
|
750
|
+
y_origin = bbox.top if not is_inverted else bbox.left
|
|
751
|
+
point_of_origin = [x_origin, y_origin]
|
|
752
|
+
elif corner_of_origin == "bottomLeft":
|
|
753
|
+
x_origin = bbox.left if not is_inverted else bbox.bottom
|
|
754
|
+
y_origin = bbox.bottom if not is_inverted else bbox.left
|
|
755
|
+
point_of_origin = [x_origin, y_origin]
|
|
756
|
+
else:
|
|
757
|
+
raise ValueError(
|
|
758
|
+
f"Invalid `corner_of_origin` value: {corner_of_origin}, must be either 'topLeft' or 'bottomLeft'"
|
|
759
|
+
)
|
|
760
|
+
|
|
723
761
|
width = abs(bbox.right - bbox.left)
|
|
724
762
|
height = abs(bbox.top - bbox.bottom)
|
|
725
763
|
mpu = meters_per_unit(crs)
|
|
@@ -738,7 +776,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
738
776
|
"id": str(zoom),
|
|
739
777
|
"scaleDenominator": res * mpu / screen_pixel_size,
|
|
740
778
|
"cellSize": res,
|
|
741
|
-
"
|
|
779
|
+
"cornerOfOrigin": corner_of_origin,
|
|
780
|
+
"pointOfOrigin": point_of_origin,
|
|
742
781
|
"tileWidth": tile_width,
|
|
743
782
|
"tileHeight": tile_height,
|
|
744
783
|
"matrixWidth": matrix_scale[0] * decimation_base**zoom,
|
|
@@ -810,6 +849,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
810
849
|
id=str(int(tile_matrix.id) + 1),
|
|
811
850
|
scaleDenominator=tile_matrix.scaleDenominator / factor,
|
|
812
851
|
cellSize=tile_matrix.cellSize / factor,
|
|
852
|
+
cornerOfOrigin=tile_matrix.cornerOfOrigin,
|
|
813
853
|
pointOfOrigin=tile_matrix.pointOfOrigin,
|
|
814
854
|
tileWidth=tile_matrix.tileWidth,
|
|
815
855
|
tileHeight=tile_matrix.tileHeight,
|
|
@@ -887,7 +927,17 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
887
927
|
|
|
888
928
|
return zoom_level
|
|
889
929
|
|
|
890
|
-
def
|
|
930
|
+
def intersect_tms(self, bbox: BoundingBox) -> bool:
|
|
931
|
+
"""Check if a bounds intersects with the TMS bounds."""
|
|
932
|
+
tms_bounds = self.xy_bbox
|
|
933
|
+
return (
|
|
934
|
+
(bbox[0] < tms_bounds[2])
|
|
935
|
+
and (bbox[2] > tms_bounds[0])
|
|
936
|
+
and (bbox[3] > tms_bounds[1])
|
|
937
|
+
and (bbox[1] < tms_bounds[3])
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
def lnglat(self, x: float, y: float, truncate: bool = False) -> Coords:
|
|
891
941
|
"""Transform point(x,y) to geographic longitude and latitude."""
|
|
892
942
|
inside = point_in_bbox(Coords(x, y), self.xy_bbox)
|
|
893
943
|
if not inside:
|
|
@@ -900,14 +950,14 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
900
950
|
lng, lat = self._to_geographic.transform(x, y)
|
|
901
951
|
|
|
902
952
|
if truncate:
|
|
903
|
-
lng, lat =
|
|
953
|
+
lng, lat = truncate_coordinates(lng, lat, self.bbox)
|
|
904
954
|
|
|
905
955
|
return Coords(lng, lat)
|
|
906
956
|
|
|
907
|
-
def xy(self, lng: float, lat: float, truncate=False) -> Coords:
|
|
957
|
+
def xy(self, lng: float, lat: float, truncate: bool = False) -> Coords:
|
|
908
958
|
"""Transform geographic longitude and latitude coordinates to TMS CRS."""
|
|
909
959
|
if truncate:
|
|
910
|
-
lng, lat =
|
|
960
|
+
lng, lat = truncate_coordinates(lng, lat, self.bbox)
|
|
911
961
|
|
|
912
962
|
inside = point_in_bbox(Coords(lng, lat), self.bbox)
|
|
913
963
|
if not inside:
|
|
@@ -921,25 +971,6 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
921
971
|
|
|
922
972
|
return Coords(x, y)
|
|
923
973
|
|
|
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
974
|
def _tile(
|
|
944
975
|
self,
|
|
945
976
|
xcoord: float,
|
|
@@ -970,8 +1001,14 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
970
1001
|
if not math.isinf(xcoord)
|
|
971
1002
|
else 0
|
|
972
1003
|
)
|
|
1004
|
+
|
|
1005
|
+
coord = (
|
|
1006
|
+
(origin_y - ycoord)
|
|
1007
|
+
if matrix.cornerOfOrigin == "topLeft"
|
|
1008
|
+
else (ycoord - origin_y)
|
|
1009
|
+
)
|
|
973
1010
|
ytile = (
|
|
974
|
-
math.floor(
|
|
1011
|
+
math.floor(coord / float(matrix.cellSize * matrix.tileHeight))
|
|
975
1012
|
if not math.isinf(ycoord)
|
|
976
1013
|
else 0
|
|
977
1014
|
)
|
|
@@ -1007,6 +1044,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1007
1044
|
zoom: int,
|
|
1008
1045
|
truncate=False,
|
|
1009
1046
|
ignore_coalescence: bool = False,
|
|
1047
|
+
geographic_crs: Optional[CRS] = None,
|
|
1010
1048
|
) -> Tile:
|
|
1011
1049
|
"""
|
|
1012
1050
|
Get the tile for a given geographic longitude and latitude pair.
|
|
@@ -1019,13 +1057,38 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1019
1057
|
The zoom level.
|
|
1020
1058
|
truncate : bool
|
|
1021
1059
|
Whether or not to truncate inputs to limits of TMS geographic bounds.
|
|
1060
|
+
ignore_coalescence : bool
|
|
1061
|
+
Whether or not to ignore coalescence factor for TMS with variable matrix width.
|
|
1062
|
+
geographic_crs: pyproj.CRS, optional
|
|
1063
|
+
Geographic CRS of the given coordinates. Default to TMS's Geographic CRS.
|
|
1022
1064
|
|
|
1023
1065
|
Returns
|
|
1024
1066
|
-------
|
|
1025
1067
|
Tile
|
|
1026
1068
|
|
|
1027
1069
|
"""
|
|
1028
|
-
|
|
1070
|
+
geographic_crs = geographic_crs or self.geographic_crs or WGS84_CRS
|
|
1071
|
+
_from_geographic = TransformerFromCRS(
|
|
1072
|
+
geographic_crs, self.crs._pyproj_crs, always_xy=True
|
|
1073
|
+
)
|
|
1074
|
+
_to_geographic = TransformerFromCRS(
|
|
1075
|
+
self.crs._pyproj_crs, geographic_crs, always_xy=True
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
if truncate:
|
|
1079
|
+
bbox = BoundingBox(
|
|
1080
|
+
*_to_geographic.transform_bounds(*self.xy_bbox, densify_pts=21),
|
|
1081
|
+
)
|
|
1082
|
+
lng, lat = truncate_coordinates(lng, lat, bbox)
|
|
1083
|
+
|
|
1084
|
+
x, y = _from_geographic.transform(lng, lat)
|
|
1085
|
+
if not point_in_bbox(Coords(x, y), self.xy_bbox):
|
|
1086
|
+
warnings.warn(
|
|
1087
|
+
f"Point ({lng}, {lat}) is outside TMS bounds.",
|
|
1088
|
+
PointOutsideTMSBounds,
|
|
1089
|
+
stacklevel=1,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1029
1092
|
return self._tile(x, y, zoom, ignore_coalescence=ignore_coalescence)
|
|
1030
1093
|
|
|
1031
1094
|
def _ul(self, *tile: Tile) -> Coords:
|
|
@@ -1051,11 +1114,17 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1051
1114
|
if matrix.variableMatrixWidths is not None
|
|
1052
1115
|
else 1
|
|
1053
1116
|
)
|
|
1054
|
-
|
|
1055
|
-
origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1056
|
-
|
|
1117
|
+
x_coord = (
|
|
1118
|
+
origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1119
|
+
)
|
|
1120
|
+
y_coord = (
|
|
1121
|
+
origin_y - t.y * matrix.cellSize * matrix.tileHeight
|
|
1122
|
+
if matrix.cornerOfOrigin == "topLeft"
|
|
1123
|
+
else origin_y + t.y * matrix.cellSize * matrix.tileHeight
|
|
1057
1124
|
)
|
|
1058
1125
|
|
|
1126
|
+
return Coords(x_coord, y_coord)
|
|
1127
|
+
|
|
1059
1128
|
def _lr(self, *tile: Tile) -> Coords:
|
|
1060
1129
|
"""
|
|
1061
1130
|
Return the lower right coordinate of the tile in TMS coordinate reference system.
|
|
@@ -1079,11 +1148,17 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1079
1148
|
if matrix.variableMatrixWidths is not None
|
|
1080
1149
|
else 1
|
|
1081
1150
|
)
|
|
1082
|
-
|
|
1151
|
+
x_coord = (
|
|
1083
1152
|
origin_x
|
|
1084
|
-
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1085
|
-
origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight,
|
|
1153
|
+
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1086
1154
|
)
|
|
1155
|
+
y_coord = (
|
|
1156
|
+
origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1157
|
+
if matrix.cornerOfOrigin == "topLeft"
|
|
1158
|
+
else origin_y + (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
return Coords(x_coord, y_coord)
|
|
1087
1162
|
|
|
1088
1163
|
def xy_bounds(self, *tile: Tile) -> BoundingBox:
|
|
1089
1164
|
"""
|
|
@@ -1110,12 +1185,16 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1110
1185
|
)
|
|
1111
1186
|
|
|
1112
1187
|
left = origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1113
|
-
top = origin_y - t.y * matrix.cellSize * matrix.tileHeight
|
|
1114
1188
|
right = (
|
|
1115
1189
|
origin_x
|
|
1116
1190
|
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1117
1191
|
)
|
|
1118
|
-
|
|
1192
|
+
if matrix.cornerOfOrigin == "topLeft":
|
|
1193
|
+
top = origin_y - t.y * matrix.cellSize * matrix.tileHeight
|
|
1194
|
+
bottom = origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1195
|
+
else:
|
|
1196
|
+
bottom = origin_y + t.y * matrix.cellSize * matrix.tileHeight
|
|
1197
|
+
top = origin_y + (t.y + 1) * matrix.cellSize * matrix.tileHeight
|
|
1119
1198
|
|
|
1120
1199
|
return BoundingBox(left, bottom, right, top)
|
|
1121
1200
|
|
|
@@ -1174,7 +1253,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1174
1253
|
|
|
1175
1254
|
return BoundingBox(left, bottom, right, top)
|
|
1176
1255
|
|
|
1177
|
-
@
|
|
1256
|
+
@cached_property
|
|
1178
1257
|
def xy_bbox(self):
|
|
1179
1258
|
"""Return TMS bounding box in TileMatrixSet's CRS."""
|
|
1180
1259
|
zoom = self.minzoom
|
|
@@ -1186,7 +1265,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1186
1265
|
)
|
|
1187
1266
|
return BoundingBox(left, bottom, right, top)
|
|
1188
1267
|
|
|
1189
|
-
@
|
|
1268
|
+
@property
|
|
1190
1269
|
def bbox(self):
|
|
1191
1270
|
"""Return TMS bounding box in geographic coordinate reference system."""
|
|
1192
1271
|
left, bottom, right, top = self.xy_bbox
|
|
@@ -1200,16 +1279,6 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1200
1279
|
)
|
|
1201
1280
|
)
|
|
1202
1281
|
|
|
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
1282
|
def tiles( # noqa: C901
|
|
1214
1283
|
self,
|
|
1215
1284
|
west: float,
|
|
@@ -1218,6 +1287,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1218
1287
|
north: float,
|
|
1219
1288
|
zooms: Sequence[int],
|
|
1220
1289
|
truncate: bool = False,
|
|
1290
|
+
geographic_crs: Optional[CRS] = None,
|
|
1221
1291
|
) -> Iterator[Tile]:
|
|
1222
1292
|
"""
|
|
1223
1293
|
Get the tiles overlapped by a geographic bounding box
|
|
@@ -1232,6 +1302,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1232
1302
|
One or more zoom levels.
|
|
1233
1303
|
truncate : bool, optional
|
|
1234
1304
|
Whether or not to truncate inputs to TMS limits.
|
|
1305
|
+
geographic_crs: pyproj.CRS, optional
|
|
1306
|
+
Geographic CRS of the given coordinates. Default to TMS's Geographic CRS
|
|
1235
1307
|
|
|
1236
1308
|
Yields
|
|
1237
1309
|
------
|
|
@@ -1249,39 +1321,43 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1249
1321
|
if isinstance(zooms, int):
|
|
1250
1322
|
zooms = (zooms,)
|
|
1251
1323
|
|
|
1324
|
+
geographic_crs = geographic_crs or self.geographic_crs or WGS84_CRS
|
|
1325
|
+
_from_geographic = TransformerFromCRS(
|
|
1326
|
+
geographic_crs, self.crs._pyproj_crs, always_xy=True
|
|
1327
|
+
)
|
|
1328
|
+
_to_geographic = TransformerFromCRS(
|
|
1329
|
+
self.crs._pyproj_crs, geographic_crs, always_xy=True
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
# TMS bbox
|
|
1333
|
+
bbox = BoundingBox(
|
|
1334
|
+
*_to_geographic.transform_bounds(*self.xy_bbox, densify_pts=21),
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1252
1337
|
if truncate:
|
|
1253
|
-
west, south =
|
|
1254
|
-
east, north =
|
|
1338
|
+
west, south = truncate_coordinates(west, south, bbox)
|
|
1339
|
+
east, north = truncate_coordinates(east, north, bbox)
|
|
1255
1340
|
|
|
1256
1341
|
if west > east:
|
|
1257
|
-
bbox_west = (
|
|
1258
|
-
bbox_east = (west, south,
|
|
1342
|
+
bbox_west = (bbox.left, south, east, north)
|
|
1343
|
+
bbox_east = (west, south, bbox.right, north)
|
|
1259
1344
|
bboxes = [bbox_west, bbox_east]
|
|
1260
1345
|
else:
|
|
1261
1346
|
bboxes = [(west, south, east, north)]
|
|
1262
1347
|
|
|
1263
1348
|
for w, s, e, n in bboxes:
|
|
1264
1349
|
# Clamp bounding values.
|
|
1265
|
-
es_contain_180th = lons_contain_antimeridian(e,
|
|
1266
|
-
w = max(
|
|
1267
|
-
s = max(
|
|
1268
|
-
e = max(
|
|
1269
|
-
n = min(
|
|
1270
|
-
|
|
1350
|
+
es_contain_180th = lons_contain_antimeridian(e, bbox.right)
|
|
1351
|
+
w = max(bbox.left, w)
|
|
1352
|
+
s = max(bbox.bottom, s)
|
|
1353
|
+
e = max(bbox.right, e) if es_contain_180th else min(bbox.right, e)
|
|
1354
|
+
n = min(bbox.top, n)
|
|
1355
|
+
|
|
1356
|
+
w, n = _from_geographic.transform(w + LL_EPSILON, n - LL_EPSILON)
|
|
1357
|
+
e, s = _from_geographic.transform(e - LL_EPSILON, s + LL_EPSILON)
|
|
1271
1358
|
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
|
-
|
|
1359
|
+
nw_tile = self._tile(w, n, z, ignore_coalescence=True)
|
|
1360
|
+
se_tile = self._tile(e, s, z, ignore_coalescence=True)
|
|
1285
1361
|
minx = min(nw_tile.x, se_tile.x)
|
|
1286
1362
|
maxx = max(nw_tile.x, se_tile.x)
|
|
1287
1363
|
miny = min(nw_tile.y, se_tile.y)
|
|
@@ -1308,6 +1384,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1308
1384
|
buffer: Optional[NumType] = None,
|
|
1309
1385
|
precision: Optional[int] = None,
|
|
1310
1386
|
projected: bool = False,
|
|
1387
|
+
geographic_crs: Optional[CRS] = None,
|
|
1311
1388
|
) -> Dict:
|
|
1312
1389
|
"""
|
|
1313
1390
|
Get the GeoJSON feature corresponding to a tile.
|
|
@@ -1329,16 +1406,27 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1329
1406
|
otherwise original coordinate values will be preserved (default).
|
|
1330
1407
|
projected : bool, optional
|
|
1331
1408
|
Return coordinates in TMS projection. Default is false.
|
|
1409
|
+
geographic_crs: pyproj.CRS, optional
|
|
1410
|
+
Geographic CRS to use when `projected=False`. Default to 'EPSG:4326' as per GeoJSON specification.
|
|
1411
|
+
.
|
|
1332
1412
|
|
|
1333
1413
|
Returns
|
|
1334
1414
|
-------
|
|
1335
1415
|
dict
|
|
1336
1416
|
|
|
1337
1417
|
"""
|
|
1418
|
+
geographic_crs = geographic_crs or WGS84_CRS
|
|
1419
|
+
|
|
1420
|
+
feature_crs = self.crs._pyproj_crs
|
|
1338
1421
|
west, south, east, north = self.xy_bounds(tile)
|
|
1339
1422
|
|
|
1340
1423
|
if not projected:
|
|
1341
|
-
|
|
1424
|
+
feature_crs = geographic_crs
|
|
1425
|
+
tr = pyproj.Transformer.from_crs(
|
|
1426
|
+
self.crs._pyproj_crs, geographic_crs, always_xy=True
|
|
1427
|
+
)
|
|
1428
|
+
|
|
1429
|
+
west, south, east, north = tr.transform_bounds(
|
|
1342
1430
|
west, south, east, north, densify_pts=21
|
|
1343
1431
|
)
|
|
1344
1432
|
|
|
@@ -1364,26 +1452,38 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1364
1452
|
"geometry": geom,
|
|
1365
1453
|
"properties": {
|
|
1366
1454
|
"title": f"XYZ tile {xyz}",
|
|
1367
|
-
"
|
|
1368
|
-
"
|
|
1455
|
+
"tms": self.id,
|
|
1456
|
+
"tms_crs": CRS_to_uri(self.crs._pyproj_crs),
|
|
1369
1457
|
},
|
|
1370
1458
|
}
|
|
1371
1459
|
|
|
1372
|
-
if
|
|
1460
|
+
if feature_crs != WGS84_CRS:
|
|
1373
1461
|
warnings.warn(
|
|
1374
1462
|
"CRS is no longer part of the GeoJSON specification."
|
|
1375
1463
|
"Other projection than EPSG:4326 might not be supported.",
|
|
1376
1464
|
UserWarning,
|
|
1377
1465
|
stacklevel=1,
|
|
1378
1466
|
)
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1467
|
+
|
|
1468
|
+
if authority_code := feature_crs.to_authority(min_confidence=20):
|
|
1469
|
+
authority, code = authority_code
|
|
1470
|
+
feat.update(
|
|
1471
|
+
{
|
|
1472
|
+
"crs": {
|
|
1473
|
+
"type": "name",
|
|
1474
|
+
"properties": {"name": CRS_to_uri(feature_crs)},
|
|
1475
|
+
}
|
|
1384
1476
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1477
|
+
)
|
|
1478
|
+
else:
|
|
1479
|
+
feat.update(
|
|
1480
|
+
{
|
|
1481
|
+
"crs": {
|
|
1482
|
+
"type": "wkt",
|
|
1483
|
+
"properties": {"wkt": feature_crs.to_wkt()},
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
)
|
|
1387
1487
|
|
|
1388
1488
|
if props:
|
|
1389
1489
|
feat["properties"].update(props)
|
|
@@ -1480,11 +1580,12 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1480
1580
|
"y": {"min": 0, "max": m.matrixHeight - 1},
|
|
1481
1581
|
}
|
|
1482
1582
|
|
|
1483
|
-
def is_valid(self, *tile: Tile) -> bool:
|
|
1583
|
+
def is_valid(self, *tile: Tile, strict: bool = True) -> bool:
|
|
1484
1584
|
"""Check if a tile is valid."""
|
|
1485
1585
|
t = _parse_tile_arg(*tile)
|
|
1486
1586
|
|
|
1487
|
-
|
|
1587
|
+
disable_overzoom = self.is_variable or strict
|
|
1588
|
+
if t.z < self.minzoom or (disable_overzoom and t.z > self.maxzoom):
|
|
1488
1589
|
return False
|
|
1489
1590
|
|
|
1490
1591
|
matrix = self.matrix(t.z)
|