GithubHelp home page GithubHelp logo

developmentseed / morecantile Goto Github PK

View Code? Open in Web Editor NEW
104.0 9.0 23.0 1.02 MB

Construct and use OGC TileMatrixSets (TMS)

Home Page: https://developmentseed.org/morecantile/

License: MIT License

Python 100.00%

morecantile's Introduction

Morecantile

Construct and use map tile grids (a.k.a TileMatrixSet / TMS).

Test Coverage Package version Downloads License


Documentation: https://developmentseed.org/morecantile/

Source Code: https://github.com/developmentseed/morecantile


Morecantile is like mercantile (the best tool to work with Web Mercator tile indexes), but with support for other TileMatrixSet grids.

Morecantile follows the OGC Two Dimensional Tile Matrix Set specification 2.0 found in https://docs.ogc.org/is/17-083r4/17-083r4.html

Morecantile Version OGC Specification Version Link
>=4.0 2.0 https://docs.ogc.org/is/17-083r4/17-083r4.html
=<3.0 1.0 http://docs.opengeospatial.org/is/17-083r2/17-083r2.html

Install

$ python -m pip install -U pip
$ python -m pip install morecantile

# Or install from source:
$ python -m pip install -U pip
$ python -m pip install git+https://github.com/developmentseed/morecantile.git

Usage

import morecantile

tms = morecantile.tms.get("WebMercatorQuad")

# Get TMS bounding box
print(tms.xy_bbox)
>>> BoundingBox(
    left=-20037508.342789244,
    bottom=-20037508.34278919,
    right=20037508.34278919,
    top=20037508.342789244,
)

# Get the bounds for tile Z=4, X=10, Y=10 in the TMS's CRS (e.g epsg:3857)
print(tms.xy_bounds(morecantile.Tile(10, 10, 4)))
>>> BoundingBox(
    left=5009377.085697308,
    bottom=-7514065.628545959,
    right=7514065.628545959,
    top=-5009377.085697308,
)

# Get the bounds for tile Z=4, X=10, Y=10 in Geographic CRS (e.g epsg:4326)
print(tms.bounds(morecantile.Tile(10, 10, 4)))
>>> BoundingBox(
    left=44.999999999999964,
    bottom=-55.776573018667634,
    right=67.4999999999999,
    top=-40.97989806962009,
)

More info can be found at https://developmentseed.org/morecantile/usage/

Defaults Grids

morecantile provides a set of default TileMatrixSets:

  • CDB1GlobalGrid *: CDB 1 Global Grid - EPGS:4326 (WGS84)
  • CanadianNAD83_LCC: Lambert conformal conic NAD83 for Canada - EPSG:3978
  • EuropeanETRS89_LAEAQuad: ETRS89-extended / LAEA Europe - EPGS:3035
  • GNOSISGlobalGrid *: GNOSIS Global Grid - EPGS:4326 (WGS84)
  • LINZAntarticaMapTilegrid: LINZ Antarctic Map Tile Grid (Ross Sea Region) - EPSG:5482
  • NZTM2000Quad: LINZ NZTM2000 Map Tile Grid - EPSG:2193
  • UPSAntarcticWGS84Quad: Universal Polar Stereographic WGS 84 Quad for Antarctic - EPSG:5042
  • UPSArcticWGS84Quad: Universal Polar Stereographic WGS 84 Quad for Arctic - EPSG:5041
  • UTM31WGS84Quad: Example of UTM grid - EPSG:32631
  • WebMercatorQuad: Spherical Mercator - EPGS:3857 (default grid for Web Mercator based maps)
  • WGS1984Quad: EPSG:4326 for the World - EPGS:4326 (WGS84)
  • WorldCRS84Quad: CRS84 for the World
  • WorldMercatorWGS84Quad: Elliptical Mercator projection - EPGS:3395

