morecantile 6.1.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 +13 -13
- morecantile/models.py +231 -127
- morecantile/scripts/cli.py +19 -5
- morecantile/utils.py +27 -7
- {morecantile-6.1.0.dist-info → morecantile-7.0.0.dist-info}/METADATA +39 -35
- {morecantile-6.1.0.dist-info → morecantile-7.0.0.dist-info}/RECORD +12 -12
- {morecantile-6.1.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.1.0.dist-info/entry_points.txt +0 -3
- {morecantile-6.1.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,21 +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
|
-
)
|
|
514
|
-
self._to_geographic = None
|
|
515
|
-
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
|
+
)
|
|
516
521
|
|
|
517
522
|
@model_validator(mode="before")
|
|
518
523
|
def check_for_old_specification(cls, data):
|
|
@@ -537,10 +542,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
537
542
|
def is_variable(self) -> bool:
|
|
538
543
|
"""Check if TMS has variable width matrix."""
|
|
539
544
|
return any(
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
for matrix in self.tileMatrices
|
|
543
|
-
]
|
|
545
|
+
True if matrix.variableMatrixWidths is not None else False
|
|
546
|
+
for matrix in self.tileMatrices
|
|
544
547
|
)
|
|
545
548
|
|
|
546
549
|
def __iter__(self):
|
|
@@ -552,20 +555,36 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
552
555
|
"""Simplify default pydantic model repr."""
|
|
553
556
|
return f"<TileMatrixSet title='{self.title}' id='{self.id}' crs='{CRS_to_uri(self.crs._pyproj_crs)}>"
|
|
554
557
|
|
|
555
|
-
@cached_property
|
|
556
|
-
def geographic_crs(self) -> pyproj.CRS:
|
|
557
|
-
"""Return the TMS's geographic CRS."""
|
|
558
|
-
return self.crs._pyproj_crs.geodetic_crs
|
|
559
|
-
|
|
560
558
|
@cached_property
|
|
561
559
|
def rasterio_crs(self):
|
|
562
560
|
"""Return rasterio CRS."""
|
|
563
561
|
return to_rasterio_crs(self.crs._pyproj_crs)
|
|
564
562
|
|
|
565
|
-
|
|
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
|
|
566
585
|
def rasterio_geographic_crs(self):
|
|
567
586
|
"""Return the geographic CRS as a rasterio CRS."""
|
|
568
|
-
return to_rasterio_crs(self.
|
|
587
|
+
return to_rasterio_crs(self._geographic_crs)
|
|
569
588
|
|
|
570
589
|
@property
|
|
571
590
|
def minzoom(self) -> int:
|
|
@@ -658,6 +677,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
658
677
|
ordered_axes: Optional[List[str]] = None,
|
|
659
678
|
screen_pixel_size: float = 0.28e-3,
|
|
660
679
|
decimation_base: int = 2,
|
|
680
|
+
corner_of_origin: Literal["topLeft", "bottomLeft"] = "topLeft",
|
|
681
|
+
point_of_origin: List[float] = None,
|
|
661
682
|
**kwargs: Any,
|
|
662
683
|
):
|
|
663
684
|
"""
|
|
@@ -665,10 +686,10 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
665
686
|
|
|
666
687
|
Attributes
|
|
667
688
|
----------
|
|
668
|
-
crs: pyproj.CRS
|
|
669
|
-
Tile Matrix Set coordinate reference system
|
|
670
689
|
extent: list
|
|
671
690
|
Bounding box of the Tile Matrix Set, (left, bottom, right, top).
|
|
691
|
+
crs: pyproj.CRS
|
|
692
|
+
Tile Matrix Set coordinate reference system
|
|
672
693
|
tile_width: int
|
|
673
694
|
Width of each tile of this tile matrix in pixels (default is 256).
|
|
674
695
|
tile_height: int
|
|
@@ -694,6 +715,10 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
694
715
|
Rendering pixel size. 0.28 mm was the actual pixel size of a common display from 2005 and considered as standard by OGC.
|
|
695
716
|
decimation_base: int, optional
|
|
696
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.
|
|
697
722
|
kwargs: Any
|
|
698
723
|
Attributes to forward to the TileMatrixSet
|
|
699
724
|
|
|
@@ -719,8 +744,20 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
719
744
|
)
|
|
720
745
|
|
|
721
746
|
bbox = BoundingBox(*extent)
|
|
722
|
-
|
|
723
|
-
|
|
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
|
+
|
|
724
761
|
width = abs(bbox.right - bbox.left)
|
|
725
762
|
height = abs(bbox.top - bbox.bottom)
|
|
726
763
|
mpu = meters_per_unit(crs)
|
|
@@ -739,7 +776,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
739
776
|
"id": str(zoom),
|
|
740
777
|
"scaleDenominator": res * mpu / screen_pixel_size,
|
|
741
778
|
"cellSize": res,
|
|
742
|
-
"
|
|
779
|
+
"cornerOfOrigin": corner_of_origin,
|
|
780
|
+
"pointOfOrigin": point_of_origin,
|
|
743
781
|
"tileWidth": tile_width,
|
|
744
782
|
"tileHeight": tile_height,
|
|
745
783
|
"matrixWidth": matrix_scale[0] * decimation_base**zoom,
|
|
@@ -800,6 +838,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
800
838
|
warnings.warn(
|
|
801
839
|
f"TileMatrix not found for level: {zoom} - Creating values from TMS Scale.",
|
|
802
840
|
UserWarning,
|
|
841
|
+
stacklevel=1,
|
|
803
842
|
)
|
|
804
843
|
|
|
805
844
|
# TODO: what if we want to construct a matrix for a level up ?
|
|
@@ -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,57 +927,50 @@ 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:
|
|
894
944
|
warnings.warn(
|
|
895
945
|
f"Point ({x}, {y}) is outside TMS bounds {list(self.xy_bbox)}.",
|
|
896
946
|
PointOutsideTMSBounds,
|
|
947
|
+
stacklevel=1,
|
|
897
948
|
)
|
|
898
949
|
|
|
899
950
|
lng, lat = self._to_geographic.transform(x, y)
|
|
900
951
|
|
|
901
952
|
if truncate:
|
|
902
|
-
lng, lat =
|
|
953
|
+
lng, lat = truncate_coordinates(lng, lat, self.bbox)
|
|
903
954
|
|
|
904
955
|
return Coords(lng, lat)
|
|
905
956
|
|
|
906
|
-
def xy(self, lng: float, lat: float, truncate=False) -> Coords:
|
|
957
|
+
def xy(self, lng: float, lat: float, truncate: bool = False) -> Coords:
|
|
907
958
|
"""Transform geographic longitude and latitude coordinates to TMS CRS."""
|
|
908
959
|
if truncate:
|
|
909
|
-
lng, lat =
|
|
960
|
+
lng, lat = truncate_coordinates(lng, lat, self.bbox)
|
|
910
961
|
|
|
911
962
|
inside = point_in_bbox(Coords(lng, lat), self.bbox)
|
|
912
963
|
if not inside:
|
|
913
964
|
warnings.warn(
|
|
914
965
|
f"Point ({lng}, {lat}) is outside TMS bounds {list(self.bbox)}.",
|
|
915
966
|
PointOutsideTMSBounds,
|
|
967
|
+
stacklevel=1,
|
|
916
968
|
)
|
|
917
969
|
|
|
918
970
|
x, y = self._from_geographic.transform(lng, lat)
|
|
919
971
|
|
|
920
972
|
return Coords(x, y)
|
|
921
973
|
|
|
922
|
-
def truncate_lnglat(self, lng: float, lat: float) -> Tuple[float, float]:
|
|
923
|
-
"""
|
|
924
|
-
Truncate geographic coordinates to TMS geographic bbox.
|
|
925
|
-
|
|
926
|
-
Adapted from https://github.com/mapbox/mercantile/blob/master/mercantile/__init__.py
|
|
927
|
-
|
|
928
|
-
"""
|
|
929
|
-
if lng > self.bbox.right:
|
|
930
|
-
lng = self.bbox.right
|
|
931
|
-
elif lng < self.bbox.left:
|
|
932
|
-
lng = self.bbox.left
|
|
933
|
-
|
|
934
|
-
if lat > self.bbox.top:
|
|
935
|
-
lat = self.bbox.top
|
|
936
|
-
elif lat < self.bbox.bottom:
|
|
937
|
-
lat = self.bbox.bottom
|
|
938
|
-
|
|
939
|
-
return lng, lat
|
|
940
|
-
|
|
941
974
|
def _tile(
|
|
942
975
|
self,
|
|
943
976
|
xcoord: float,
|
|
@@ -968,8 +1001,14 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
968
1001
|
if not math.isinf(xcoord)
|
|
969
1002
|
else 0
|
|
970
1003
|
)
|
|
1004
|
+
|
|
1005
|
+
coord = (
|
|
1006
|
+
(origin_y - ycoord)
|
|
1007
|
+
if matrix.cornerOfOrigin == "topLeft"
|
|
1008
|
+
else (ycoord - origin_y)
|
|
1009
|
+
)
|
|
971
1010
|
ytile = (
|
|
972
|
-
math.floor(
|
|
1011
|
+
math.floor(coord / float(matrix.cellSize * matrix.tileHeight))
|
|
973
1012
|
if not math.isinf(ycoord)
|
|
974
1013
|
else 0
|
|
975
1014
|
)
|
|
@@ -1005,6 +1044,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1005
1044
|
zoom: int,
|
|
1006
1045
|
truncate=False,
|
|
1007
1046
|
ignore_coalescence: bool = False,
|
|
1047
|
+
geographic_crs: Optional[CRS] = None,
|
|
1008
1048
|
) -> Tile:
|
|
1009
1049
|
"""
|
|
1010
1050
|
Get the tile for a given geographic longitude and latitude pair.
|
|
@@ -1017,13 +1057,38 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1017
1057
|
The zoom level.
|
|
1018
1058
|
truncate : bool
|
|
1019
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.
|
|
1020
1064
|
|
|
1021
1065
|
Returns
|
|
1022
1066
|
-------
|
|
1023
1067
|
Tile
|
|
1024
1068
|
|
|
1025
1069
|
"""
|
|
1026
|
-
|
|
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
|
+
|
|
1027
1092
|
return self._tile(x, y, zoom, ignore_coalescence=ignore_coalescence)
|
|
1028
1093
|
|
|
1029
1094
|
def _ul(self, *tile: Tile) -> Coords:
|
|
@@ -1049,11 +1114,17 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1049
1114
|
if matrix.variableMatrixWidths is not None
|
|
1050
1115
|
else 1
|
|
1051
1116
|
)
|
|
1052
|
-
|
|
1053
|
-
origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1054
|
-
|
|
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
|
|
1055
1124
|
)
|
|
1056
1125
|
|
|
1126
|
+
return Coords(x_coord, y_coord)
|
|
1127
|
+
|
|
1057
1128
|
def _lr(self, *tile: Tile) -> Coords:
|
|
1058
1129
|
"""
|
|
1059
1130
|
Return the lower right coordinate of the tile in TMS coordinate reference system.
|
|
@@ -1077,11 +1148,17 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1077
1148
|
if matrix.variableMatrixWidths is not None
|
|
1078
1149
|
else 1
|
|
1079
1150
|
)
|
|
1080
|
-
|
|
1151
|
+
x_coord = (
|
|
1081
1152
|
origin_x
|
|
1082
|
-
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1083
|
-
origin_y - (t.y + 1) * matrix.cellSize * matrix.tileHeight,
|
|
1153
|
+
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1084
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)
|
|
1085
1162
|
|
|
1086
1163
|
def xy_bounds(self, *tile: Tile) -> BoundingBox:
|
|
1087
1164
|
"""
|
|
@@ -1108,12 +1185,16 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1108
1185
|
)
|
|
1109
1186
|
|
|
1110
1187
|
left = origin_x + math.floor(t.x / cf) * matrix.cellSize * cf * matrix.tileWidth
|
|
1111
|
-
top = origin_y - t.y * matrix.cellSize * matrix.tileHeight
|
|
1112
1188
|
right = (
|
|
1113
1189
|
origin_x
|
|
1114
1190
|
+ (math.floor(t.x / cf) + 1) * matrix.cellSize * cf * matrix.tileWidth
|
|
1115
1191
|
)
|
|
1116
|
-
|
|
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
|
|
1117
1198
|
|
|
1118
1199
|
return BoundingBox(left, bottom, right, top)
|
|
1119
1200
|
|
|
@@ -1172,7 +1253,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1172
1253
|
|
|
1173
1254
|
return BoundingBox(left, bottom, right, top)
|
|
1174
1255
|
|
|
1175
|
-
@
|
|
1256
|
+
@cached_property
|
|
1176
1257
|
def xy_bbox(self):
|
|
1177
1258
|
"""Return TMS bounding box in TileMatrixSet's CRS."""
|
|
1178
1259
|
zoom = self.minzoom
|
|
@@ -1184,7 +1265,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1184
1265
|
)
|
|
1185
1266
|
return BoundingBox(left, bottom, right, top)
|
|
1186
1267
|
|
|
1187
|
-
@
|
|
1268
|
+
@property
|
|
1188
1269
|
def bbox(self):
|
|
1189
1270
|
"""Return TMS bounding box in geographic coordinate reference system."""
|
|
1190
1271
|
left, bottom, right, top = self.xy_bbox
|
|
@@ -1198,16 +1279,6 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1198
1279
|
)
|
|
1199
1280
|
)
|
|
1200
1281
|
|
|
1201
|
-
def intersect_tms(self, bbox: BoundingBox) -> bool:
|
|
1202
|
-
"""Check if a bounds intersects with the TMS bounds."""
|
|
1203
|
-
tms_bounds = self.xy_bbox
|
|
1204
|
-
return (
|
|
1205
|
-
(bbox[0] < tms_bounds[2])
|
|
1206
|
-
and (bbox[2] > tms_bounds[0])
|
|
1207
|
-
and (bbox[3] > tms_bounds[1])
|
|
1208
|
-
and (bbox[1] < tms_bounds[3])
|
|
1209
|
-
)
|
|
1210
|
-
|
|
1211
1282
|
def tiles( # noqa: C901
|
|
1212
1283
|
self,
|
|
1213
1284
|
west: float,
|
|
@@ -1216,6 +1287,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1216
1287
|
north: float,
|
|
1217
1288
|
zooms: Sequence[int],
|
|
1218
1289
|
truncate: bool = False,
|
|
1290
|
+
geographic_crs: Optional[CRS] = None,
|
|
1219
1291
|
) -> Iterator[Tile]:
|
|
1220
1292
|
"""
|
|
1221
1293
|
Get the tiles overlapped by a geographic bounding box
|
|
@@ -1230,6 +1302,8 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1230
1302
|
One or more zoom levels.
|
|
1231
1303
|
truncate : bool, optional
|
|
1232
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
|
|
1233
1307
|
|
|
1234
1308
|
Yields
|
|
1235
1309
|
------
|
|
@@ -1247,39 +1321,43 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1247
1321
|
if isinstance(zooms, int):
|
|
1248
1322
|
zooms = (zooms,)
|
|
1249
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
|
+
|
|
1250
1337
|
if truncate:
|
|
1251
|
-
west, south =
|
|
1252
|
-
east, north =
|
|
1338
|
+
west, south = truncate_coordinates(west, south, bbox)
|
|
1339
|
+
east, north = truncate_coordinates(east, north, bbox)
|
|
1253
1340
|
|
|
1254
1341
|
if west > east:
|
|
1255
|
-
bbox_west = (
|
|
1256
|
-
bbox_east = (west, south,
|
|
1342
|
+
bbox_west = (bbox.left, south, east, north)
|
|
1343
|
+
bbox_east = (west, south, bbox.right, north)
|
|
1257
1344
|
bboxes = [bbox_west, bbox_east]
|
|
1258
1345
|
else:
|
|
1259
1346
|
bboxes = [(west, south, east, north)]
|
|
1260
1347
|
|
|
1261
1348
|
for w, s, e, n in bboxes:
|
|
1262
1349
|
# Clamp bounding values.
|
|
1263
|
-
es_contain_180th = lons_contain_antimeridian(e,
|
|
1264
|
-
w = max(
|
|
1265
|
-
s = max(
|
|
1266
|
-
e = max(
|
|
1267
|
-
n = min(
|
|
1268
|
-
|
|
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)
|
|
1269
1358
|
for z in zooms:
|
|
1270
|
-
nw_tile = self.
|
|
1271
|
-
|
|
1272
|
-
n - LL_EPSILON,
|
|
1273
|
-
z,
|
|
1274
|
-
ignore_coalescence=True,
|
|
1275
|
-
) # Not in mercantile
|
|
1276
|
-
se_tile = self.tile(
|
|
1277
|
-
e - LL_EPSILON,
|
|
1278
|
-
s + LL_EPSILON,
|
|
1279
|
-
z,
|
|
1280
|
-
ignore_coalescence=True,
|
|
1281
|
-
)
|
|
1282
|
-
|
|
1359
|
+
nw_tile = self._tile(w, n, z, ignore_coalescence=True)
|
|
1360
|
+
se_tile = self._tile(e, s, z, ignore_coalescence=True)
|
|
1283
1361
|
minx = min(nw_tile.x, se_tile.x)
|
|
1284
1362
|
maxx = max(nw_tile.x, se_tile.x)
|
|
1285
1363
|
miny = min(nw_tile.y, se_tile.y)
|
|
@@ -1306,6 +1384,7 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1306
1384
|
buffer: Optional[NumType] = None,
|
|
1307
1385
|
precision: Optional[int] = None,
|
|
1308
1386
|
projected: bool = False,
|
|
1387
|
+
geographic_crs: Optional[CRS] = None,
|
|
1309
1388
|
) -> Dict:
|
|
1310
1389
|
"""
|
|
1311
1390
|
Get the GeoJSON feature corresponding to a tile.
|
|
@@ -1327,16 +1406,27 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1327
1406
|
otherwise original coordinate values will be preserved (default).
|
|
1328
1407
|
projected : bool, optional
|
|
1329
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
|
+
.
|
|
1330
1412
|
|
|
1331
1413
|
Returns
|
|
1332
1414
|
-------
|
|
1333
1415
|
dict
|
|
1334
1416
|
|
|
1335
1417
|
"""
|
|
1418
|
+
geographic_crs = geographic_crs or WGS84_CRS
|
|
1419
|
+
|
|
1420
|
+
feature_crs = self.crs._pyproj_crs
|
|
1336
1421
|
west, south, east, north = self.xy_bounds(tile)
|
|
1337
1422
|
|
|
1338
1423
|
if not projected:
|
|
1339
|
-
|
|
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(
|
|
1340
1430
|
west, south, east, north, densify_pts=21
|
|
1341
1431
|
)
|
|
1342
1432
|
|
|
@@ -1362,25 +1452,38 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1362
1452
|
"geometry": geom,
|
|
1363
1453
|
"properties": {
|
|
1364
1454
|
"title": f"XYZ tile {xyz}",
|
|
1365
|
-
"
|
|
1366
|
-
"
|
|
1455
|
+
"tms": self.id,
|
|
1456
|
+
"tms_crs": CRS_to_uri(self.crs._pyproj_crs),
|
|
1367
1457
|
},
|
|
1368
1458
|
}
|
|
1369
1459
|
|
|
1370
|
-
if
|
|
1460
|
+
if feature_crs != WGS84_CRS:
|
|
1371
1461
|
warnings.warn(
|
|
1372
1462
|
"CRS is no longer part of the GeoJSON specification."
|
|
1373
1463
|
"Other projection than EPSG:4326 might not be supported.",
|
|
1374
1464
|
UserWarning,
|
|
1465
|
+
stacklevel=1,
|
|
1375
1466
|
)
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
+
}
|
|
1381
1476
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1477
|
+
)
|
|
1478
|
+
else:
|
|
1479
|
+
feat.update(
|
|
1480
|
+
{
|
|
1481
|
+
"crs": {
|
|
1482
|
+
"type": "wkt",
|
|
1483
|
+
"properties": {"wkt": feature_crs.to_wkt()},
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
)
|
|
1384
1487
|
|
|
1385
1488
|
if props:
|
|
1386
1489
|
feat["properties"].update(props)
|
|
@@ -1477,11 +1580,12 @@ class TileMatrixSet(BaseModel, arbitrary_types_allowed=True):
|
|
|
1477
1580
|
"y": {"min": 0, "max": m.matrixHeight - 1},
|
|
1478
1581
|
}
|
|
1479
1582
|
|
|
1480
|
-
def is_valid(self, *tile: Tile) -> bool:
|
|
1583
|
+
def is_valid(self, *tile: Tile, strict: bool = True) -> bool:
|
|
1481
1584
|
"""Check if a tile is valid."""
|
|
1482
1585
|
t = _parse_tile_arg(*tile)
|
|
1483
1586
|
|
|
1484
|
-
|
|
1587
|
+
disable_overzoom = self.is_variable or strict
|
|
1588
|
+
if t.z < self.minzoom or (disable_overzoom and t.z > self.maxzoom):
|
|
1485
1589
|
return False
|
|
1486
1590
|
|
|
1487
1591
|
matrix = self.matrix(t.z)
|