* TileMatrixSets with variable Matrix Width (see https://docs.ogc.org/is/17-083r4/17-083r4.html#toc15)

ref: https://schemas.opengis.net/tms/2.0/json/examples/tilematrixset/

Implementations

  • rio-tiler: Create tile from raster using Morecantile TMS.
  • titiler: A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL.
  • tipg: OGC Features and Tiles API.
  • planetcantile: Tile matrix sets for other planets.
  • supermorecado: Extend the functionality of morecantile with additional commands.

Changes

See CHANGES.md.

Contribution & Development

See CONTRIBUTING.md

License

See LICENSE

Authors

Created by Development Seed

morecantile's People

Contributors

adrian-knauer avatar andrewannex avatar blacha avatar davenquinn avatar dchirst avatar geospatial-jeff avatar jlaura avatar kylebarron avatar pratikyadav avatar samn avatar vincentsarago avatar yellowcap avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

morecantile's Issues

Release 1.0.0 - 2020/05/11

With the changes made in #4 we broke most of the functionalities. Gonna cut a major release which will be 1.0.0

Register new TileMatrixSet

Right now to load a TMS we do morecantile.TileMatrixSet.load("WebMercatorQuad") which in the backend will fetch and parse the JSON file.

This is fine when an application only use the default grids but when it needs custom ones the application will have to do something like

if identifier == "MyCustomGrid":
   tms = morecantile.TileMatrixSet.custom(extent, crs)
else:
   tms = morecantile.TileMatrixSet.load(identifier)

It would be nice to have a way to register custom TMS when an app loads

custom_tms = morecantile.TileMatrixSet.custom(extent, crs, identifier="MyCustomTMS")
morecantile.TileMatrixSet.register(custom_tms)

I'm not sure what is the best practice to do this.

enable `Variable width tile matrices`

Until now, it has been assumed that matrixWidth is constant for all tile rows. This is common usage for projections that do not distort the Earth too much. But when using Equirectangular Plate CarrΓ©e projection (see Annex D subsection 2) the distortion increases for tiles closer to the poles. In the extreme, the upper row of the upper tile (the one representing the North Pole) contains a list of repeated values that represents almost the same position in the space. The same can be said for the lower row of the lower tile (the one representing the South Pole). When the tiles are represented in a flat projection, this is an effect that cannot be avoided, but when the data are presented in a virtual globe, the distortion results in redundant information in the poles that need to be eliminated by the client during the rendering. Compensating for distortion is better done at the server side instead.

The solution consists of reducing the number of tiles (matrixWidth) in the high latitude rows and generating those tiles with a compressed scale in the longitudinal dimension (see Figure 8). To allow this solution, the tile model must be extended to specify coalescence coefficients (𝑐) that reduce the number of tiles in the width direction by aggregating 𝑐 horizontal tiles but keeping the tileWidth (and tileHeight). The coalescence coefficient is not applied next to the Equator but is used in medium and high latitudes (the higher the latitude the larger the coefficient).

Even if tiles can coalesce, this does not change the indexing or the tile matrix set that will be the same as if no coalescence has been applied. For example, if the 𝑐 coefficient is 4, the tileCol of the first tile will be 0, the tileCol of the second tile will be 4, the tileCol of the third tile will be 8 and so on. In other words, and for the same example, tileCol 0, 1, 2 and 3 points to the same tile.

ref: https://docs.ogc.org/is/17-083r4/17-083r4.html#toc15

I've added the needed attributes in the model (commented)

# class variableMatrixWidth(BaseModel):
# """Variable Matrix Width Definition
# ref: https://github.com/opengeospatial/2D-Tile-Matrix-Set/blob/master/schemas/tms/2.0/json/variableMatrixWidth.json
# """
# coalesce: int = Field(..., ge=2, multiple_of=1, description="Number of tiles in width that coalesce in a single tile for these rows")
# minTileRow: int = Field(..., ge=0, multiple_of=1, description="First tile row where the coalescence factor applies for this tilematrix")
# maxTileRow: int = Field(..., ge=0, multiple_of=1, description="Last tile row where the coalescence factor applies for this tilematrix")
and
# variableMatrixWidths: Optional[List[variableMatrixWidth]] = Field(description="Describes the rows that has variable matrix width")
but I'm not quite sure I understand how it works πŸ˜“

Custom Tileset can't be set

I'm trying to create a custom tile matrix based on the example:

import rasterio, morecantile
from rasterio.crs import CRS
crs = CRS.from_proj4("+proj=stere +lat_0=90 +lon_0=0 +k=2 +x_0=0 +y_0=0 +R=3396190 +units=m +no_defs")
extent = [-13584760.000,-13585240.000,13585240.000,13584760.000]
tms = morecantile.TileMatrixSet.custom(extent, crs, indentifier="MarsNPolek2MOLA5k")
Traceback (most recent call last):
File "", line 1, in
TypeError: custom() got an unexpected keyword argument 'indentifier'

if I remove 'indentifier':

tms = morecantile.TileMatrixSet.custom(extent, crs, "MarsNPolek2MOLA5k")
Traceback (most recent call last):
File "", line 1, in
File "/Users/fcalef/miniconda3/envs/py37/lib/python3.7/site-packages/morecantile/models.py", line 157, in custom
width / (tile_width * matrix_scale[0]) / 2.0 ** zoom,
TypeError: unsupported operand type(s) for /: 'float' and 'str'

I don't see any obvious syntax mistakes.

Misleading tiles() iterator

The tiles() iterator for 'WebMercatorQuad' can return incorrect tile coordinates. The 'truncate' option in this case is misleading. The truncate_lnglat method it uses in utils.py is hard coded to bound coordinates to [-180, 180] and [-90, 90] irrespective of the TMS's bounds (in this case, correctly described in the warning).

I believe a less misleading interface would be to bound the coordinates by self.bbox in tiles().

Expected Result:

>>> import morecantile
>>> tms = morecantile.tms.get('WebMercatorQuad')
>>> tiles = list(tms.tiles(-180., -90., 180., 90., range(1), truncate=True))
<redacted>/.venv/lib/python3.8/site-packages/morecantile/models.py:450: PointOutsideTMSBounds: Point (-179.99999999999, 89.99999999999) is outside TMS bounds [-179.9999999999996, -85.05112877980656, 179.9999999999996, 85.05112877980656].
  warnings.warn(
<redacted>/.venv/lib/python3.8/site-packages/morecantile/models.py:450: PointOutsideTMSBounds: Point (179.99999999999, -89.99999999999) is outside TMS bounds [-179.9999999999996, -85.05112877980656, 179.9999999999996, 85.05112877980656].
  warnings.warn(
>>> tiles
[Tile(x=0, y=0, z=0)]

Actual Result:

>>> import morecantile
>>> tms = morecantile.tms.get('WebMercatorQuad')
>>> tiles = list(tms.tiles(-180., -90., 180., 90., range(1), truncate=True))
<redacted>/.venv/lib/python3.8/site-packages/morecantile/models.py:450: PointOutsideTMSBounds: Point (-179.99999999999, 89.99999999999) is outside TMS bounds [-179.9999999999996, -85.05112877980656, 179.9999999999996, 85.05112877980656].
  warnings.warn(
<redacted>/.venv/lib/python3.8/site-packages/morecantile/models.py:450: PointOutsideTMSBounds: Point (179.99999999999, -89.99999999999) is outside TMS bounds [-179.9999999999996, -85.05112877980656, 179.9999999999996, 85.05112877980656].
  warnings.warn(
>>> tiles
[Tile(x=0, y=0, z=0), Tile(x=0, y=1, z=0)]

`https://www.opengis.net/def/crs/EPSG/0/2193` is not a valid URI

"supportedCRS": "https://www.opengis.net/def/crs/EPSG/0/2193",

from pyproj import CRS
CRS.from_user_input("https://www.opengis.net/def/crs/EPSG/0/2193")
>>> CRSError: Invalid projection: https://www.opengis.net/def/crs/EPSG/0/2193: (Internal Proj Error: proj_create: crs not found)

# Works with rasterio + GDAL 3
import rasterio
rasterio.crs.CRS.from_user_input("https://www.opengis.net/def/crs/EPSG/0/2193")
>>> CRS.from_epsg(2193)


# Fails with GDAL 2 (rasterio==1.1.8)
CRS.from_user_input('https://www.opengis.net/def/crs/EPSG/0/2193')
Traceback (most recent call last):
  File "rasterio/_crs.pyx", line 428, in rasterio._crs._CRS.from_user_input
  File "rasterio/_err.pyx", line 202, in rasterio._err.exc_wrap_ogrerr
rasterio._err.CPLE_BaseError: OGR Error code 5

cc @blacha

Quadkey support

Tile matrix sets have the notion of X/Y/Z coordinates so technically we can generate a quadkey. However not all tile matrix sets have 4 children per parent which breaks the parent/child hierarchy. A good example of this is Canadian NAD83 LCC.

This all being said, I do think there are many benefits to supporting quadkeys:

  • serialization/deserialization of tiles
  • server side caching (generating unique per-tile cache keys)
  • fast parent/child lookups

I think this comes down to whether or not morecantile will support tile matrices which don't decimate by a factor of 4 (width or height is 2**zoom). From the README, docstrings, and general usage my thought is that it shouldn't, but that isn't entirely clear.

Custom CRS

in #22 an user wanted to used a custom CRS defined from a proj4 string

import morecantile 
from rasterio.crs import CRS 

crs = CRS.from_proj4("+proj=stere +lat_0=90 +lon_0=0 +k=2 +x_0=0 +y_0=0 +R=3396190 +units=m +no_defs") 
extent = [-13584760.000,-13585240.000,13585240.000,13584760.000] 
tms = morecantile.TileMatrixSet.custom(extent, crs, identifier="MarsNPolek2MOLA5k")  

While the TMS creation works, the internal representation of the CRS won't work because morecantile is trying to create an URI type

tms.supportedCRS                                                                                                                         
>  'http://www.opengis.net/def/crs/EPSG/0/None'

Because

crs = CRS.from_proj4("+proj=stere +lat_0=90 +lon_0=0 +k=2 +x_0=0 +y_0=0 +R=3396190 +units=m +no_defs") 
crs.to_epsg()
> None

I think we should default the tms.crs to the custom one instead of doing
inputCRS -> toEPSG -> toURI -> toCRS

cc @cirquelar

Use official TileMatrixSet JSON documents

Right now we use simple 3 elements (CRS, Extent, scale) to define TileMatrixSet.

"WorldCRS84Quad": {
"crs": CRS({"init": "EPSG:4326"}),
"extent": [-180, -90, 180, 90],
"matrix_scale": [2, 1],
},

But sometimes they can be more complexes e.g https://github.com/OSGeo/gdal/blob/93b48b00b13456f59c84670a9f95d544a1d76748/gdal/data/tms_LINZAntarticaMapTileGrid.json

The main idea is to host the TileMatrixSet as json (found in http://schemas.opengis.net/tms/1.0/json/examples/) and to use each zoom matrice to derive the information we need.

Note: This will align better with changes added in GDAL 3.2 https://github.com/OSGeo/gdal/pull/2505/files

Unknown

  • Can we use a pydantic model to describe the TileMatrixSet?
  • Do we need two classes TileMatrixSet (pydantic model) and TileSchema ?
  • is supportedCRS easily translatable to rasterio CRS? (e.g http://www.opengis.net/def/crs/EPSG/0/5482)

Define _resolution on the tile matrix

The current usage to get resolution of each matrix is:

tms = TileMatrixSet.load(...)
for matrix in tms.tileMatrix:
    res = tms._resolution(matrix)

I think it would be nicer to do

for matrix in tms.tileMatrix:
    res = matrix._resolution

Sort tile matrices by identifier

It would be nice if the pydantic model sorted the list of matrices by identifier or by scale.

From the spec:

A Tile Matrix has a unique alphanumeric identifier in the Tile Matrix Set. Some tile-based implementations prefer to use a zoom level number which has the advantage of suggesting some order in the list of tile matrices. This standard does not use the zoom level concept but, to ease adoption of this standard in implementations that prefer numeric zoom levels, many Tile Matrix Sets defined in Annex D use numbers as Tile Matrix identifiers. If this is not the case, the index order in the list of tile matrices defined in a Tile Matrix Set could still be used as a zoom levelorderinginternally.

TileMatrixSet iterable

I think it would be convenient to turn TileMatrixSet into an iterable so a user may more easily iterate over its matrices:

def __iter__(self):
    for matrix in self.tileMatrix:
        yield matrix

# usage
tms = TileMatrix.load(...)
for matrix in tms:
     ...

remove comments!

# tms_bounds = self._tms_bounds
for w, s, e, n in bboxes:
# w, s, e, n = transform_bounds(WGS84_CRS, self.crs, w, s, e, n, densify_pts=21)
# if not self.intersect_tms(w, s, e, n):
# continue
# w = max(tms_bounds[0], w)
# s = max(tms_bounds[1], s)
# e = min(tms_bounds[2], e)
# n = min(tms_bounds[3], n)

Align WebMercatorQuad TMS with mercantile and GDAL

as mentioned in #77 and #63 the WebMercatorQuad TMS which is aligned with the OGC doc is not as precise as the one used in GDAL or mercantile (it all comes down to the definition of the earth half size

# GDAL
https://github.com/OSGeo/gdal/blob/35c07b18316b4b6d238f6d60b82c31e25662ad27/gcore/tilematrixset.cpp#L79
>> 20037508.342789244

# Morecantile 
https://github.com/developmentseed/morecantile/blob/master/morecantile/data/WebMercatorQuad.json#L9
>> 20037508.3427892

Pro

  • align with mercantile
  • alight with GoogleMapsCompatible TMS

Cons

  • COG created with rio-cogeo's Web Optimized will have different origins (but with the change, they should be aligned with GDAL ones)
  • TiTiler tiles might be slightly different

cc @geospatial-jeff

Custom Tileset not returning bounds

I can't get xy bounds to return with my custom tile set:

import rasterio, morecantile
from rasterio.crs import CRS

crs = CRS.from_proj4("+proj=stere +lat_0=90 +lon_0=0 +k=2 +x_0=0 +y_0=0 +R=3396190 +units=m +no_defs")
extent = [-13584760.000,-13585240.000,13585240.000,13584760.000]
tms = morecantile.TileMatrixSet.custom(extent, crs, identifier="MarsNPolek2MOLA5k”)

print(tms.matrix(0).dict(exclude_none=True))
> {'type': 'TileMatrixType', 'identifier': '0', 'scaleDenominator': 42195180871278.51, 'topLeftCorner': (-13584760.0, 13584760.0), 'tileWidth': 256, 'tileHeight': 256, 'matrixWidth': 1, 'matrixHeight': 1}

morecantile.tms.register(tms)
tms = morecantile.tms.get("MarsNPolek2MOLA5k")
assert "MarsNPolek2MOLA5k" in morecantile.tms.list()

tms.xy_bounds(morecantile.Tile(10,10,4))
ERROR 6: EPSG PCS/GCS code 0 not found in EPSG support files.  Is this a valid EPSG coordinate system?
Traceback (most recent call last):
  File "rasterio/_crs.pyx", line 363, in rasterio._crs._CRS.from_user_input
  File "rasterio/_err.pyx", line 194, in rasterio._err.exc_wrap_ogrerr
rasterio._err.CPLE_BaseError: OGR Error code 7
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/fcalef/miniconda3/envs/py37/lib/python3.7/site-packages/morecantile/models.py", line 290, in xy_bounds
    left, top = self._ul(*tile)
  File "/Users/fcalef/miniconda3/envs/py37/lib/python3.7/site-packages/morecantile/models.py", line 271, in _ul
    res = self._resolution(matrix)
  File "/Users/fcalef/miniconda3/envs/py37/lib/python3.7/site-packages/morecantile/models.py", line 192, in _resolution
    return matrix.scaleDenominator * 0.28e-3 / meters_per_unit(self.crs)
  File "/Users/fcalef/miniconda3/envs/py37/lib/python3.7/site-packages/morecantile/models.py", line 66, in crs
    return CRS.from_user_input(self.supportedCRS)
  File "/Users/fcalef/miniconda3/envs/py37/lib/python3.7/site-packages/rasterio/crs.py", line 459, in from_user_input
    obj._crs = _CRS.from_user_input(value, morph_from_esri_dialect=morph_from_esri_dialect)
  File "rasterio/_crs.pyx", line 367, in rasterio._crs._CRS.from_user_input
rasterio.errors.CRSError: The WKT could not be parsed. OGR Error code 7

It seems it doesn't like my custom non-Earth projection that doesn't have an EPSG code. Is there a way I can assign it a fake one? Do I need to modify local EPSG list?

Switch to pyproj for new morecantile major release

in #58 we are proposing a major change to this library.

When we started the development of morecantile we choose to use rasterio only to avoid adding requirements like pyproj (because morecantile was mostly made to work with rio-tiler). In addition for a long time rasterio wheels were stuck at proj 6 while pyproj were already using proj>=7, so we wanted to avoid using coordinates coming from 2 different proj version.

Now that rasterio wheels uses proj>=7, if think we are in a better place to switch to pyproj and thus remove rasterio from this libraries.

The main motivation is trying to avoid some weird behaviour with coordinate re-projection which seems to be better handled by proj (with rasterio, you use GDAL which use proj while with pyproj you directly use Proj).

The PR is ready and the CI passes (still trying to match mercantile tests)

What it means for libraries using morecantile

Mostly nothing. The biggest breaking change is the type of CRS being returned by morecantile.TileMatrixSet.crs property, which is now of type pyproj.CRS instead of rasterio.crs.CRS. This is used in rio-tiler for example (https://github.com/cogeotiff/rio-tiler/search?l=Python&q=tms.crs). To help with the transition we are adding morecantile.TileMatrixSet.rasterio_crs property which will return a rasterio.crs.CRS. This method will only be available is rasterio is installed.

import morecantile

# before
tms = morecantile.tms.get("WebMercatorQuad")
crs = tms.crs

# now 
tms = morecantile.tms.get("WebMercatorQuad")
crs = tms.rasterio_crs

If no negative feedback, we'll do a pre-release next week

rename default grids to strings

in

4326: {"extent": [-180, -90, 180, 90], "matrix_scale": [2, 1]},

we use matrix_scale: [2, 1] to match the WorldCRS84Quad OGC simple profile definition (https://docs.opengeospatial.org/is/13-082r2/13-082r2.html)

ref: http://schemas.opengis.net/tms/1.0/xml/examples/

new names:

  • 4326 -> WorldCRS84Quad
  • 3395 -> WorldMercatorWGS84Quad
  • 3857 -> WebMercatorQuad
  • 3035 -> EuropeanETRS89_LAEAQuad
  • 3413 -> NSIDCSeaIcePolarQuad
  • 3031 -> NSIDCAntarcticPolarQuad

Add `minmax` and `neighbors`

mercantile==1.2a1 (changelog) added two new functions:

  • minmax: to get minimum and maximum x and y tile coordinates
  • neighbors: function to get adjacent tiles

These are quite useful, and I think should be added here as well.

Units other than meters and degrees

The referenced specification only considers Meters and Degrees. Would it be possible to consider other units? In the United States were have the State Plane Coordinate System with either defined units in "US Survey Foot" or "International Foot".

US Survey Foot = 0.3048006096
International Foot = 0.3048

I hard-coded the else statement to the US Survey Foot for EPSG:2276 data that I am working with just as a test. And I am very happy to have found success in properly tiling locally referenced imagery!

I don't know Python, but I could take a crack at adding a case statement for the defined units if there is no objection.

1.0 if crs.axis_info[0].unit_name == "metre" else 2 * math.pi * 6378137 / 360.0

From note g in http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#table_2:
If the CRS uses meters as units of measure for the horizontal dimensions,
then metersPerUnit=1; if it has degrees, then metersPerUnit=2pa/360
(a is the Earth maximum radius of the ellipsoid).

Extent a TMS when we need more zoom level ?

in

def matrix(self, zoom: int) -> TileMatrix:
"""Return the TileMatrix for a specific zoom."""
try:
return list(filter(lambda m: m.identifier == str(zoom), self.tileMatrix))[0]
except IndexError:
raise Exception(f"TileMatrix not found for level: {zoom}")
morecantile will fail if we are trying to fetch a TileMatrix for a level that is not in the TMS document.

e.g WebMercatorQuad stops at zoom 24 will @geospatial-jeff has data up to zoom 29

How To

https://github.com/vincentsarago/TileMatrixSets/blob/03b35bfe4a297551424c1eb71a775dfa47cff5c1/WorldCRS84Quad.json#L228-L253

Just takes the last tilematrix and do scaleDenominator/2, matrixWidth * 2, matrixHeight * 2

This will not work for variable scales ...

Wrong `_invert_axis` method result for `WorldCRS84Quad` TMS

import morecantile
tms = morecantile.tms.get("WorldCRS84Quad")
tms._invert_axis
>> True

While we can clearly see in the axis info that the order is Lon,Lat

tms.crs.axis_info
>> 
[Axis(name=Geodetic longitude, abbrev=Lon, direction=east, unit_auth_code=EPSG, unit_code=9122, unit_name=degree),
 Axis(name=Geodetic latitude, abbrev=Lat, direction=north, unit_auth_code=EPSG, unit_code=9122, unit_name=degree)]

FastAPI is not happy

crs: Union[CRS, AnyHttpUrl]

f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type"

fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that typing.Union[rasterio.crs.CRS, pydantic.networks.AnyHttpUrl] is a valid pydantic field type

Tests fails under rasterio==1.1.5

ref #29

xy = (-28366731.739810849, -1655181.9927159143)
lnglat = tms.lnglat(*xy, truncate=True)
# GDAL returns ('inf', 'inf') and then inf is translated to 180,90 by truncate_lnglat
# assert round(lnglat.x, 5) == -180.0 # in Mercantile
# assert round(lnglat.y, 5) == -14.70462 # in Mercantile
assert round(lnglat.x, 5) == 180.0
assert round(lnglat.y, 5) == 90

This ☝️ is failing because of change between rasterio 1.1.4 and rasterio 1.1.4

## rasterio 1.1.4
import numpy
from rasterio.crs import CRS
from rasterio.warp import transform
x, y = -28366731.739810849, -1655181.9927159143
xs, ys = transform(CRS.from_epsg(3857), CRS.from_epsg(4326), [x], [y])

assert xs[0] == numpy.inf  # OK
assert ys[0] == numpy.inf  # OK
## rasterio 1.1.5
import numpy
from rasterio.crs import CRS
from rasterio.warp import transform
x, y = -28366731.739810849, -1655181.9927159143
xs, ys = transform(CRS.from_epsg(3857), CRS.from_epsg(4326), [x], [y])

assert xs[0] == numpy.inf  # NOK -> xs[0] == 105.1773131760957
assert ys[0] == numpy.inf  # NOK -> ys[0] == -14.70462000000001

This might be linked to rasterio/rasterio#1942 because when using CHECK_WITH_INVERT_PROJ=True we get the expected result

import numpy 
import rasterio 
from rasterio.crs import CRS 
from rasterio.warp import transform 
x, y = -28366731.739810849, -1655181.9927159143 
with rasterio.Env(CHECK_WITH_INVERT_PROJ=True): 
    xs, ys = transform(CRS.from_epsg(3857), CRS.from_epsg(4326), [x], [y]) 
    
assert xs[0] == numpy.inf  # OK
assert ys[0] == numpy.inf  # OK

cc @sgillies

Non-Earth TMS definitions

I'm trying to use this library to create TileMatrixSet definitions (ultimately for cogeo-mosaic, titiler and friends) for non-Earth (Mars) coordinate reference systems. Most aspects of the math are exactly the same, but recent Proj has gotten strict about ellipsoids. This is a good thing for most users, but the "Cannot find coordinate operations from 'EPSG:4326' to XXX" has become a source of dread for planetary scientists ;)

I've been able to successfully adapt the TileMatrixSet definition to mostly work for the "Mars 2000" CRS, but I had to reimplement more than I ideally would to simply change the CRS:

MARS2000_SPHERE = CRS.from_dict(
    {"proj": "longlat", "R": 3396190, "no_defs": True}
)

class MarsTMS(TileMatrixSet):
    _to_mars2000 = PrivateAttr()
    _from_mars2000 = PrivateAttr()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Mars 2000 sphere can be used because lat/lon are defined
        # as planetocentric on Mars
        self._to_mars2000 = Transformer.from_crs(
            self.supportedCRS, MARS2000_SPHERE, always_xy=True
        )
        self._from_mars2000 = Transformer.from_crs(
            MARS2000_SPHERE, self.supportedCRS, always_xy=True
        )

    @property
    def bbox(self):
        """Return TMS bounding box in WGS84."""
        bbox = self._to_mars2000.transform_bounds(*self.xy_bbox, densify_pts=21)
        return BoundingBox(*bbox)

    def xy(self, lng: float, lat: float, truncate=False) -> Coords:
        """Transform longitude and latitude coordinates to TMS CRS."""
        if truncate:
            lng, lat = truncate_lnglat(lng, lat)

        inside = point_in_bbox(Coords(lng, lat), self.bbox)
        if not inside:
            warnings.warn(
                f"Point ({lng}, {lat}) is outside TMS bounds {list(self.bbox)}.",
                PointOutsideTMSBounds,
            )

        x, y = self._from_mars2000.transform(lng, lat)

        return Coords(x, y)

The lengthy redefinition is needed to replace the _to_wgs84 and _from_wgs84 definitions. Ideally, these could be replaced by _to_geographic and _from_geographic methods, with the geographic coordinate system being user-supplied (WGS84 by default). This could also help with other planets and other reasons people might define a custom ellipsoid.

If you think I'm missing a simpler way to achieve this, please let me know. If not, I'm happy to supply a PR.

Werid bounding boxes in NZTM

When using NZTM TileMatrixSet the numbers appear to be off.

import morecantile

nztm = morecantile.tms.get('NZTM2000')
print(nztm.xy_bounds(morecantile.Tile(1, 2, 0)))

returns

CoordsBbox(xmin=12293760.0, ymin=-7881280.0, xmax=14587520.0, ymax=-5587520.0)

When viewing this tile inside of QGIS the top left point is { x: 1293760, y: 5412480 }

I believe this is caused by https://github.com/developmentseed/morecantile/blob/master/morecantile/models.py#L305 as NZTM is defined as [y, x] rather than [x, y]

backport of features to 3.y.z versions

for developmentseed/cogeo-mosaic#210, I would like to update the pr to use the changes from #109 and #111 but cogeo-mosaic pins to versions of morecantile older than 4.0.0. Maybe the changes for tms 2.0 aren't too disruptive and cogeo-mosaic can be updated to use the alpha release, but a backport 3.3.1 or 3.4.0 release with these changes and any others that have occurred since 3.3.0 would be preferable for now I think to make the upgrade path smoother for other projects currently using 3.3.0 morecantile.

Pip install error

Hello, since the version bump to 3.1.0 we are getting the following output from pip. It seems like there's an issue with the package? Everything was fine under 3.0.5. Thanks for your help.

#!/bin/bash -eo pipefail
sudo pip3 install cogeo-mosaic
Collecting cogeo-mosaic
  Downloading cogeo-mosaic-4.0.0.tar.gz (23 kB)
  Preparing metadata (setup.py) ... -οΏ½ οΏ½done
Collecting attrs
  Downloading attrs-21.4.0-py2.py3-none-any.whl (60 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 60.6/60.6 KB 32.0 MB/s eta 0:00:00

Collecting cachetools
  Downloading cachetools-5.0.0-py3-none-any.whl (9.1 kB)
Collecting httpx
  Downloading httpx-0.22.0-py3-none-any.whl (84 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 84.2/84.2 KB 39.5 MB/s eta 0:00:00

Collecting mercantile
  Downloading mercantile-1.2.1-py3-none-any.whl (14 kB)
Collecting morecantile<4.0,>=3.0
  Downloading morecantile-3.1.0.tar.gz (25 kB)
  Preparing metadata (setup.py) ... -οΏ½ οΏ½error
  error: subprocess-exited-with-error
  
  Γ— python setup.py egg_info did not run successfully.
  β”‚ exit code: 1
  ╰─> [36 lines of output]
      Traceback (most recent call last):
        File "<string>", line 36, in <module>
        File "<pip-setuptools-caller>", line 34, in <module>
        File "/tmp/pip-install-e4svnxu4/morecantile_2e4a53c9883345079bf39a2a4ca2e893/setup.py", line 45, in <module>
          "morecantile=morecantile.scripts.cli:cli",
        File "/usr/lib/python3/dist-packages/setuptools/__init__.py", line 145, in setup
          return distutils.core.setup(**attrs)
        File "/usr/lib/python3.7/distutils/core.py", line 121, in setup
          dist.parse_config_files()
        File "/usr/lib/python3/dist-packages/setuptools/dist.py", line 705, in parse_config_files
          ignore_option_errors=ignore_option_errors)
        File "/usr/lib/python3/dist-packages/setuptools/config.py", line 120, in parse_configuration
          meta.parse()
        File "/usr/lib/python3/dist-packages/setuptools/config.py", line 425, in parse
          section_parser_method(section_options)
        File "/usr/lib/python3/dist-packages/setuptools/config.py", line 398, in parse_section
          self[name] = value
        File "/usr/lib/python3/dist-packages/setuptools/config.py", line 183, in __setitem__
          value = parser(value)
        File "/usr/lib/python3/dist-packages/setuptools/config.py", line 513, in _parse_version
          version = self._parse_attr(value, self.package_dir)
        File "/usr/lib/python3/dist-packages/setuptools/config.py", line 348, in _parse_attr
          module = import_module(module_name)
        File "/usr/lib/python3.7/importlib/__init__.py", line 127, in import_module
          return _bootstrap._gcd_import(name[level:], package, level)
        File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
        File "<frozen importlib._bootstrap>", line 983, in _find_and_load
        File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
        File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
        File "<frozen importlib._bootstrap_external>", line 728, in exec_module
        File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
        File "/tmp/pip-install-e4svnxu4/morecantile_2e4a53c9883345079bf39a2a4ca2e893/morecantile/__init__.py", line 14, in <module>
          from .defaults import tms  # noqa
        File "/tmp/pip-install-e4svnxu4/morecantile_2e4a53c9883345079bf39a2a4ca2e893/morecantile/defaults.py", line 8, in <module>
          import attr
      ModuleNotFoundError: No module named 'attr'
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: metadata-generation-failed

Γ— Encountered error while generating package metadata.
╰─> See above for output.

note: This is an issue with the package mentioned above, not pip.
hint: See above for details.


Exited with code exit status 1

NZTM2000 TMS fails test if BBox is removed

If you remove the "boundingBox" section from the NZTM2000.json TMS definition, the following test fails

def test_InvertedLatLonGrids():
        """Check Inverted LatLon grids."""
        tms = morecantile.tms.get("NZTM2000")
        bound = tms.xy_bounds(morecantile.Tile(1, 2, 0))
        assert bound == (1293760.0, 3118720.0, 3587520.0, 5412480.0)
>       assert tms.xy_bbox == (274000.0, 3087000.0, 3327000.0, 7173000.0)
E       assert BoundingBox(left=-1000000.0, bottom=824960.0, right=3587520.0, top=10000000.0) == (274000.0, 3087000.0, 3327000.0, 7173000.0)
E         At index 0 diff: -1000000.0 != 274000.0
E         Full diff:
E         - (274000.0, 3087000.0, 3327000.0, 7173000.0)
E         + BoundingBox(left=-1000000.0, bottom=824960.0, right=3587520.0, top=10000000.0)

This is important because the boundingBox should be able to be calculated without an explicit definition. In TMS 2.0, the boundingBox property is optional, so we can't rely upon the explicit definition.

Steps to reproduce:

  1. Clone main branch
  2. Remove the boundingBox property from the top of NZTM2000.json
  3. Run pytest

Is there something basic about TMS definitions that I'm missing? And is this an issue better suited for the definition repo at LINZ? Thanks!

Change how `TileMatrixSet.bbox` is calculated

right now we get the TileMatrixSet bbox either by using boundingBox or by getting the extent of the tms's first matrix.

In TMS 2.0, boundingBox is optional and from the wording used in the spec it seems we shouldn't rely on the boundingBox anyway:

The boundingBox property was made optional, highlighting the fact that the space occupied by tiles is really defined by the pointOfOrigin as well as the scaleDenominator / resolution, and the matrixWidth and matrixHeight of each TileMatrix, not the boundingBox of the overall TileMatrixSet. Examples were updated to not define the bounding box, which should not be relied upon by clients.

@property
def xy_bbox(self):
"""Return TMS bounding box in TileMatrixSet's CRS."""
if self.boundingBox:
left = (
self.boundingBox.lowerCorner[1]
if self._invert_axis
else self.boundingBox.lowerCorner[0]
)
bottom = (
self.boundingBox.lowerCorner[0]
if self._invert_axis
else self.boundingBox.lowerCorner[1]
)
right = (
self.boundingBox.upperCorner[1]
if self._invert_axis
else self.boundingBox.upperCorner[0]
)
top = (
self.boundingBox.upperCorner[0]
if self._invert_axis
else self.boundingBox.upperCorner[1]
)
if self.boundingBox.crs != self.crs:
transform = Transformer.from_crs(
self.boundingBox.crs, self.crs, always_xy=True
)
left, bottom, right, top = transform.transform_bounds(
left,
bottom,
right,
top,
densify_pts=21,
)
else:
zoom = self.minzoom
matrix = self.matrix(zoom)
left, top = self._ul(Tile(0, 0, zoom))
right, bottom = self._ul(
Tile(matrix.matrixWidth, matrix.matrixHeight, zoom)
)
return BoundingBox(left, bottom, right, top)

ref: #103

make default TMS immutable

ref: developmentseed/titiler#137

class DefaultTileMatrixSets(object):
"""Default TileMatrixSets holder."""
def __init__(self):
"""Load default TMS in a dict."""
tms_paths = list(pathlib.Path(morecantile_tms_dir).glob("*.json"))
if user_tms_dir:
tms_paths.extend(list(pathlib.Path(user_tms_dir).glob("*.json")))
self._data: Dict[str, TileMatrixSet] = {
tms.stem: TileMatrixSet.parse_file(tms) for tms in tms_paths
}
def get(self, identifier: str) -> TileMatrixSet:
"""Fetch a TMS."""
if identifier not in self._data:
raise InvalidIdentifier(f"Invalid identifier: {identifier}")
return self._data[identifier]
def list(self) -> List[str]:
"""List registered TMS."""
return list(self._data.keys())
def register(self, custom_tms: TileMatrixSet):
"""Register a custom TileMatrixSet."""
self._data[custom_tms.identifier] = custom_tms
tms = DefaultTileMatrixSets() # noqa

Note: same kind of discussion over matplotlib/matplotlib#16296 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.