├── .gitignore
├── .travis.yml
├── LICENSE.txt
├── README.rst
├── docs
├── Makefile
├── cli.rst
├── conf.py
├── index.rst
├── motivation.rst
├── tiletanic.rst
└── user_guide.rst
├── requirements-dev.txt
├── setup.py
├── tests
├── test_basictilingbottomleft.py
├── test_basictilingtopleft.py
├── test_cli_main.py
├── test_cli_tilecover.py
├── test_dgtiling.py
├── test_tilecover.py
├── test_utmtiling.py
├── test_webmercator.py
└── test_webmercatorbl.py
├── tiletanic.info.yml
└── tiletanic
├── __init__.py
├── base.py
├── cli.py
├── tilecover.py
└── tileschemes.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /venv/
2 | *.pyc
3 | /.cache*
4 | *.cache*
5 | *.egg_info*
6 | .egg-info*
7 | *.egg-info*
8 | docenv/
9 | docs/_build/
10 | docs/_build
11 | docs/_build*
12 | /build/
13 | /dist/
14 | reqenv/
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "3.6"
5 | - "3.7"
6 |
7 | before_install:
8 | - pip install -r requirements-dev.txt
9 |
10 | install:
11 | - pip install -e .
12 |
13 | script: "py.test tests"
14 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Tiletanic: Tools for Manipulating Geospatial Tiling Schemes
2 | ===========================================================
3 |
4 | .. image:: https://travis-ci.org/DigitalGlobe/tiletanic.svg?branch=master
5 | :target: https://travis-ci.org/DigitalGlobe/tiletanic
6 |
7 |
8 |
9 | Tiletanic is a library for making and using geospatial tiling schemes. It's goal is to provide tooling for dealing with the conversion between a tile specified in as (row, column, zoom level), geospatial coordinates, or quadkeys. It also provides functionaly for taking an input geometry and figuring out what tiles cover it.
10 |
11 | Tiletanic is MIT licensed.
12 |
13 | Full documentation is hosted `here `_.
14 |
15 | Installation is easy::
16 |
17 | pip install tiletanic
18 |
19 | The only dependency is shapely_, and to install that, you'll need GEOS_ installed (usually in your package manager).
20 |
21 | Motivation
22 | ----------
23 |
24 | You may be familar with the `Web Mercator`_ (or Spherical Mercator, Google Tiles, etc) tiling scheme, which is the most commonly encountered tiling scheme on the web. There are good tools out there for dealing specifically with this projection, for instance Mercantile_ fits the bill. If you're dealing exclusively with this projection, you might get better mileage with them!
25 |
26 | One oddity of Web Mercator is that coordinates are usually expressed in geographic (longitude, latitude) coordinates that live on the ellipsoid (WGS84) rather than in units of the Web Mercator projection (meters). Dealing with this kind of conversion is exactly what Mercantile_ and its like were made to handle.
27 |
28 | Tiletanic's use cases are a bit different:
29 |
30 | - What do you do if your data is already in some projection? For instance, you might want to know what tile covers a point specified in the Web Mercator projection without first converting back to WGS84. Tiletanic can help.
31 | - At DigitalGlobe_, our imagery is often projected into many other projections (UTM, Geographic, etc) and it is often times extremely convienent to organize raster data into a tiled format before proceeding with processing (a single "strip" from one of our satellite collects can easily be 100km long!). When dealing with different projections, the user typically needs to impose their own tiling scheme. Tiletanic provides an easy way for you to define you're own scheme and get a lot of tiling functionality right out of the gate.
32 |
33 | .. _`Web Mercator`: https://en.wikipedia.org/wiki/Web_Mercator
34 | .. _Mercantile: https://github.com/mapbox/mercantile
35 | .. _DigitalGlobe: https://www.digitalglobe.com/
36 | .. _shapely: https://github.com/Toblerity/Shapely
37 | .. _GEOS: http://geos.osgeo.org/doxygen/
38 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " applehelp to make an Apple Help Book"
34 | @echo " devhelp to make HTML files and a Devhelp project"
35 | @echo " epub to make an epub"
36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
37 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
39 | @echo " text to make text files"
40 | @echo " man to make manual pages"
41 | @echo " texinfo to make Texinfo files"
42 | @echo " info to make Texinfo files and run them through makeinfo"
43 | @echo " gettext to make PO message catalogs"
44 | @echo " changes to make an overview of all changed/added/deprecated items"
45 | @echo " xml to make Docutils-native XML files"
46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
47 | @echo " linkcheck to check all external links for integrity"
48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
49 | @echo " coverage to run coverage check of the documentation (if enabled)"
50 |
51 | clean:
52 | rm -rf $(BUILDDIR)/*
53 |
54 | html:
55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
56 | @echo
57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
58 |
59 | dirhtml:
60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
61 | @echo
62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
63 |
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | pickle:
70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
71 | @echo
72 | @echo "Build finished; now you can process the pickle files."
73 |
74 | json:
75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
76 | @echo
77 | @echo "Build finished; now you can process the JSON files."
78 |
79 | htmlhelp:
80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
81 | @echo
82 | @echo "Build finished; now you can run HTML Help Workshop with the" \
83 | ".hhp project file in $(BUILDDIR)/htmlhelp."
84 |
85 | qthelp:
86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
87 | @echo
88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/tiletanic.qhcp"
91 | @echo "To view the help file:"
92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/tiletanic.qhc"
93 |
94 | applehelp:
95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
96 | @echo
97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
98 | @echo "N.B. You won't be able to view it unless you put it in" \
99 | "~/Library/Documentation/Help or install it in your application" \
100 | "bundle."
101 |
102 | devhelp:
103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
104 | @echo
105 | @echo "Build finished."
106 | @echo "To view the help file:"
107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/tiletanic"
108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/tiletanic"
109 | @echo "# devhelp"
110 |
111 | epub:
112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
113 | @echo
114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
115 |
116 | latex:
117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
118 | @echo
119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
121 | "(use \`make latexpdf' here to do that automatically)."
122 |
123 | latexpdf:
124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
125 | @echo "Running LaTeX files through pdflatex..."
126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
128 |
129 | latexpdfja:
130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
131 | @echo "Running LaTeX files through platex and dvipdfmx..."
132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
134 |
135 | text:
136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
137 | @echo
138 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
139 |
140 | man:
141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
142 | @echo
143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
144 |
145 | texinfo:
146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
147 | @echo
148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
149 | @echo "Run \`make' in that directory to run these through makeinfo" \
150 | "(use \`make info' here to do that automatically)."
151 |
152 | info:
153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
154 | @echo "Running Texinfo files through makeinfo..."
155 | make -C $(BUILDDIR)/texinfo info
156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
157 |
158 | gettext:
159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
160 | @echo
161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
162 |
163 | changes:
164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
165 | @echo
166 | @echo "The overview file is in $(BUILDDIR)/changes."
167 |
168 | linkcheck:
169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
170 | @echo
171 | @echo "Link check complete; look for any errors in the above output " \
172 | "or in $(BUILDDIR)/linkcheck/output.txt."
173 |
174 | doctest:
175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
176 | @echo "Testing of doctests in the sources finished, look at the " \
177 | "results in $(BUILDDIR)/doctest/output.txt."
178 |
179 | coverage:
180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
181 | @echo "Testing of coverage in the sources finished, look at the " \
182 | "results in $(BUILDDIR)/coverage/python.txt."
183 |
184 | xml:
185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
186 | @echo
187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
188 |
189 | pseudoxml:
190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
191 | @echo
192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
193 |
--------------------------------------------------------------------------------
/docs/cli.rst:
--------------------------------------------------------------------------------
1 | Command Line Interface
2 | ======================
3 |
4 | Tiletanic's command line interface is a program named `tiletanic`.
5 | Execute `tiletanic --help` for more information.
6 |
7 | cover_geometry
8 | --------------
9 |
10 | Added in 0.0.5
11 |
12 | Given an area of interest, calculate the tile covering at a particular
13 | zoom level. Execute `tiletanic cover_geometry --help` for details.
14 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # tiletanic documentation build configuration file, created by
4 | # sphinx-quickstart on Mon Nov 2 15:35:27 2015.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import sys
16 | import os
17 | import shlex
18 |
19 | # Below is some juice to keep ReadTheDocks from failing due to the
20 | # shapely dependency (it can't handle a c extension).
21 | from mock import Mock as MagicMock
22 |
23 | class Mock(MagicMock):
24 | @classmethod
25 | def __getattr__(cls, name):
26 | return Mock()
27 |
28 | MOCK_MODULES = ['shapely']
29 | sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
30 |
31 | # If extensions (or modules to document with autodoc) are in another directory,
32 | # add these directories to sys.path here. If the directory is relative to the
33 | # documentation root, use os.path.abspath to make it absolute, like shown here.
34 | sys.path.insert(0, os.path.abspath('..'))
35 |
36 | # -- General configuration ------------------------------------------------
37 |
38 | # If your documentation needs a minimal Sphinx version, state it here.
39 | #needs_sphinx = '1.0'
40 |
41 | # Add any Sphinx extension module names here, as strings. They can be
42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
43 | # ones.
44 | extensions = [
45 | 'sphinx.ext.autodoc', 'sphinx.ext.napoleon'
46 | ]
47 |
48 | # Add any paths that contain templates here, relative to this directory.
49 | templates_path = ['_templates']
50 |
51 | # The suffix(es) of source filenames.
52 | # You can specify multiple suffix as a list of string:
53 | # source_suffix = ['.rst', '.md']
54 | source_suffix = '.rst'
55 |
56 | # The encoding of source files.
57 | #source_encoding = 'utf-8-sig'
58 |
59 | # The master toctree document.
60 | master_doc = 'index'
61 |
62 | # General information about the project.
63 | project = u'Tiletanic'
64 | copyright = u'2015, DigitalGlobe'
65 | author = u'Patrick Young'
66 |
67 | # The version info for the project you're documenting, acts as replacement for
68 | # |version| and |release|, also used in various other places throughout the
69 | # built documents.
70 | #
71 | # The short X.Y version.
72 | version = '0.0.5'
73 | # The full version, including alpha/beta/rc tags.
74 | release = '0.0.5'
75 |
76 | # The language for content autogenerated by Sphinx. Refer to documentation
77 | # for a list of supported languages.
78 | #
79 | # This is also used if you do content translation via gettext catalogs.
80 | # Usually you set "language" from the command line for these cases.
81 | language = None
82 |
83 | # There are two options for replacing |today|: either, you set today to some
84 | # non-false value, then it is used:
85 | #today = ''
86 | # Else, today_fmt is used as the format for a strftime call.
87 | #today_fmt = '%B %d, %Y'
88 |
89 | # List of patterns, relative to source directory, that match files and
90 | # directories to ignore when looking for source files.
91 | exclude_patterns = ['_build']
92 |
93 | # The reST default role (used for this markup: `text`) to use for all
94 | # documents.
95 | #default_role = None
96 |
97 | # If true, '()' will be appended to :func: etc. cross-reference text.
98 | #add_function_parentheses = True
99 |
100 | # If true, the current module name will be prepended to all description
101 | # unit titles (such as .. function::).
102 | #add_module_names = True
103 |
104 | # If true, sectionauthor and moduleauthor directives will be shown in the
105 | # output. They are ignored by default.
106 | #show_authors = False
107 |
108 | # The name of the Pygments (syntax highlighting) style to use.
109 | pygments_style = 'sphinx'
110 |
111 | # A list of ignored prefixes for module index sorting.
112 | #modindex_common_prefix = []
113 |
114 | # If true, keep warnings as "system message" paragraphs in the built documents.
115 | #keep_warnings = False
116 |
117 | # If true, `todo` and `todoList` produce output, else they produce nothing.
118 | todo_include_todos = False
119 |
120 |
121 | # -- Options for HTML output ----------------------------------------------
122 |
123 | # The theme to use for HTML and HTML Help pages. See the documentation for
124 | # a list of builtin themes.
125 | html_theme = 'sphinx_rtd_theme'
126 |
127 | # Theme options are theme-specific and customize the look and feel of a theme
128 | # further. For a list of options available for each theme, see the
129 | # documentation.
130 | #html_theme_options = {}
131 |
132 | # Add any paths that contain custom themes here, relative to this directory.
133 | #html_theme_path = []
134 |
135 | # The name for this set of Sphinx documents. If None, it defaults to
136 | # " v documentation".
137 | #html_title = None
138 |
139 | # A shorter title for the navigation bar. Default is the same as html_title.
140 | #html_short_title = None
141 |
142 | # The name of an image file (relative to this directory) to place at the top
143 | # of the sidebar.
144 | #html_logo = None
145 |
146 | # The name of an image file (within the static path) to use as favicon of the
147 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
148 | # pixels large.
149 | #html_favicon = None
150 |
151 | # Add any paths that contain custom static files (such as style sheets) here,
152 | # relative to this directory. They are copied after the builtin static files,
153 | # so a file named "default.css" will overwrite the builtin "default.css".
154 | html_static_path = ['_static']
155 |
156 | # Add any extra paths that contain custom files (such as robots.txt or
157 | # .htaccess) here, relative to this directory. These files are copied
158 | # directly to the root of the documentation.
159 | #html_extra_path = []
160 |
161 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
162 | # using the given strftime format.
163 | #html_last_updated_fmt = '%b %d, %Y'
164 |
165 | # If true, SmartyPants will be used to convert quotes and dashes to
166 | # typographically correct entities.
167 | #html_use_smartypants = True
168 |
169 | # Custom sidebar templates, maps document names to template names.
170 | #html_sidebars = {}
171 |
172 | # Additional templates that should be rendered to pages, maps page names to
173 | # template names.
174 | #html_additional_pages = {}
175 |
176 | # If false, no module index is generated.
177 | #html_domain_indices = True
178 |
179 | # If false, no index is generated.
180 | #html_use_index = True
181 |
182 | # If true, the index is split into individual pages for each letter.
183 | #html_split_index = False
184 |
185 | # If true, links to the reST sources are added to the pages.
186 | #html_show_sourcelink = True
187 |
188 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
189 | #html_show_sphinx = True
190 |
191 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
192 | #html_show_copyright = True
193 |
194 | # If true, an OpenSearch description file will be output, and all pages will
195 | # contain a tag referring to it. The value of this option must be the
196 | # base URL from which the finished HTML is served.
197 | #html_use_opensearch = ''
198 |
199 | # This is the file name suffix for HTML files (e.g. ".xhtml").
200 | #html_file_suffix = None
201 |
202 | # Language to be used for generating the HTML full-text search index.
203 | # Sphinx supports the following languages:
204 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
205 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
206 | #html_search_language = 'en'
207 |
208 | # A dictionary with options for the search language support, empty by default.
209 | # Now only 'ja' uses this config value
210 | #html_search_options = {'type': 'default'}
211 |
212 | # The name of a javascript file (relative to the configuration directory) that
213 | # implements a search results scorer. If empty, the default will be used.
214 | #html_search_scorer = 'scorer.js'
215 |
216 | # Output file base name for HTML help builder.
217 | htmlhelp_basename = 'tiletanicdoc'
218 |
219 | # -- Options for LaTeX output ---------------------------------------------
220 |
221 | latex_elements = {
222 | # The paper size ('letterpaper' or 'a4paper').
223 | #'papersize': 'letterpaper',
224 |
225 | # The font size ('10pt', '11pt' or '12pt').
226 | #'pointsize': '10pt',
227 |
228 | # Additional stuff for the LaTeX preamble.
229 | #'preamble': '',
230 |
231 | # Latex figure (float) alignment
232 | #'figure_align': 'htbp',
233 | }
234 |
235 | # Grouping the document tree into LaTeX files. List of tuples
236 | # (source start file, target name, title,
237 | # author, documentclass [howto, manual, or own class]).
238 | latex_documents = [
239 | (master_doc, 'tiletanic.tex', u'tiletanic Documentation',
240 | u'Patrick Young', 'manual'),
241 | ]
242 |
243 | # The name of an image file (relative to this directory) to place at the top of
244 | # the title page.
245 | #latex_logo = None
246 |
247 | # For "manual" documents, if this is true, then toplevel headings are parts,
248 | # not chapters.
249 | #latex_use_parts = False
250 |
251 | # If true, show page references after internal links.
252 | #latex_show_pagerefs = False
253 |
254 | # If true, show URL addresses after external links.
255 | #latex_show_urls = False
256 |
257 | # Documents to append as an appendix to all manuals.
258 | #latex_appendices = []
259 |
260 | # If false, no module index is generated.
261 | #latex_domain_indices = True
262 |
263 |
264 | # -- Options for manual page output ---------------------------------------
265 |
266 | # One entry per manual page. List of tuples
267 | # (source start file, name, description, authors, manual section).
268 | man_pages = [
269 | (master_doc, 'tiletanic', u'tiletanic Documentation',
270 | [author], 1)
271 | ]
272 |
273 | # If true, show URL addresses after external links.
274 | #man_show_urls = False
275 |
276 |
277 | # -- Options for Texinfo output -------------------------------------------
278 |
279 | # Grouping the document tree into Texinfo files. List of tuples
280 | # (source start file, target name, title, author,
281 | # dir menu entry, description, category)
282 | texinfo_documents = [
283 | (master_doc, 'tiletanic', u'tiletanic Documentation',
284 | author, 'tiletanic', 'One line description of project.',
285 | 'Miscellaneous'),
286 | ]
287 |
288 | # Documents to append as an appendix to all manuals.
289 | #texinfo_appendices = []
290 |
291 | # If false, no module index is generated.
292 | #texinfo_domain_indices = True
293 |
294 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
295 | #texinfo_show_urls = 'footnote'
296 |
297 | # If true, do not generate a @detailmenu in the "Top" node's menu.
298 | #texinfo_no_detailmenu = False
299 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. tiletanic documentation master file, created by
2 | sphinx-quickstart on Mon Nov 2 15:35:27 2015.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Tiletanic: Tools for Manipulating Geospatial Tiling Schemes
7 | ===========================================================
8 |
9 | Tiletanic is a library for making and using geospatial tiling schemes. It's goal is to provide tooling for dealing with the conversion between a tile specified in as (row, column, zoom level), geospatial coordinates, or quadkeys. It also provides functionaly for taking an input geometry and figuring out what tiles cover it.
10 |
11 | Tiletanic is MIT licensed.
12 |
13 | Contributions are more than welcome! Please check out the `GitHub site`_.
14 |
15 | Installation is easy::
16 |
17 | pip install tiletanic
18 |
19 | The only dependency is shapely_, and to install that, you'll need GEOS_ installed (usually in your package manager).
20 |
21 | .. toctree::
22 | :maxdepth: 2
23 |
24 | motivation
25 | user_guide
26 | tiletanic
27 | cli
28 |
29 | .. _`GitHub site`: https://github.com/DigitalGlobe/tiletanic
30 | .. _shapely: https://github.com/Toblerity/Shapely
31 | .. _GEOS: http://geos.osgeo.org/doxygen/
32 |
--------------------------------------------------------------------------------
/docs/motivation.rst:
--------------------------------------------------------------------------------
1 | Motivation
2 | ==========
3 |
4 | You may be familar with the `Web Mercator`_ (or Spherical Mercator, Google Tiles, etc) tiling scheme, which is the most commonly encountered tiling scheme on the web. There are good tools out there for dealing specifically with this projection, for instance Mercantile_ fits the bill. If you're dealing exclusively with this projection, you might get better mileage with them!
5 |
6 | One oddity of Web Mercator is that coordinates are usually expressed in geographic (longitude, latitude) coordinates that live on the ellipsoid (WGS84) rather than in units of the Web Mercator projection (meters). Dealing with this kind of conversion is exactly what Mercantile_ and its like were made to handle.
7 |
8 | Tiletanic's use cases are a bit different:
9 |
10 | - What do you do if your data is already in some projection? For instance, you might want to know what tile covers a point specified in the Web Mercator projection without first converting back to WGS84. Tiletanic can help.
11 | - At DigitalGlobe_, our imagery is often projected into many other projections (UTM, Geographic, etc) and it is often times extremely convienent to organize raster data into a tiled format before proceeding with processing (a single "strip" from one of our satellite collects can easily be 100km long!). When dealing with different projections, the user typically needs to impose their own tiling scheme. Tiletanic provides an easy way for you to define you're own scheme and get a lot of tiling functionality right out of the gate.
12 |
13 | .. _`Web Mercator`: https://en.wikipedia.org/wiki/Web_Mercator
14 | .. _Mercantile: https://github.com/mapbox/mercantile
15 | .. _DigitalGlobe: https://www.digitalglobe.com/
16 |
--------------------------------------------------------------------------------
/docs/tiletanic.rst:
--------------------------------------------------------------------------------
1 | API Documentation
2 | =======================
3 |
4 | tiletanic.base module
5 | ---------------------
6 |
7 | .. automodule:: tiletanic.base
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | tiletanic.tilecover module
13 | --------------------------
14 |
15 | .. automodule:: tiletanic.tilecover
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
20 | tiletanic.tileschemes module
21 | ----------------------------
22 |
23 | .. automodule:: tiletanic.tileschemes
24 | :members:
25 | :undoc-members:
26 | :show-inheritance:
27 |
--------------------------------------------------------------------------------
/docs/user_guide.rst:
--------------------------------------------------------------------------------
1 | User Guide
2 | ==========
3 |
4 | Building Blocks
5 | ---------------
6 |
7 | Tiletanic has extremely simple structures (named tuples) for representing the building blocks that compose tiles. You don't have to use these, they just make your life a little easier. Some of the prebaked tiling schemes return these named tuples, but they can be treated as tuples just the same if that's the way you prefer to role.
8 |
9 | :class:`Tile `: Just a named tuple for storing (x, y, z) tile coordinates, which is of course the column, row, and zoom level of the tile. Note that the origin of the row and column coordinates is sometimes defined in the bottom left or top left of the grid.
10 |
11 | .. code-block:: pycon
12 |
13 | >>> from tiletanic import base
14 | >>> tile = base.Tile(1, 2, 3)
15 | >>> tile
16 | Tile(x=1, y=2, z=3)
17 | >>> tile.x
18 | 1
19 | >>> tile.y
20 | 2
21 | >>> tile.z
22 | 3
23 | >>> tile[0]
24 | 1
25 | >>> tile[1]
26 | 2
27 | >>> tile[2]
28 | 3
29 |
30 | :class:`Coords `: Geospatial coordinate pairs (x, y).
31 |
32 | .. code-block:: pycon
33 |
34 | >>> base.Coords(0., 1.)
35 | Coords(x=0.0, y=1.0)
36 |
37 | :class:`CoordsBbox `: Bounding box coordiantes (xmin, ymin, xmax, ymax)
38 |
39 | .. code-block:: pycon
40 |
41 | >>> base.CoordsBbox(0., 0., 1., 1.)
42 | CoordsBbox(xmin=0.0, ymin=0.0, xmax=1.0, ymax=1.0)
43 |
44 |
45 | Tiling Schemes
46 | --------------
47 |
48 | Tile schemes are how you convert back and forth from tile coordinates to geospatial coordinates or quadkeys and the like. They also let you easily traverse the tile structure. You can use one of the schemes that comes with Tiletanic (see :py:class:`here `) or build your own.
49 |
50 | If you build your own, you'll want to implement the public API of a tilescheme (see :py:mod:`here `) so that you can use the tile algorithms defined around this API.
51 |
52 | Here's a Web Mercator tile scheme:
53 |
54 | .. code-block:: pycon
55 |
56 | >>> from tiletanic import tileschemes
57 | >>> tiler = tileschemes.WebMercator()
58 |
59 | You can check the bounds for which it is defined:
60 |
61 | .. code-block:: pycon
62 |
63 | >>> tiler.bounds
64 | CoordsBbox(xmin=-20037508.342789244, ymin=-20037508.342789244, xmax=20037508.342789244, ymax=20037508.342789244)
65 |
66 | Get the XYZ tile coordinates of a geospatial coordinate at a given zoom level:
67 |
68 | .. code-block:: pycon
69 |
70 | >>> t = tiler.tile(14765187.879790928, -3029352.3049981054, 14)
71 | >>> t
72 | Tile(x=14228, y=9430, z=14)
73 |
74 |
75 | How about that tile's parent and children:
76 |
77 | .. code-block:: pycon
78 |
79 | >>> tiler.parent(t)
80 | Tile(x=7114, y=4715, z=13)
81 | >>> tiler.children(t)
82 | [Tile(x=28456, y=18860, z=15), Tile(x=28457, y=18860, z=15), Tile(x=28456, y=18861, z=15), Tile(x=28457, y=18861, z=15)]
83 |
84 | What are the upper left, bottom right, and bounding box geospatial coordinates of that tile?
85 |
86 | .. code-block:: pycon
87 |
88 | >>> tiler.ul(t)
89 | Coords(x=14763964.887338366, y=-3028129.3125455417)
90 | >>> tiler.br(t)
91 | Coords(x=14766410.87224349, y=-3030575.297450669)
92 | >>> tiler.bbox(t)
93 | CoordsBbox(xmin=14763964.887338366, ymin=-3030575.297450669, xmax=14766410.87224349, ymax=-3028129.3125455417)
94 |
95 | Conversion to and from quadkeys is also supported:
96 |
97 | .. code-block:: pycon
98 |
99 | >>> qk = tiler.quadkey(t)
100 | >>> qk
101 | '31031132030320'
102 | >>> tiler.quadkey_to_tile(qk)
103 | Tile(x=14228, y=9430, z=14)
104 |
105 | Tile Covering
106 | -------------
107 |
108 | Often times, one is given a geometry and would like to know what tiles at a given zoom level cover it. Luckily for you, Tiletanic provides just such functionality! Just define your tile scheme, get a `shapely`_ geometry representing the geometry you'd like covered, and call :py:func:`cover_geometry() `.
109 |
110 | Here's an example using the previous output tile Tile(x=14228, y=9430, z=14):
111 |
112 | .. code-block:: pycon
113 |
114 | >>> from tiletanic import tilecover
115 | >>> from shapely import geometry
116 | >>> [t for t in tilecover.cover_geometry(tiler, geometry.box(*tiler.bbox(t)), 14)]
117 | [Tile(x=14228, y=9430, z=14), Tile(x=14229, y=9430, z=14), Tile(x=14228, y=9431, z=14), Tile(x=14229, y=9431, z=14), Tile(x=14230, y=9430, z=14), Tile(x=14230, y=9431, z=14), Tile(x=14228, y=9432, z=14), Tile(x=14229, y=9432, z=14), Tile(x=14230, y=9432, z=14)]
118 |
119 | Note that 9 tiles are returned; this is expected as a tile has 8 neighbor tiles that touch it at a given level. If we try a corner tile at that same level, we get back four tiles as expected:
120 |
121 | .. code-block:: pycon
122 |
123 | >>> [t for t in tilecover.cover_geometry(tiler, geometry.box(*tiler.bbox(0,0,14)), 14)]
124 | [Tile(x=0, y=0, z=14), Tile(x=1, y=0, z=14), Tile(x=0, y=1, z=14), Tile(x=1, y=1, z=14)]
125 |
126 | :py:func:`cover_geometry() ` works with all the shapely geometry types (Points, Polygons, and LineStrings as well as their Multi versions).
127 |
128 | .. _shapely: https://github.com/Toblerity/Shapely
129 |
130 |
131 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | click
2 | geojson
3 | pytest>=5.0
4 | Shapely>=1.6
5 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from setuptools import setup, find_packages
3 |
4 | open_kwds = {}
5 | if sys.version_info > (3,):
6 | open_kwds['encoding'] = 'utf-8'
7 |
8 | with open('README.rst', **open_kwds) as f:
9 | readme = f.read()
10 |
11 | setup(name='tiletanic',
12 | version='1.1.0',
13 | description='Geospatial tiling utilities',
14 | long_description=readme,
15 | classifiers=[
16 | 'Programming Language :: Python :: 3.6',
17 | 'Programming Language :: Python :: 3.7',
18 | 'Programming Language :: Python :: 3.8',
19 | ],
20 | keywords='',
21 | author='Patrick Young',
22 | author_email='patrick.young@digitalglobe.com',
23 | url='https://github.com/digitalglobe/tiletanic',
24 | license='MIT',
25 | packages=find_packages(exclude=['tests', 'docs']),
26 | include_package_data=True,
27 | zip_safe=False,
28 | install_requires=['click',
29 | 'geojson',
30 | 'shapely>=1.6'],
31 | entry_points='''
32 | [console_scripts]
33 | tiletanic=tiletanic.cli:cli
34 | '''
35 | )
36 |
--------------------------------------------------------------------------------
/tests/test_basictilingbottomleft.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tiletanic.tileschemes import BasicTilingBottomLeft
4 |
5 | @pytest.fixture
6 | def tiler():
7 | return BasicTilingBottomLeft(0, 0, 1, 1)
8 |
9 |
10 | def test_init():
11 | """Constructor exceptions."""
12 | with pytest.raises(ValueError):
13 | BasicTilingBottomLeft(1, 0, 1, 1)
14 |
15 | with pytest.raises(ValueError):
16 | BasicTilingBottomLeft(0, 1, 1, 1)
17 |
18 | def test_bounds(tiler):
19 | """Geographic bounds."""
20 | assert tiler.bounds.xmin == 0.
21 | assert tiler.bounds.ymin == 0.
22 | assert tiler.bounds.xmax == 1.
23 | assert tiler.bounds.ymax == 1.
24 |
25 |
26 | def test_tile(tiler):
27 | """Tile generation from gespatial coordinates and zoom."""
28 | assert tiler.tile(0., 0., 0) == (0, 0, 0)
29 |
30 | assert tiler.tile(0.25, 0.25, 1) == (0, 0, 1)
31 | assert tiler.tile(0.75, 0.25, 1) == (1, 0, 1)
32 | assert tiler.tile(0.25, 0.75, 1) == (0, 1, 1)
33 | assert tiler.tile(0.75, 0.75, 1) == (1, 1, 1)
34 |
35 |
36 | def test_parent(tiler):
37 | """Parent of a tile."""
38 | assert tiler.parent(0, 0, 1) == (0, 0, 0)
39 | assert tiler.parent(1, 0, 1) == (0, 0, 0)
40 |
41 | assert tiler.parent(2, 3, 2) == (1, 1, 1)
42 | assert tiler.parent(3, 0, 2) == (1, 0, 1)
43 |
44 |
45 | def test_children(tiler):
46 | """Children of a tile."""
47 | assert set(tiler.children(0, 0, 0)) == {(0, 0, 1),
48 | (0, 1, 1),
49 | (1, 0, 1),
50 | (1, 1, 1)}
51 |
52 | def test_quadkey(tiler):
53 | """Quadkey generation from tile coordinates."""
54 | assert tiler.quadkey(0, 0, 1) == '0'
55 | assert tiler.quadkey(1, 0, 1) == '1'
56 |
57 | assert tiler.quadkey(0, 0, 2) == '00'
58 | assert tiler.quadkey(1, 0, 2) == '01'
59 | assert tiler.quadkey(0, 1, 2) == '02'
60 | assert tiler.quadkey(1, 1, 2) == '03'
61 | assert tiler.quadkey(2, 0, 2) == '10'
62 | assert tiler.quadkey(3, 0, 2) == '11'
63 | assert tiler.quadkey(2, 1, 2) == '12'
64 | assert tiler.quadkey(3, 1, 2) == '13'
65 |
66 | assert tiler.quadkey(20, 35, 9) == '000210122'
67 |
68 |
69 | def test_ul(tiler):
70 | """Upper left coordinates of input tile."""
71 | assert tiler.ul(0, 0, 0) == (0., 1.)
72 |
73 | assert tiler.ul(0, 0, 1) == (0., 0.5)
74 | assert tiler.ul(1, 0, 1) == (0.5, 0.5)
75 | assert tiler.ul(0, 1, 1) == (0., 1.)
76 | assert tiler.ul(1, 1, 1) == (0.5, 1.)
77 |
78 |
79 | def test_br(tiler):
80 | """Bottom right coordinates of input tile."""
81 | assert tiler.br(0, 0, 0) == (1., 0.)
82 |
83 | assert tiler.br(0, 0, 1) == (0.5, 0.)
84 | assert tiler.br(1, 0, 1) == (1., 0.)
85 | assert tiler.br(0, 1, 1) == (0.5, 0.5)
86 | assert tiler.br(1, 1, 1) == (1., 0.5)
87 |
88 |
89 | def test_bbox(tiler):
90 | """Bounding boxes of tiles."""
91 | assert tiler.bbox(0, 0, 0) == (0., 0., 1., 1.)
92 |
93 | assert tiler.bbox(0, 0, 1) == (0., 0., 0.5, 0.5)
94 | assert tiler.bbox(1, 0, 1) == (0.5, 0., 1., 0.5)
95 | assert tiler.bbox(0, 1, 1) == (0., 0.5, 0.5, 1.)
96 | assert tiler.bbox(1, 1, 1) == (0.5, 0.5, 1., 1.)
97 |
98 |
99 | def test_quadkey(tiler):
100 | """Tile to quadkey."""
101 | assert not tiler.quadkey(0, 0, 0)
102 |
103 | assert tiler.quadkey(0, 0, 1) == '0'
104 | assert tiler.quadkey(1, 0, 1) == '1'
105 | assert tiler.quadkey(0, 1, 1) == '2'
106 | assert tiler.quadkey(1, 1, 1) == '3'
107 |
108 |
109 | def test_quadkey_to_tile1(tiler):
110 | """Quadkey to tile exceptions."""
111 | with pytest.raises(ValueError):
112 | tiler.quadkey_to_tile('4')
113 |
114 |
115 | def test_quadkey_to_tile2(tiler):
116 | """Quadkey to tile."""
117 | assert tiler.quadkey_to_tile('0') == (0, 0, 1)
118 | assert tiler.quadkey_to_tile('130232101') == (405, 184, 9)
119 |
--------------------------------------------------------------------------------
/tests/test_basictilingtopleft.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tiletanic.tileschemes import BasicTilingTopLeft
4 |
5 | @pytest.fixture
6 | def tiler():
7 | return BasicTilingTopLeft(-20037508.342789244,
8 | -20037508.342789244,
9 | 20037508.342789244,
10 | 20037508.342789244)
11 |
12 |
13 | def test_init():
14 | """Constructor exceptions."""
15 | with pytest.raises(ValueError):
16 | BasicTilingTopLeft(1, 0, 1, 1)
17 |
18 | with pytest.raises(ValueError):
19 | BasicTilingTopLeft(0, 1, 1, 1)
20 |
21 |
22 | def test_bounds(tiler):
23 | """Web Mercator bounds."""
24 | assert tiler.bounds.xmin == -20037508.342789244
25 | assert tiler.bounds.xmax == 20037508.342789244
26 | assert tiler.bounds.ymin == -20037508.342789244
27 | assert tiler.bounds.ymax == 20037508.342789244
28 |
29 |
30 | def test_tile(tiler):
31 | """Tile generation from gespatial coordinates and zoom."""
32 | assert tiler.tile(0., 0., 0) == (0, 0, 0)
33 |
34 | assert tiler.tile((-20037508.342789244 + -10018754.171394622)/2.,
35 | (-20037508.342789244 + -10018754.171394622)/2.,
36 | 2) == (0, 3, 2)
37 |
38 | assert tiler.tile((-12523442.714243278 + -10018754.171394622)/2.,
39 | (5009377.085697312 + 7514065.628545966)/2.,
40 | 4) == (3, 5, 4)
41 |
42 | assert tiler.tile((14763964.887338366 + 14766410.87224349)/2.,
43 | (-3030575.297450669 + -3028129.3125455417)/2.,
44 | 14) == (14228, 9430, 14)
45 |
46 |
47 | def test_parent(tiler):
48 | """Parent of a tile."""
49 | assert tiler.parent(0, 0, 1) == (0, 0, 0)
50 | assert tiler.parent(1, 0, 1) == (0, 0, 0)
51 |
52 | assert tiler.parent(2, 3, 2) == (1, 1, 1)
53 | assert tiler.parent(3, 0, 2) == (1, 0, 1)
54 |
55 |
56 | def test_children1(tiler):
57 | """Children of a level 0 tile."""
58 | assert set(tiler.children(0, 0, 0)) == {(0, 0, 1),
59 | (1, 0, 1),
60 | (0, 1, 1),
61 | (1, 1, 1)}
62 |
63 |
64 | def test_children2(tiler):
65 | """Children of a level > 0 tile."""
66 | assert set(tiler.children(1, 1, 1)) == {(2, 2, 2),
67 | (2, 3, 2),
68 | (3, 2, 2),
69 | (3, 3, 2)}
70 |
71 |
72 | def test_quadkey(tiler):
73 | """Quadkey generation from tile coordinates."""
74 | assert tiler.quadkey(0, 0, 1) == '0'
75 | assert tiler.quadkey(1, 0, 1) == '1'
76 |
77 | assert tiler.quadkey(0, 0, 2) == '00'
78 | assert tiler.quadkey(1, 0, 2) == '01'
79 | assert tiler.quadkey(0, 1, 2) == '02'
80 | assert tiler.quadkey(1, 1, 2) == '03'
81 | assert tiler.quadkey(2, 0, 2) == '10'
82 | assert tiler.quadkey(3, 0, 2) == '11'
83 | assert tiler.quadkey(2, 1, 2) == '12'
84 | assert tiler.quadkey(3, 1, 2) == '13'
85 |
86 | assert tiler.quadkey(20, 35, 9) == '000210122'
87 |
88 |
89 | def test_ul(tiler):
90 | """Upper left coordinates of input tile."""
91 | assert tiler.ul(0, 0, 0) == (-20037508.342789244, 20037508.342789244)
92 |
93 | assert tiler.ul(0, 0, 1) == (-20037508.342789244, 20037508.342789244)
94 | assert tiler.ul(1, 1, 1) == (0.0, 0.0)
95 |
96 | assert tiler.ul(3, 6, 3) == (-5009377.085697312, -10018754.17139462)
97 | assert tiler.ul(4, 6, 3) == (0.0, -10018754.17139462)
98 | assert tiler.ul(118, 35, 7) == (16906647.66422843, 9079495.967826376)
99 | assert tiler.ul(84, 124, 8) == (-6887893.4928338025, 626172.1357121654)
100 |
101 |
102 | def test_br(tiler):
103 | """Bottom right coordinates of input tile."""
104 | assert tiler.br(0, 0, 0) == (20037508.342789244, -20037508.342789244)
105 |
106 | assert tiler.br(0, 0, 1) == (0.0, 0.0)
107 | assert tiler.br(1, 1, 1) == (20037508.342789244, -20037508.342789244)
108 |
109 | assert tiler.br(2, 5, 3) == (-5009377.085697312, -10018754.17139462)
110 | assert tiler.br(3, 5, 3) == (0.0, -10018754.17139462)
111 | assert tiler.br(117, 34, 7) == (16906647.66422843, 9079495.967826376)
112 | assert tiler.br(83, 123, 8) == (-6887893.4928338025, 626172.1357121654)
113 |
114 |
115 | def test_bbox(tiler):
116 | """Bounding boxes of tiles."""
117 | assert tiler.bbox(1, 5, 3) == (-15028131.257091932, -10018754.17139462, -10018754.171394622, -5009377.085697312)
118 | assert tiler.bbox(77, 93, 8) == (-7983694.730330089, 5322463.153553393, -7827151.696402049, 5479006.187481433)
119 | assert tiler.bbox(27685, 19041, 15) == (13821037.70641243, -3250713.9389119744, 13822260.698864993, -3249490.9464594126)
120 |
121 |
122 | def test_quadkey_to_tile1(tiler):
123 | """Quadkey to tile exceptions."""
124 | with pytest.raises(ValueError):
125 | tiler.quadkey_to_tile('4')
126 |
127 |
128 | def test_quadkey_to_tile2(tiler):
129 | """Quadkey to tile."""
130 | assert tiler.quadkey_to_tile('0') == (0, 0, 1)
131 | assert tiler.quadkey_to_tile('130232101') == (405, 184, 9)
132 |
--------------------------------------------------------------------------------
/tests/test_cli_main.py:
--------------------------------------------------------------------------------
1 | from click.testing import CliRunner
2 |
3 | from tiletanic import cli
4 |
5 | def test_tiletanic():
6 | """Basic call to root command"""
7 | runner = CliRunner()
8 | result = runner.invoke(cli.cli)
9 | assert result.exit_code == 0
10 |
11 | def test_version():
12 | runner = CliRunner()
13 | result = runner.invoke(cli.cli, ['--version'])
14 | assert result.exit_code == 0
15 |
--------------------------------------------------------------------------------
/tests/test_cli_tilecover.py:
--------------------------------------------------------------------------------
1 | from click.testing import CliRunner
2 |
3 | from tiletanic import cli
4 |
5 | def test_cover_geometry_dgtiling_level_9_feature_collection():
6 | # Wall South Dakota AOI from geojson.io:
7 | #http://bl.ocks.org/d/fbc0b6427b48274c1782
8 | #http://bl.ocks.org/anonymous/raw/fbc0b6427b48274c1782/map.geojson
9 | wall_south_dakota_aoi = '{"type":"FeatureCollection","features":[{"geometry":{"type":"Polygon","coordinates":[[[-101.953125,43.59375],[-101.953125,44.296875],[-102.65625,44.296875],[-102.65625,43.59375],[-101.953125,43.59375]]]},"type":"Feature","properties":{}}]}'
10 |
11 | runner = CliRunner()
12 | result = runner.invoke(cli.cover_geometry, ['-'], input=wall_south_dakota_aoi)
13 |
14 | assert result.exit_code == 0
15 | assert result.output == "021323330\n"
16 |
17 | def test_cover_geometry_dgtiling_level_9_feature():
18 | # Wall South Dakota AOI from geojson.io:
19 | #http://bl.ocks.org/d/fbc0b6427b48274c1782
20 | #http://bl.ocks.org/anonymous/raw/fbc0b6427b48274c1782/map.geojson
21 | wall_south_dakota_aoi = '{"geometry":{"coordinates":[[[ -101.953125,43.59375],[-101.953125,44.296875],[-102.65625,44.296875],[-102.65625,43.59375],[-101.953125,43.59375]]],"type":"Polygon"},"type":"Feature"}'
22 |
23 | runner = CliRunner()
24 | result = runner.invoke(cli.cover_geometry, ['-'], input=wall_south_dakota_aoi)
25 |
26 | assert result.exit_code == 0
27 | assert result.output == "021323330\n"
28 |
29 |
30 | def test_cover_geometry_dgtiling_level_9_adajcent_tiles():
31 | # Wall South Dakota AOI from geojson.io:
32 | #http://bl.ocks.org/d/fbc0b6427b48274c1782
33 | #http://bl.ocks.org/anonymous/raw/fbc0b6427b48274c1782/map.geojson
34 | wall_south_dakota_aoi = '{"geometry":{"coordinates":[[[ -101.953125,43.59375],[-101.953125,44.296875],[-102.65625,44.296875],[-102.65625,43.59375],[-101.953125,43.59375]]],"type":"Polygon"},"type":"Feature"}'
35 |
36 | runner = CliRunner()
37 | result = runner.invoke(cli.cover_geometry, ['--adjacent', '-'], input=wall_south_dakota_aoi)
38 |
39 | assert result.exit_code == 0
40 | assert result.output == "021323303\n021323312\n021323313\n021323321\n021323323\n021323330\n021323331\n021323332\n021323333\n"
41 |
42 |
--------------------------------------------------------------------------------
/tests/test_dgtiling.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tiletanic.tileschemes import DGTiling
4 |
5 | @pytest.fixture
6 | def tiler():
7 | return DGTiling()
8 |
9 |
10 | def test_bounds(tiler):
11 | """Geographic bounds."""
12 | assert tiler.bounds.xmin == -180.
13 | assert tiler.bounds.xmax == 180.
14 | assert tiler.bounds.ymin == -90.
15 | assert tiler.bounds.ymax == 90.
16 |
17 |
18 | def test_tile(tiler):
19 | """Tile generation from gespatial coordinates and zoom."""
20 | assert tiler.tile(0., 0., 0) == (0, 0, 0)
21 |
22 | assert tiler.tile(-90., 0., 1) == (0, 0, 1)
23 | assert tiler.tile(90., 0., 1) == (1, 0, 1)
24 |
25 | assert tiler.tile(-135., -45., 2) == (0, 0, 2)
26 | assert tiler.tile(-45., -45., 2) == (1, 0, 2)
27 | assert tiler.tile(45., -45., 2) == (2, 0, 2)
28 | assert tiler.tile(135., -45., 2) == (3, 0, 2)
29 | assert tiler.tile(-135., 45., 2) == (0, 1, 2)
30 | assert tiler.tile(-45., 45., 2) == (1, 1, 2)
31 | assert tiler.tile(45., 45., 2) == (2, 1, 2)
32 | assert tiler.tile(135., 45., 2) == (3, 1, 2)
33 |
34 | assert tiler.tile(105.1092, 40.1717, 12) == (3243, 1481, 12)
35 |
36 |
37 | def test_parent(tiler):
38 | """Parent of a tile."""
39 | assert tiler.parent(0, 0, 1) == (0, 0, 0)
40 | assert tiler.parent(1, 0, 1) == (0, 0, 0)
41 |
42 | assert tiler.parent(2, 3, 2) == (1, 1, 1)
43 | assert tiler.parent(3, 0, 2) == (1, 0, 1)
44 |
45 |
46 | def test_children1(tiler):
47 | """Children of a level 0 tile."""
48 | assert set(tiler.children(0, 0, 0)) == {(0, 0, 1),
49 | (1, 0, 1)}
50 |
51 |
52 | def test_children2(tiler):
53 | """Children of a level > 0 tile."""
54 | assert set(tiler.children(1, 1, 1)) == {(2, 2, 2),
55 | (2, 3, 2),
56 | (3, 2, 2),
57 | (3, 3, 2)}
58 |
59 |
60 | def test_quadkey(tiler):
61 | "Quadkey generation from tile coordinates."""
62 | assert tiler.quadkey(0, 0, 1) == '0'
63 | assert tiler.quadkey(1, 0, 1) == '1'
64 |
65 | assert tiler.quadkey(0, 0, 2) == '00'
66 | assert tiler.quadkey(1, 0, 2) == '01'
67 | assert tiler.quadkey(0, 1, 2) == '02'
68 | assert tiler.quadkey(1, 1, 2) == '03'
69 | assert tiler.quadkey(2, 0, 2) == '10'
70 | assert tiler.quadkey(3, 0, 2) == '11'
71 | assert tiler.quadkey(2, 1, 2) == '12'
72 | assert tiler.quadkey(3, 1, 2) == '13'
73 |
74 | assert not tiler.quadkey(0, 0, 0)
75 |
76 | assert tiler.quadkey(3, 1, 3) == '013'
77 | assert tiler.quadkey(4, 1, 3) == '102'
78 | assert tiler.quadkey(3, 2, 3) == '031'
79 | assert tiler.quadkey(4, 2, 3) == '120'
80 |
81 | assert tiler.quadkey(20, 35, 9) == '000210122'
82 |
83 |
84 | def test_ul(tiler):
85 | """Upper left coordinates of input tile."""
86 | assert tiler.ul(0, 0, 1) == (-180., 90.)
87 | assert tiler.ul(1, 0, 1) == (0., 90.)
88 |
89 | assert tiler.ul(3, 1, 3) == (-45., 0.)
90 | assert tiler.ul(4, 1, 3) == (0., 0.)
91 | assert tiler.ul(3, 2, 3) == (-45., 45.)
92 | assert tiler.ul(4, 2, 3) == (0., 45.)
93 |
94 |
95 | def test_br(tiler):
96 | """Bottom right coordinates of input tile."""
97 | assert tiler.br(0, 0, 1) == (0., -90.)
98 | assert tiler.br(1, 0, 1) == (180., -90.)
99 |
100 | assert tiler.br(3, 1, 3) == (0., -45.)
101 | assert tiler.br(4, 1, 3) == (45., -45.)
102 | assert tiler.br(3, 2, 3) == (0., 0.)
103 | assert tiler.br(4, 2, 3) == (45., 0.)
104 |
105 |
106 | def test_bbox(tiler):
107 | """Bounding boxes of tiles."""
108 | assert tiler.bbox(0, 0, 1) == (-180, -90., 0., 90.)
109 | assert tiler.bbox(1, 0, 1) == (0., -90., 180., 90.)
110 |
111 | assert tiler.bbox(3, 1, 3) == (-45., -45., 0., 0.)
112 | assert tiler.bbox(4, 1, 3) == (0., -45., 45., 0.)
113 | assert tiler.bbox(3, 2, 3) == (-45., 0., 0., 45.)
114 | assert tiler.bbox(4, 2, 3) == (0., 0., 45., 45.)
115 |
116 |
117 | def test_quadkey_to_tile1(tiler):
118 | """Quadkey to tile exceptions."""
119 | with pytest.raises(ValueError):
120 | tiler.quadkey_to_tile('4')
121 |
122 |
123 | def test_quadkey_to_tile2(tiler):
124 | """Quadkey to tile."""
125 | assert tiler.quadkey_to_tile('0') == (0, 0, 1)
126 | assert tiler.quadkey_to_tile('130232101') == (405, 184, 9)
127 |
--------------------------------------------------------------------------------
/tests/test_tilecover.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from shapely import geometry
3 |
4 | from tiletanic.tilecover import cover_geometry
5 | from tiletanic.tileschemes import DGTiling, WebMercator
6 |
7 |
8 | @pytest.fixture
9 | def tiler():
10 | return DGTiling()
11 |
12 |
13 | @pytest.fixture
14 | def wmtiler():
15 | return WebMercator()
16 |
17 |
18 | @pytest.fixture
19 | def pt():
20 | return geometry.Point(-94.39453125, 15.908203125)
21 |
22 |
23 | @pytest.fixture
24 | def mpt():
25 | return geometry.MultiPoint([(-94.39453125, 15.908203125),
26 | (-94.306640625, 15.908203125),
27 | (-94.306640625, 15.8203125),
28 | (-94.39453125, 15.8203125)])
29 |
30 |
31 | @pytest.fixture
32 | def ls():
33 | return geometry.shape({"coordinates": [[-123.12515258789061, 45.70809729528788], [-122.4755859375, 45.615958580368364], [-123.32977294921874, 45.44664375276733], [-122.25173950195311, 45.334771196762766], [-123.3819580078125, 45.11133093583217], [-122.23388671874999, 45.04829981381567], [-122.57995605468749, 45.74740199642105], [-122.6348876953125, 44.961882876810925], [-122.80654907226562, 45.75315158411652], [-122.98645019531249, 44.998795943614084], [-123.05648803710938, 45.744526980468436], [-123.33938598632812, 45.031803280058554]], "type": "LineString"})
34 |
35 |
36 | @pytest.fixture
37 | def mls():
38 | return geometry.shape({'type': 'MultiLineString', 'coordinates': (((-0.9709167480468749, 35.18840002173177), (-0.9832763671875, 34.83296838321102), (-0.954437255859375, 35.02774729487063), (-0.850067138671875, 35.02662273458687), (-0.833587646484375, 34.838604318635014), (-0.8074951171874999, 35.185032937998294)), ((-0.0823974609375, 35.191766965947394), (-0.111236572265625, 34.83071390101431), (0.048065185546875, 34.829586636768205), (0.067291259765625, 35.192889249680945), (-0.06591796875, 35.191766965947394)), ((-0.28564453125, 35.19064466671118), (-0.34606933593749994, 34.84085858477277), (-0.18402099609375, 34.831841149828676)), ((-0.48614501953124994, 35.183910545750834), (-0.517730712890625, 34.854382885097905), (-0.391387939453125, 34.84874803007872)), ((-0.553436279296875, 35.183910545750834), (-0.7347106933593749, 35.185032937998294), (-0.751190185546875, 35.05922870088872), (-0.591888427734375, 35.0502352513963), (-0.758056640625, 35.03336986422378), (-0.767669677734375, 34.86227103378598), (-0.597381591796875, 34.85550980979319)))})
39 |
40 |
41 | @pytest.fixture
42 | def poly():
43 | return geometry.shape({'type': 'Polygon', 'coordinates': (((74.5751953125, 46.86019101567027), (73.916015625, 46.45299704748289), (73.564453125, 46.21785176740299), (73.32275390625, 45.920587344733654), (73.289794921875, 45.56021795715051), (73.465576171875, 45.27488643704894), (74.036865234375, 44.96479793033104), (74.256591796875, 45.07352060670971), (74.322509765625, 45.48324350868221), (74.46533203125, 45.91294412737392), (74.849853515625, 46.057985244793024), (74.99267578125, 46.354510837365254), (75.465087890625, 46.40756396630067), (76.102294921875, 46.42271253466719), (76.904296875, 46.34692761055676), (77.376708984375, 46.27863122156088), (78.2666015625, 46.195042108660154), (78.94775390625, 46.31658418182218), (79.29931640625, 46.5286346952717), (79.398193359375, 46.77749276376827), (79.12353515625, 47.017716353979225), (78.42041015625, 46.912750956378915), (77.574462890625, 46.76244305208004), (76.46484375, 46.81509864599243), (76.036376953125, 46.98025235521883), (75.399169921875, 46.830133640447386), (74.893798828125, 46.93526088057719), (74.5751953125, 46.86019101567027)),)})
44 |
45 |
46 | @pytest.fixture
47 | def poly_w_hole():
48 | return geometry.shape({'type': 'Polygon', 'coordinates': (((31.794433593749996, -28.979312036722447), (31.289062500000004, -29.401319510041485), (31.036376953125, -29.897805610155864), (30.377197265625, -30.845647420182598), (29.278564453125, -31.886886525780806), (26.74072265625, -31.94283997285307), (23.9501953125, -31.184609135743237), (23.917236328125, -28.98892237190413), (25.5322265625, -27.615406013399603), (27.894287109374996, -27.019984007982554), (30.377197265625, -27.32297494724568), (31.794433593749996, -28.979312036722447)), ((28.652343749999996, -28.584521719370393), (28.399658203125, -28.632746799225856), (28.3282470703125, -28.724313406473463), (28.1634521484375, -28.729130483430154), (28.015136718749996, -28.8831596093235), (27.745971679687496, -28.92163128242129), (27.531738281249996, -29.200123477644983), (27.410888671874996, -29.382175075145277), (27.257080078125, -29.54956657394792), (26.987915039062496, -29.640320395351402), (27.1966552734375, -30.002516938570686), (27.31201171875, -30.140376821599734), (27.3834228515625, -30.14987731644208), (27.366943359375, -30.249577240467637), (27.39990234375, -30.334953881988564), (27.454833984375, -30.33021268543272), (27.745971679687496, -30.60954979719083), (27.8997802734375, -30.619004797647793), (28.108520507812496, -30.680439786468128), (28.1744384765625, -30.50075098029068), (28.14697265625, -30.462879341709876), (28.229370117187496, -30.410781790845878), (28.2513427734375, -30.29701788337205), (28.3612060546875, -30.202113679097216), (28.71826171875, -30.135626231134587), (28.9544677734375, -30.026299582223675), (29.179687499999996, -29.912090918781477), (29.1192626953125, -29.831113764737136), (29.141235351562504, -29.67850809103362), (29.256591796874996, -29.640320395351402), (29.300537109374996, -29.492206334848714), (29.410400390625, -29.40610505570927), (29.443359375, -29.31993078977759), (29.393920898437496, -29.195328267099118), (29.2950439453125, -29.08977693862319), (29.091796875, -28.936054482136658), (28.976440429687496, -28.90239722855847), (28.916015625, -28.762843805266016), (28.800659179687496, -28.772474183943018), (28.789672851562496, -28.6905876542507), (28.7017822265625, -28.656851034203406), (28.652343749999996, -28.584521719370393)))})
49 |
50 |
51 | @pytest.fixture
52 | def mpoly():
53 | return geometry.shape({'type': 'MultiPolygon', 'coordinates': [(((-155.60651896977458, 20.13795556629634), (-155.16804273408488, 19.946827215003964), (-154.81406356568098, 19.50657921146143), (-155.67475562357313, 18.906117143691233), (-155.9039060717078, 19.069459067981597), (-156.0601639283226, 19.73122055328588), (-155.83545761767488, 19.975703442260055), (-155.88048255092895, 20.252020575289293), (-155.60651896977458, 20.13795556629634)),), (((-156.91373752666547, 20.73472409144364), (-156.96228010324413, 20.73238773010962), (-157.05521360182624, 20.911371030348732), (-156.81633843277243, 20.841704170311175), (-156.91373752666547, 20.73472409144364)),), (((-156.53412841109724, 20.531787476211036), (-156.6792613032665, 20.504712931856318), (-156.7005618241688, 20.525426565083876), (-156.5826214116488, 20.59995670154052), (-156.53412841109724, 20.531787476211036)),), (((-156.5895997046626, 21.027738813695294), (-156.24290930851774, 20.941555080502553), (-155.98469586911904, 20.717591061278426), (-156.40765319493894, 20.587206654063834), (-156.68490668409615, 20.881887778698), (-156.5895997046626, 21.027738813695294)),), (((-157.24884316468027, 21.22171060725873), (-156.96576900873274, 21.213324286186833), (-156.70572493545737, 21.15654296368683), (-157.2730606762092, 21.087347723418702), (-157.24884316468027, 21.22171060725873)),), (((-157.89376363688228, 21.598381702453594), (-157.72570752668096, 21.45898509818369), (-157.69321165049723, 21.262742044309903), (-157.9609268868663, 21.388820705311275), (-158.0988663403093, 21.29539622635474), (-158.27893301424717, 21.575264710198212), (-157.98264568694884, 21.7098504289541), (-157.89376363688228, 21.598381702453594)),), (((-161.9451798172947, 23.03986237235057), (-161.94477291633697, 23.04621002837007), (-161.94041907528006, 23.045599677383166), (-161.94090735588972, 23.041978257690403), (-161.9451798172947, 23.03986237235057)),), (((-164.7043350900923, 23.5793317732427), (-164.69908606656867, 23.570542710577627), (-164.70848548112002, 23.574530341402124), (-164.7043350900923, 23.5793317732427)),), (((-167.99616451697884, 25.004339911058025), (-168.00279700357882, 25.014797268279267), (-167.99986731722288, 25.01642487211012), (-167.99555416599193, 25.011664130095483), (-167.99616451697884, 25.004339911058025)),), (((-171.72280839828892, 25.773016669183903), (-171.72935950433762, 25.7519391954375), (-171.7390844392954, 25.756293036494412), (-171.74067135330029, 25.769029039258726), (-171.72280839828892, 25.773016669183903)),), (((-173.9591772130579, 26.059637762073066), (-173.96564693855532, 26.05857982030244), (-173.96499589774245, 26.07135651199343), (-173.96051998630827, 26.063137111388585), (-173.9591772130579, 26.059637762073066)),), (((-178.2983970579778, 28.387372162295435), (-178.3043661632678, 28.387912197090202), (-178.29038113887842, 28.401678575605786), (-178.28790478098082, 28.39298946089491), (-178.2983970579778, 28.387372162295435)),), (((-160.198240306345, 21.783883779013195), (-160.23442352432616, 21.874455639016503), (-160.05996362062456, 22.000562970404758), (-160.07221432223315, 21.910834052149767), (-160.198240306345, 21.783883779013195)),), (((-159.38818451870054, 22.228789014401457), (-159.29470434293106, 22.107627229413083), (-159.43731532294376, 21.868769246403588), (-159.78721108632254, 22.019270121718705), (-159.38818451870054, 22.228789014401457)),)]})
54 |
55 |
56 | @pytest.fixture
57 | def donut():
58 | return geometry.shape({'type': 'Polygon', 'coordinates': (((-8478680.853644049, 5697329.705967457), (-8475876.604351476, 5693825.364227657), (-8478353.796980098, 5691831.500531353), (-8479056.000328023, 5691230.9688630225), (-8482763.718607875, 5687921.707011377), (-8484066.71324761, 5689357.283425693), (-8484132.72570565, 5689458.9609730365), (-8485603.924095975, 5691119.276002927), (-8489564.56025891, 5695499.188019603), (-8487563.926370371, 5695598.554369273), (-8487020.909894282, 5696010.158685913), (-8486833.002593823, 5696653.397796723), (-8486272.50895768, 5701826.232722361), (-8485641.661403354, 5702462.741808833), (-8483894.05671739, 5703160.777664438), (-8481629.706955165, 5700827.320925154), (-8479652.672798675, 5698504.158803017), (-8478680.853644049, 5697329.705967457)), ((-8485619.620144177, 5698875.087400831), (-8486120.001255292, 5698515.59130402), (-8486124.231395943, 5698419.68577629), (-8486086.494088564, 5698217.5571292695), (-8485962.48417582, 5697920.644304513), (-8485402.101859165, 5696988.529093507), (-8485168.776206464, 5696640.697662884), (-8482860.1212869, 5694044.057728255), (-8482570.690610839, 5694045.16866679), (-8482441.560001519, 5694099.446117623), (-8481249.550894104, 5695150.144684806), (-8483046.692753471, 5697250.959475276), (-8484276.773126736, 5698761.869977695), (-8484799.30681652, 5699340.04017765), (-8484900.050955689, 5699521.073411321), (-8485474.014250219, 5699024.669980715), (-8485499.951691575, 5698955.594911603), (-8485619.620144177, 5698875.087400831)))})
59 |
60 |
61 | def test_cover_geometry_empty_geoms(tiler):
62 | """Empty geometries should return empty iterators."""
63 | assert not cover_geometry(tiler, geometry.Point(), 0) == True
64 | assert not cover_geometry(tiler, geometry.Point(), [0, 1]) == True
65 | assert not cover_geometry(tiler, geometry.MultiPoint(), 0) == True
66 | assert not cover_geometry(tiler, geometry.MultiPoint(), [0, 1]) == True
67 | assert not cover_geometry(tiler, geometry.LineString(), 0) == True
68 | assert not cover_geometry(tiler, geometry.LineString(), [0, 1]) == True
69 | assert not cover_geometry(tiler, geometry.MultiLineString(), 0) == True
70 | assert not cover_geometry(tiler, geometry.MultiLineString(), [0, 1]) == True
71 | assert not cover_geometry(tiler, geometry.Polygon(), 0) == True
72 | assert not cover_geometry(tiler, geometry.Polygon(), [0, 1]) == True
73 | assert not cover_geometry(tiler, geometry.MultiPolygon(), 0) == True
74 | assert not cover_geometry(tiler, geometry.MultiPolygon(), [0, 1]) == True
75 | assert not cover_geometry(tiler, geometry.GeometryCollection(), 0) == True
76 | assert not cover_geometry(tiler, geometry.GeometryCollection(), [0, 1]) == True
77 |
78 |
79 | def test_cover_geometry_nonshapely_geom(tiler):
80 | """Only accept shapely geometries."""
81 | with pytest.raises(ValueError):
82 | for _ in cover_geometry(tiler, None, 0):
83 | pass
84 |
85 | with pytest.raises(ValueError):
86 | for _ in cover_geometry(tiler, None, [0, 1]):
87 | pass
88 |
89 |
90 | def test_cover_geometry_point1(tiler, pt):
91 | """A Point geometry."""
92 | tiles = [tile for tile in cover_geometry(tiler, pt, 4)]
93 | assert len(tiles) == 1
94 | assert set(tiles) == {(3, 4, 4)}
95 |
96 |
97 | def test_cover_geometry_point2(tiler, pt):
98 | """A Point geometry."""
99 | tiles = [tile for tile in cover_geometry(tiler, pt, 12)]
100 | assert len(tiles) == 4
101 | assert set(tiles) == {(973, 1204, 12), (973, 1205, 12), (974, 1204, 12), (974, 1205, 12)}
102 |
103 |
104 | def test_cover_geometry_point3(tiler, pt):
105 | """A Point geometry."""
106 | tiles = [tile for tile in cover_geometry(tiler, pt, [3, 4])]
107 | assert len(tiles) == 1
108 | assert set(tiles) == {(1, 2, 3)}
109 |
110 |
111 | def test_cover_geometry_point4(tiler, pt):
112 | """A Point geometry."""
113 | tiles = [tile for tile in cover_geometry(tiler, pt, [11, 12])]
114 | assert len(tiles) == 2
115 | assert set(tiles) == {(487, 602, 11), (486, 602, 11)}
116 |
117 |
118 | def test_cover_geometry_multipoint1(tiler, mpt):
119 | """A MultiPoint geometry."""
120 | tiles = [tile for tile in cover_geometry(tiler, mpt, 4)]
121 | assert len(tiles) == 1
122 |
123 |
124 | def test_cover_geometry_multipoint2(tiler, mpt):
125 | """A MultiPoint geometry."""
126 | tiles = [tile for tile in cover_geometry(tiler, mpt, 12)]
127 | assert len(tiles) == 9
128 | assert set(tiles) == {(973, 1203, 12), (974, 1203, 12), (975, 1203, 12),
129 | (973, 1204, 12), (973, 1205, 12), (974, 1204, 12),
130 | (975, 1204, 12), (974, 1205, 12), (975, 1205, 12)}
131 |
132 |
133 | def test_cover_geometry_multipoint3(tiler, mpt):
134 | """A MultiPoint geometry."""
135 | tiles = [tile for tile in cover_geometry(tiler, mpt, [3, 4])]
136 | assert len(tiles) == 1
137 |
138 |
139 | def test_cover_geometry_multipoint4(tiler, mpt):
140 | """A MultiPoint geometry."""
141 | tiles = [tile for tile in cover_geometry(tiler, mpt, [11, 12])]
142 | assert len(tiles) == 4
143 | assert set(tiles) == {(486, 601, 11), (487, 601, 11),
144 | (487, 602, 11), (486, 602, 11)}
145 |
146 |
147 | def test_cover_geometry_linestring1(tiler, ls):
148 | """A LineString geometry."""
149 | tiles = [tile for tile in cover_geometry(tiler, ls, 4)]
150 | assert len(tiles) == 2
151 | assert set(tiles) == {(2, 5, 4), (2, 6, 4)}
152 |
153 |
154 | def test_cover_geometry_linestring2(tiler, ls):
155 | """A LineString geometry."""
156 | tiles = [tile for tile in cover_geometry(tiler, ls, 11)]
157 | assert len(tiles) == 30
158 | assert set(tiles) == {(322, 768, 11), (322, 769, 11), (322, 770, 11), (323, 768, 11),
159 | (323, 769, 11), (323, 770, 11), (323, 771, 11), (323, 772, 11),
160 | (324, 767, 11), (324, 768, 11), (324, 769, 11), (324, 770, 11),
161 | (324, 771, 11), (325, 768, 11), (325, 769, 11), (325, 770, 11),
162 | (325, 771, 11), (325, 772, 11), (326, 767, 11), (326, 768, 11),
163 | (326, 769, 11), (326, 770, 11), (326, 771, 11), (326, 772, 11),
164 | (327, 768, 11), (327, 769, 11), (327, 770, 11), (327, 771, 11),
165 | (328, 768, 11), (328, 769, 11)}
166 |
167 |
168 | def test_cover_geometry_linestring3(tiler, ls):
169 | """A LineString geometry."""
170 | tiles = [tile for tile in cover_geometry(tiler, ls, [3, 4])]
171 | assert len(tiles) == 2
172 | assert set(tiles) == {(1, 3, 3), (1, 2, 3)}
173 |
174 |
175 | def test_cover_geometry_linestring4(tiler, ls):
176 | """A LineString geometry."""
177 | tiles = [tile for tile in cover_geometry(tiler, ls, [10, 11])]
178 | assert len(tiles) == 12
179 | assert set(tiles) == {(161, 384, 10), (163, 385, 10), (162, 386, 10), (163, 386, 10),
180 | (161, 386, 10), (164, 384, 10), (162, 383, 10), (163, 383, 10),
181 | (163, 384, 10), (162, 384, 10), (162, 385, 10), (161, 385, 10)}
182 |
183 |
184 | def test_cover_geometry_multilinestring1(tiler, mls):
185 | """A MultiLineString geometry."""
186 | tiles = [tile for tile in cover_geometry(tiler, mls, 8)]
187 | assert len(tiles) == 4
188 | assert set(tiles) == {(127, 88, 8), (127, 89, 8), (128, 88, 8), (128, 89, 8)}
189 |
190 |
191 | def test_cover_geometry_multilinestring2(tiler, mls):
192 | """A MultiLineString geometry."""
193 | tiles = [tile for tile in cover_geometry(tiler, mls, 12)]
194 | assert len(tiles) == 47
195 | assert set(tiles) == {(2036, 1420, 12), (2036, 1421, 12), (2036, 1422, 12), (2036, 1423, 12),
196 | (2036, 1424, 12), (2037, 1421, 12), (2037, 1422, 12), (2038, 1420, 12),
197 | (2038, 1421, 12), (2038, 1422, 12), (2038, 1423, 12), (2038, 1424, 12),
198 | (2039, 1420, 12), (2039, 1421, 12), (2039, 1422, 12), (2039, 1423, 12),
199 | (2039, 1424, 12), (2040, 1420, 12), (2040, 1422, 12), (2040, 1424, 12),
200 | (2041, 1420, 12), (2041, 1422, 12), (2041, 1424, 12), (2042, 1420, 12),
201 | (2042, 1421, 12), (2042, 1422, 12), (2042, 1423, 12), (2042, 1424, 12),
202 | (2043, 1420, 12), (2044, 1420, 12), (2044, 1421, 12), (2044, 1422, 12),
203 | (2044, 1423, 12), (2044, 1424, 12), (2045, 1420, 12), (2046, 1420, 12),
204 | (2046, 1421, 12), (2046, 1422, 12), (2046, 1423, 12), (2047, 1420, 12),
205 | (2047, 1423, 12), (2047, 1424, 12), (2048, 1420, 12), (2048, 1421, 12),
206 | (2048, 1422, 12), (2048, 1423, 12), (2048, 1424, 12)}
207 |
208 |
209 | def test_cover_geometry_multilinestring3(tiler, mls):
210 | """A MultiLineString geometry."""
211 | tiles = [tile for tile in cover_geometry(tiler, mls, [7, 8])]
212 | assert len(tiles) == 2
213 | assert set(tiles) == {(63, 44, 7), (64, 44, 7)}
214 |
215 |
216 | def test_cover_geometry_multilinestring4(tiler, mls):
217 | """A MultiLineString geometry."""
218 | tiles = [tile for tile in cover_geometry(tiler, mls, [11, 12])]
219 | assert len(tiles) == 21
220 | assert set(tiles) == {(1022, 712, 11), (1019, 710, 11), (1024, 712, 11), (1020, 710, 11),
221 | (1023, 710, 11), (1020, 711, 11), (1018, 712, 11), (1018, 711, 11),
222 | (1023, 712, 11), (1024, 711, 11), (1022, 710, 11), (1024, 710, 11),
223 | (1019, 711, 11), (1018, 710, 11), (1019, 712, 11), (1021, 711, 11),
224 | (1021, 710, 11), (1023, 711, 11), (1020, 712, 11), (1022, 711, 11),
225 | (1021, 712, 11)}
226 |
227 |
228 | def test_cover_geometry_poly1(tiler, poly):
229 | """A Polygon geometry."""
230 | tiles = [tile for tile in cover_geometry(tiler, poly, 7)]
231 | assert len(tiles) == 4
232 | assert set(tiles) == {(90, 47, 7), (90, 48, 7), (91, 48, 7), (92, 48, 7)}
233 |
234 |
235 | def test_cover_geometry_poly2(tiler, poly):
236 | """A Polygon geometry."""
237 | tiles = [tile for tile in cover_geometry(tiler, poly, [7, 8])]
238 | assert len(tiles) == 5
239 | assert set(tiles) == {(90, 48, 7), (91, 48, 7),
240 | (180, 95, 8), (184, 97, 8), (184, 96, 8)}
241 |
242 |
243 | def test_cover_geometry_poly3(tiler, poly):
244 | """A Polygon geometry."""
245 | tiles = [tile for tile in cover_geometry(tiler, poly, [8, 9, 10])]
246 | assert len(tiles) == 25
247 | assert set(tiles) == {(180, 96, 8),
248 | (362, 194, 9), (363, 194, 9), (364, 194, 9), (365, 194, 9), (366, 194, 9), (367, 194, 9), (368, 194, 9), (722, 383, 10),
249 | (722, 388, 10), (723, 388, 10), (723, 389, 10), (724, 386, 10), (724, 387, 10), (725, 387, 10), (726, 387, 10), (728, 387, 10), (729, 387, 10), (730, 387, 10), (731, 387, 10), (732, 387, 10), (733, 387, 10), (734, 387, 10), (735, 387, 10), (736, 387, 10), }
250 |
251 |
252 | def test_cover_geometry_poly_w_hole1(tiler, poly_w_hole):
253 | """A Polygon geometry with a hole in it."""
254 | tiles = [tile for tile in cover_geometry(tiler, poly_w_hole, 7)]
255 | assert len(tiles) == 11
256 | assert set(tiles) == set([(72, 22, 7), (74, 21, 7), (75, 22, 7), (73, 20, 7), (74, 22, 7), (73, 22, 7), (74, 20, 7), (73, 21, 7), (75, 21, 7), (72, 21, 7), (72, 20, 7)])
257 |
258 |
259 | def test_cover_geometry_poly_w_hole2(tiler, poly_w_hole):
260 | """A Polygon geometry with a hole in it."""
261 | tiles = [tile for tile in cover_geometry(tiler, poly_w_hole, 9)]
262 | assert len(tiles) == 77
263 | assert set(tiles) == set([(297, 82, 9), (301, 87, 9), (294, 87, 9), (299, 88, 9), (300, 85, 9), (292, 83, 9), (296, 83, 9), (298, 89, 9), (295, 82, 9), (290, 86, 9), (291, 87, 9), (297, 88, 9), (292, 87, 9), (298, 86, 9), (298, 84, 9), (294, 84, 9), (294, 88, 9), (299, 89, 9), (292, 85, 9), (300, 86, 9), (294, 82, 9), (290, 85, 9), (298, 82, 9), (295, 84, 9), (296, 87, 9), (293, 84, 9), (299, 85, 9), (291, 85, 9), (299, 86, 9), (296, 85, 9), (297, 85, 9), (296, 89, 9), (293, 89, 9), (292, 86, 9), (293, 87, 9), (291, 88, 9), (298, 88, 9), (298, 87, 9), (295, 87, 9), (296, 88, 9), (293, 83, 9), (301, 86, 9), (291, 86, 9), (297, 86, 9), (297, 89, 9), (292, 88, 9), (294, 86, 9), (294, 85, 9), (292, 82, 9), (300, 87, 9), (295, 89, 9), (290, 87, 9), (296, 82, 9), (298, 85, 9), (297, 83, 9), (291, 83, 9), (295, 83, 9), (300, 88, 9), (293, 86, 9), (299, 83, 9), (299, 84, 9), (297, 87, 9), (294, 83, 9), (297, 84, 9), (298, 83, 9), (293, 82, 9), (294, 89, 9), (296, 84, 9), (290, 84, 9), (293, 88, 9), (290, 83, 9), (295, 86, 9), (293, 85, 9), (295, 88, 9), (292, 84, 9), (291, 84, 9), (299, 87, 9)])
264 |
265 |
266 | def test_cover_geometry_poly_w_hole3(tiler, poly_w_hole):
267 | """A Polygon geometry with a hole in it."""
268 | tiles = [tile for tile in cover_geometry(tiler, poly_w_hole, [6, 7])]
269 | assert len(tiles) == 8
270 | assert set(tiles) == set([(36, 10, 6),
271 | (72, 22, 7), (74, 21, 7), (75, 22, 7), (74, 22, 7), (73, 22, 7), (74, 20, 7), (75, 21, 7)])
272 |
273 |
274 | def test_cover_geometry_poly_w_hole4(tiler, poly_w_hole):
275 | """A Polygon geometry with a hole in it."""
276 | tiles = [tile for tile in cover_geometry(tiler, poly_w_hole, [8, 9])]
277 | assert len(tiles) == 32
278 | assert set(tiles) == set([(148, 42, 8), (146, 43, 8), (147, 43, 8), (145, 42, 8), (148, 44, 8), (147, 41, 8), (149, 43, 8), (149, 44, 8), (145, 43, 8), (146, 41, 8), (149, 42, 8), (150, 43, 8), (147, 44, 8), (148, 41, 8), (146, 42, 8),
279 | (299, 83, 9), (293, 89, 9), (298, 82, 9), (300, 85, 9), (295, 84, 9), (296, 87, 9), (291, 88, 9), (300, 88, 9), (297, 87, 9), (292, 88, 9), (293, 88, 9), (294, 85, 9), (298, 83, 9), (290, 83, 9), (291, 83, 9), (297, 86, 9), (294, 84, 9)])
280 |
281 |
282 | def test_cover_geometry_poly_w_hole5(tiler, poly_w_hole):
283 | """A Polygon geometry with a hole in it."""
284 | tiles = [tile for tile in cover_geometry(tiler, poly_w_hole, [8, 9, 11])]
285 | assert len(tiles) == 318
286 | assert set(tiles) == set([(145, 42, 8), (146, 42, 8), (146, 43, 8), (149, 43, 8),
287 | (290, 86, 9), (291, 86, 9), (291, 87, 9), (292, 83, 9), (293, 83, 9), (293, 88, 9), (294, 83, 9), (294, 84, 9), (294, 86, 9), (294, 87, 9), (294, 88, 9), (295, 83, 9), (295, 87, 9), (295, 88, 9), (296, 83, 9), (296, 84, 9), (296, 88, 9), (297, 83, 9), (297, 84, 9), (297, 87, 9), (297, 88, 9), (298, 84, 9), (298, 85, 9), (298, 88, 9), (299, 85, 9),
288 | (1160, 334, 11), (1160, 335, 11), (1161, 334, 11), (1161, 335, 11), (1161, 348, 11), (1162, 333, 11), (1162, 334, 11), (1162, 335, 11), (1162, 348, 11), (1162, 349, 11), (1163, 333, 11), (1163, 334, 11), (1163, 335, 11), (1163, 348, 11), (1163, 349, 11), (1163, 350, 11), (1164, 333, 11), (1164, 334, 11), (1164, 335, 11), (1165, 333, 11), (1165, 334, 11), (1165, 335, 11), (1165, 352, 11), (1166, 332, 11), (1166, 333, 11), (1166, 334, 11), (1166, 335, 11), (1166, 352, 11), (1167, 332, 11), (1167, 333, 11), (1167, 334, 11), (1167, 335, 11), (1167, 352, 11), (1167, 353, 11), (1168, 352, 11), (1168, 353, 11), (1168, 354, 11), (1169, 331, 11), (1169, 352, 11), (1169, 353, 11), (1169, 354, 11), (1169, 355, 11), (1170, 331, 11), (1170, 352, 11), (1170, 353, 11), (1170, 354, 11), (1170, 355, 11), (1171, 331, 11), (1171, 352, 11), (1171, 353, 11), (1171, 354, 11), (1171, 355, 11), (1172, 331, 11), (1173, 330, 11), (1173, 331, 11), (1173, 356, 11), (1174, 330, 11), (1174, 331, 11), (1174, 356, 11), (1175, 330, 11), (1175, 331, 11), (1175, 356, 11), (1176, 330, 11), (1176, 331, 11), (1176, 340, 11), (1176, 341, 11), (1176, 342, 11), (1176, 343, 11), (1176, 356, 11), (1177, 330, 11), (1177, 331, 11), (1177, 340, 11), (1177, 341, 11), (1177, 342, 11), (1177, 343, 11), (1177, 356, 11), (1177, 357, 11), (1178, 330, 11), (1178, 331, 11), (1178, 340, 11), (1178, 341, 11), (1178, 342, 11), (1178, 343, 11), (1178, 356, 11), (1178, 357, 11), (1179, 330, 11), (1179, 331, 11), (1179, 340, 11), (1179, 343, 11), (1179, 356, 11), (1179, 357, 11), (1180, 330, 11), (1180, 331, 11), (1180, 336, 11), (1180, 337, 11), (1180, 338, 11), (1180, 339, 11), (1180, 344, 11), (1180, 345, 11), (1180, 346, 11), (1180, 347, 11), (1180, 356, 11), (1180, 357, 11), (1181, 330, 11), (1181, 331, 11), (1181, 336, 11), (1181, 337, 11), (1181, 338, 11), (1181, 346, 11), (1181, 347, 11), (1181, 356, 11), (1181, 357, 11), (1181, 358, 11), (1182, 330, 11), (1182, 331, 11), (1182, 336, 11), (1182, 337, 11), (1182, 347, 11), (1182, 356, 11), (1182, 357, 11), (1182, 358, 11), (1183, 330, 11), (1183, 331, 11), (1183, 336, 11), (1183, 337, 11), (1183, 347, 11), (1183, 356, 11), (1183, 357, 11), (1183, 358, 11), (1184, 330, 11), (1184, 331, 11), (1184, 348, 11), (1184, 349, 11), (1184, 350, 11), (1184, 351, 11), (1184, 356, 11), (1184, 357, 11), (1184, 358, 11), (1185, 330, 11), (1185, 331, 11), (1185, 340, 11), (1185, 348, 11), (1185, 349, 11), (1185, 350, 11), (1185, 351, 11), (1185, 356, 11), (1185, 357, 11), (1185, 358, 11), (1186, 330, 11), (1186, 331, 11), (1186, 340, 11), (1186, 349, 11), (1186, 350, 11), (1186, 351, 11), (1186, 356, 11), (1186, 357, 11), (1187, 330, 11), (1187, 331, 11), (1187, 340, 11), (1187, 348, 11), (1187, 349, 11), (1187, 350, 11), (1187, 351, 11), (1187, 356, 11), (1187, 357, 11), (1188, 330, 11), (1188, 331, 11), (1188, 340, 11), (1188, 341, 11), (1188, 347, 11), (1188, 356, 11), (1188, 357, 11), (1189, 330, 11), (1189, 331, 11), (1189, 340, 11), (1189, 341, 11), (1189, 342, 11), (1189, 343, 11), (1189, 347, 11), (1189, 356, 11), (1189, 357, 11), (1190, 330, 11), (1190, 331, 11), (1190, 340, 11), (1190, 341, 11), (1190, 342, 11), (1190, 343, 11), (1190, 344, 11), (1190, 346, 11), (1190, 347, 11), (1190, 356, 11), (1190, 357, 11), (1191, 331, 11), (1191, 340, 11), (1191, 341, 11), (1191, 342, 11), (1191, 343, 11), (1191, 344, 11), (1191, 345, 11), (1191, 346, 11), (1191, 347, 11), (1191, 356, 11), (1191, 357, 11), (1192, 331, 11), (1192, 332, 11), (1192, 333, 11), (1192, 334, 11), (1192, 335, 11), (1192, 356, 11), (1192, 357, 11), (1193, 332, 11), (1193, 333, 11), (1193, 334, 11), (1193, 335, 11), (1193, 356, 11), (1193, 357, 11), (1194, 333, 11), (1194, 334, 11), (1194, 335, 11), (1194, 356, 11), (1195, 334, 11), (1195, 335, 11), (1195, 356, 11), (1196, 335, 11), (1196, 336, 11), (1196, 337, 11), (1196, 338, 11), (1196, 339, 11), (1196, 352, 11), (1196, 353, 11), (1196, 354, 11), (1196, 355, 11), (1196, 356, 11), (1197, 336, 11), (1197, 337, 11), (1197, 338, 11), (1197, 339, 11), (1197, 352, 11), (1197, 353, 11), (1197, 354, 11), (1197, 355, 11), (1197, 356, 11), (1198, 338, 11), (1198, 339, 11), (1198, 352, 11), (1198, 353, 11), (1198, 354, 11), (1198, 355, 11), (1199, 339, 11), (1199, 352, 11), (1199, 353, 11), (1199, 354, 11), (1200, 341, 11), (1200, 342, 11), (1200, 343, 11), (1200, 344, 11), (1200, 345, 11), (1200, 346, 11), (1200, 347, 11), (1200, 348, 11), (1200, 349, 11), (1200, 350, 11), (1200, 351, 11), (1200, 352, 11), (1201, 342, 11), (1201, 343, 11), (1201, 344, 11), (1201, 345, 11), (1201, 346, 11), (1201, 347, 11), (1201, 348, 11), (1201, 349, 11), (1201, 350, 11), (1201, 351, 11), (1202, 344, 11), (1202, 345, 11), (1202, 346, 11), (1202, 347, 11), (1202, 348, 11), (1202, 349, 11), (1202, 350, 11), (1203, 345, 11), (1203, 346, 11), (1203, 347, 11), (1203, 348, 11), (1203, 349, 11), (1204, 346, 11), (1204, 347, 11), (1204, 348, 11) ])
289 |
290 |
291 | def test_cover_geometry_multipoly1(tiler, mpoly):
292 | """A MultiPolygon geometry."""
293 | tiles = [tile for tile in cover_geometry(tiler, mpoly, 7)]
294 | assert len(tiles) == 8
295 | assert set(tiles) == set([(8, 38, 7), (4, 40, 7), (2, 41, 7), (0, 42, 7), (5, 40, 7), (7, 39, 7), (8, 39, 7), (6, 40, 7)])
296 |
297 |
298 | def test_cover_geometry_multipoly2(tiler, mpoly):
299 | """A MultiPolygon geometry."""
300 | tiles = [tile for tile in cover_geometry(tiler, mpoly, 10)]
301 | assert len(tiles) == 44
302 | assert set(tiles) == set([(23, 329, 10), (56, 318, 10), (70, 310, 10), (64, 315, 10), (67, 315, 10), (68, 310, 10), (63, 316, 10), (62, 316, 10), (71, 311, 10), (65, 314, 10), (68, 311, 10), (57, 318, 10), (70, 313, 10), (62, 317, 10), (4, 336, 10), (67, 314, 10), (64, 316, 10), (69, 310, 10), (66, 314, 10), (69, 313, 10), (68, 312, 10), (17, 330, 10), (63, 317, 10), (69, 312, 10), (58, 319, 10), (71, 312, 10), (68, 313, 10), (70, 311, 10), (69, 309, 10), (51, 321, 10), (70, 312, 10), (56, 317, 10), (61, 317, 10), (65, 315, 10), (65, 316, 10), (43, 323, 10), (68, 309, 10), (58, 318, 10), (34, 327, 10), (68, 314, 10), (66, 315, 10), (69, 311, 10), (68, 315, 10), (66, 316, 10)])
303 |
304 |
305 | def test_cover_geometry_multipoly3(tiler, mpoly):
306 | """A MultiPolygon geometry."""
307 | tiles = [tile for tile in cover_geometry(tiler, mpoly, [9, 10])]
308 | assert len(tiles) == 32
309 | assert set(tiles) == set([(31, 158, 9), (33, 157, 9), (34, 155, 9), (34, 156, 9),
310 | (4, 336, 10), (17, 330, 10), (23, 329, 10), (34, 327, 10), (43, 323, 10), (51, 321, 10), (56, 317, 10), (56, 318, 10), (57, 318, 10), (58, 318, 10), (58, 319, 10), (61, 317, 10), (64, 315, 10), (64, 316, 10), (65, 314, 10), (65, 315, 10), (65, 316, 10), (66, 316, 10), (68, 309, 10), (68, 314, 10), (68, 315, 10), (69, 309, 10), (70, 310, 10), (70, 311, 10), (70, 312, 10), (70, 313, 10), (71, 311, 10), (71, 312, 10) ])
311 |
312 |
313 | def test_cover_geometry_multipoly4(tiler, mpoly):
314 | """A MultiPolygon geometry."""
315 | tiles = [tile for tile in cover_geometry(tiler, mpoly, [9, 10, 11])]
316 | assert len(tiles) == 58
317 | assert set(tiles) == set([(34, 155, 9),
318 | (58, 318, 10), (62, 317, 10), (66, 314, 10), (66, 315, 10), (67, 315, 10), (68, 312, 10), (69, 312, 10), (70, 311, 10), (70, 312, 10), (71, 311, 10),
319 | (9, 673, 11), (34, 660, 11), (46, 658, 11), (47, 658, 11), (68, 654, 11), (86, 646, 11), (87, 646, 11), (102, 643, 11), (112, 635, 11), (112, 636, 11), (113, 636, 11), (113, 637, 11), (114, 637, 11), (115, 636, 11), (115, 637, 11), (116, 638, 11), (117, 638, 11), (123, 634, 11), (124, 633, 11), (125, 633, 11), (126, 632, 11), (126, 633, 11), (126, 634, 11), (129, 631, 11), (129, 632, 11), (130, 630, 11), (130, 632, 11), (131, 629, 11), (131, 630, 11), (131, 632, 11), (132, 632, 11), (134, 629, 11), (135, 629, 11), (136, 629, 11), (136, 630, 11), (137, 619, 11), (137, 626, 11), (137, 627, 11), (138, 619, 11), (138, 626, 11), (139, 619, 11), (139, 626, 11), (140, 620, 11), (140, 621, 11), (140, 626, 11), (141, 621, 11), (142, 624, 11) ])
320 |
321 |
322 | def test_cover_donut_webmercator(wmtiler, donut):
323 | tiles = [tile for tile in cover_geometry(wmtiler, donut, 16)]
324 | assert len(tiles) == 310
325 | assert set(tiles) == set([(18899, 23458, 16), (18896, 23451, 16), (18888, 23457, 16), (18898, 23457, 16), (18890, 23442, 16), (18894, 23449, 16), (18888, 23454, 16), (18900, 23457, 16), (18898, 23458, 16), (18890, 23447, 16), (18894, 23450, 16), (18895, 23459, 16), (18891, 23460, 16), (18896, 23450, 16), (18900, 23462, 16), (18898, 23463, 16), (18898, 23449, 16), (18894, 23447, 16), (18895, 23462, 16), (18891, 23459, 16), (18892, 23459, 16), (18898, 23464, 16), (18893, 23442, 16), (18898, 23450, 16), (18887, 23453, 16), (18896, 23446, 16), (18892, 23456, 16), (18893, 23445, 16), (18898, 23455, 16), (18887, 23456, 16), (18896, 23466, 16), (18896, 23443, 16), (18905, 23457, 16), (18892, 23461, 16), (18888, 23456, 16), (18893, 23448, 16), (18896, 23458, 16), (18897, 23460, 16), (18897, 23446, 16), (18891, 23447, 16), (18897, 23463, 16), (18893, 23454, 16), (18894, 23460, 16), (18891, 23442, 16), (18887, 23455, 16), (18897, 23458, 16), (18897, 23452, 16), (18901, 23449, 16), (18907, 23456, 16), (18894, 23457, 16), (18903, 23452, 16), (18895, 23450, 16), (18892, 23441, 16), (18897, 23455, 16), (18901, 23452, 16), (18889, 23457, 16), (18885, 23454, 16), (18890, 23457, 16), (18902, 23460, 16), (18891, 23452, 16), (18895, 23449, 16), (18897, 23464, 16), (18892, 23446, 16), (18897, 23450, 16), (18901, 23455, 16), (18889, 23452, 16), (18901, 23457, 16), (18890, 23458, 16), (18902, 23457, 16), (18891, 23451, 16), (18889, 23455, 16), (18901, 23460, 16), (18886, 23456, 16), (18902, 23458, 16), (18899, 23454, 16), (18895, 23443, 16), (18892, 23448, 16), (18889, 23450, 16), (18890, 23446, 16), (18902, 23453, 16), (18890, 23448, 16), (18886, 23455, 16), (18899, 23453, 16), (18895, 23446, 16), (18892, 23453, 16), (18889, 23445, 16), (18893, 23458, 16), (18902, 23454, 16), (18890, 23453, 16), (18899, 23448, 16), (18903, 23453, 16), (18899, 23462, 16), (18900, 23450, 16), (18893, 23461, 16), (18902, 23451, 16), (18890, 23454, 16), (18899, 23447, 16), (18895, 23464, 16), (18899, 23461, 16), (18900, 23455, 16), (18888, 23458, 16), (18884, 23453, 16), (18898, 23454, 16), (18893, 23464, 16), (18890, 23443, 16), (18894, 23454, 16), (18899, 23456, 16), (18900, 23452, 16), (18888, 23455, 16), (18900, 23458, 16), (18898, 23459, 16), (18898, 23445, 16), (18890, 23444, 16), (18895, 23458, 16), (18888, 23452, 16), (18898, 23460, 16), (18898, 23446, 16), (18894, 23444, 16), (18895, 23457, 16), (18891, 23458, 16), (18893, 23455, 16), (18893, 23441, 16), (18898, 23451, 16), (18894, 23441, 16), (18895, 23460, 16), (18891, 23457, 16), (18896, 23447, 16), (18892, 23457, 16), (18904, 23458, 16), (18893, 23444, 16), (18898, 23452, 16), (18904, 23456, 16), (18902, 23450, 16), (18896, 23462, 16), (18896, 23444, 16), (18905, 23456, 16), (18892, 23462, 16), (18893, 23447, 16), (18896, 23459, 16), (18903, 23460, 16), (18897, 23451, 16), (18897, 23445, 16), (18894, 23464, 16), (18903, 23458, 16), (18896, 23456, 16), (18891, 23446, 16), (18897, 23462, 16), (18893, 23453, 16), (18906, 23455, 16), (18894, 23461, 16), (18891, 23445, 16), (18887, 23454, 16), (18897, 23457, 16), (18896, 23448, 16), (18894, 23462, 16), (18903, 23455, 16), (18892, 23452, 16), (18892, 23442, 16), (18897, 23454, 16), (18901, 23451, 16), (18889, 23456, 16), (18885, 23453, 16), (18894, 23459, 16), (18891, 23455, 16), (18895, 23448, 16), (18904, 23459, 16), (18892, 23447, 16), (18897, 23449, 16), (18901, 23454, 16), (18901, 23456, 16), (18890, 23459, 16), (18891, 23450, 16), (18895, 23455, 16), (18892, 23444, 16), (18903, 23456, 16), (18889, 23454, 16), (18901, 23459, 16), (18890, 23460, 16), (18902, 23459, 16), (18899, 23457, 16), (18895, 23442, 16), (18905, 23455, 16), (18889, 23449, 16), (18890, 23449, 16), (18899, 23452, 16), (18892, 23454, 16), (18903, 23457, 16), (18889, 23444, 16), (18893, 23457, 16), (18902, 23455, 16), (18890, 23450, 16), (18899, 23451, 16), (18895, 23444, 16), (18900, 23451, 16), (18896, 23461, 16), (18889, 23447, 16), (18893, 23460, 16), (18890, 23455, 16), (18904, 23452, 16), (18899, 23446, 16), (18899, 23460, 16), (18900, 23448, 16), (18884, 23454, 16), (18893, 23463, 16), (18894, 23455, 16), (18903, 23459, 16), (18899, 23459, 16), (18900, 23453, 16), (18896, 23460, 16), (18900, 23459, 16), (18898, 23456, 16), (18890, 23445, 16), (18894, 23448, 16), (18891, 23462, 16), (18904, 23453, 16), (18888, 23453, 16), (18900, 23456, 16), (18898, 23461, 16), (18906, 23457, 16), (18898, 23447, 16), (18894, 23445, 16), (18895, 23456, 16), (18891, 23461, 16), (18900, 23447, 16), (18900, 23461, 16), (18898, 23462, 16), (18898, 23448, 16), (18894, 23446, 16), (18904, 23457, 16), (18895, 23463, 16), (18891, 23456, 16), (18895, 23445, 16), (18892, 23458, 16), (18893, 23443, 16), (18898, 23453, 16), (18894, 23443, 16), (18896, 23463, 16), (18896, 23445, 16), (18892, 23463, 16), (18893, 23446, 16), (18894, 23458, 16), (18887, 23457, 16), (18905, 23458, 16), (18892, 23460, 16), (18897, 23444, 16), (18893, 23449, 16), (18894, 23465, 16), (18896, 23457, 16), (18896, 23455, 16), (18897, 23461, 16), (18897, 23447, 16), (18904, 23455, 16), (18896, 23464, 16), (18900, 23454, 16), (18903, 23451, 16), (18891, 23444, 16), (18896, 23452, 16), (18897, 23456, 16), (18894, 23463, 16), (18903, 23454, 16), (18891, 23443, 16), (18896, 23449, 16), (18897, 23459, 16), (18892, 23443, 16), (18897, 23453, 16), (18901, 23450, 16), (18894, 23456, 16), (18895, 23461, 16), (18894, 23442, 16), (18891, 23454, 16), (18895, 23451, 16), (18897, 23448, 16), (18901, 23453, 16), (18889, 23458, 16), (18885, 23455, 16), (18890, 23456, 16), (18891, 23453, 16), (18897, 23465, 16), (18892, 23445, 16), (18889, 23453, 16), (18901, 23458, 16), (18902, 23456, 16), (18891, 23448, 16), (18905, 23454, 16), (18889, 23448, 16), (18901, 23461, 16), (18886, 23453, 16), (18899, 23455, 16), (18892, 23455, 16), (18889, 23451, 16), (18893, 23456, 16), (18902, 23452, 16), (18890, 23451, 16), (18886, 23454, 16), (18899, 23450, 16), (18895, 23447, 16), (18900, 23460, 16), (18889, 23446, 16), (18893, 23459, 16), (18906, 23456, 16), (18890, 23452, 16), (18899, 23449, 16), (18895, 23466, 16), (18899, 23463, 16), (18900, 23449, 16), (18904, 23454, 16), (18893, 23462, 16), (18889, 23459, 16), (18896, 23465, 16), (18895, 23465, 16)])
326 |
327 |
328 | def test_cover_donut_webmercator2(wmtiler, donut):
329 | tiles = [tile for tile in cover_geometry(wmtiler, donut, [14, 16, 17])]
330 | assert len(tiles) == 281
331 | assert set(tiles) == set([(4723, 5861, 14), (4723, 5864, 14), (4724, 5862, 14), (4724, 5864, 14), (4725, 5863, 14), (4725, 5864, 14), (18885, 23454, 16),
332 | (18886, 23454, 16), (18886, 23455, 16), (18887, 23454, 16), (18887, 23455, 16), (18887, 23456, 16), (18888, 23453, 16), (18888, 23454, 16), (18888, 23455, 16), (18888, 23456, 16), (18888, 23457, 16), (18889, 23449, 16), (18889, 23450, 16), (18889, 23451, 16), (18889, 23452, 16), (18889, 23453, 16), (18889, 23454, 16), (18889, 23455, 16), (18889, 23456, 16), (18889, 23457, 16), (18889, 23458, 16), (18890, 23443, 16), (18890, 23444, 16), (18890, 23445, 16), (18890, 23446, 16), (18890, 23447, 16), (18890, 23448, 16), (18890, 23450, 16), (18890, 23451, 16), (18890, 23452, 16), (18890, 23453, 16), (18890, 23454, 16), (18890, 23455, 16), (18890, 23456, 16), (18890, 23457, 16), (18890, 23458, 16), (18890, 23459, 16), (18891, 23442, 16), (18891, 23443, 16), (18891, 23444, 16), (18891, 23445, 16), (18891, 23446, 16), (18891, 23447, 16), (18891, 23451, 16), (18891, 23452, 16), (18891, 23453, 16), (18891, 23454, 16), (18891, 23455, 16), (18891, 23456, 16), (18891, 23457, 16), (18891, 23458, 16), (18891, 23459, 16), (18891, 23460, 16), (18892, 23442, 16), (18892, 23443, 16), (18892, 23453, 16), (18892, 23454, 16), (18892, 23455, 16), (18892, 23460, 16), (18892, 23461, 16), (18892, 23462, 16), (18893, 23442, 16), (18893, 23443, 16), (18893, 23448, 16), (18893, 23454, 16), (18893, 23455, 16), (18893, 23460, 16), (18893, 23461, 16), (18893, 23462, 16), (18893, 23463, 16), (18894, 23442, 16), (18894, 23443, 16), (18894, 23448, 16), (18894, 23449, 16), (18894, 23455, 16), (18894, 23460, 16), (18894, 23461, 16), (18894, 23462, 16), (18894, 23463, 16), (18894, 23464, 16), (18895, 23443, 16), (18895, 23448, 16), (18895, 23449, 16), (18895, 23450, 16), (18895, 23460, 16), (18895, 23461, 16), (18895, 23462, 16), (18895, 23463, 16), (18895, 23464, 16), (18895, 23465, 16), (18896, 23444, 16), (18896, 23445, 16), (18896, 23446, 16), (18896, 23447, 16), (18896, 23460, 16), (18896, 23461, 16), (18896, 23462, 16), (18896, 23463, 16), (18896, 23464, 16), (18896, 23465, 16), (18897, 23445, 16), (18897, 23446, 16), (18897, 23447, 16), (18897, 23452, 16), (18897, 23453, 16), (18897, 23455, 16), (18897, 23460, 16), (18897, 23461, 16), (18897, 23462, 16), (18897, 23463, 16), (18897, 23464, 16), (18898, 23446, 16), (18898, 23447, 16), (18898, 23452, 16), (18898, 23453, 16), (18898, 23454, 16), (18898, 23455, 16), (18898, 23460, 16), (18898, 23461, 16), (18898, 23462, 16), (18898, 23463, 16), (18899, 23447, 16), (18899, 23452, 16), (18899, 23453, 16), (18899, 23454, 16), (18899, 23455, 16), (18899, 23460, 16), (18899, 23461, 16), (18899, 23462, 16), (18900, 23449, 16), (18900, 23450, 16), (18900, 23451, 16), (18900, 23460, 16), (18900, 23461, 16), (18901, 23450, 16), (18901, 23451, 16), (18901, 23460, 16), (18902, 23451, 16), (18904, 23453, 16), (18904, 23454, 16), (18904, 23455, 16), (18904, 23456, 16), (18904, 23457, 16), (18904, 23458, 16), (18905, 23455, 16), (18905, 23456, 16), (18905, 23457, 16), (18906, 23456, 16),
333 | (37769, 46907, 17), (37769, 46908, 17), (37770, 46907, 17), (37771, 46907, 17), (37771, 46910, 17), (37772, 46907, 17), (37773, 46907, 17), (37773, 46912, 17), (37774, 46907, 17), (37774, 46914, 17), (37775, 46907, 17), (37775, 46914, 17), (37775, 46915, 17), (37776, 46916, 17), (37777, 46905, 17), (37777, 46916, 17), (37777, 46917, 17), (37778, 46918, 17), (37779, 46889, 17), (37779, 46890, 17), (37779, 46891, 17), (37779, 46892, 17), (37779, 46893, 17), (37779, 46894, 17), (37779, 46895, 17), (37779, 46896, 17), (37779, 46897, 17), (37779, 46918, 17), (37779, 46919, 17), (37780, 46898, 17), (37780, 46899, 17), (37780, 46920, 17), (37781, 46885, 17), (37781, 46899, 17), (37781, 46920, 17), (37781, 46921, 17), (37782, 46896, 17), (37782, 46897, 17), (37782, 46901, 17), (37782, 46922, 17), (37783, 46896, 17), (37783, 46922, 17), (37783, 46923, 17), (37783, 46924, 17), (37784, 46904, 17), (37784, 46905, 17), (37785, 46883, 17), (37785, 46896, 17), (37785, 46905, 17), (37785, 46926, 17), (37786, 46883, 17), (37786, 46906, 17), (37786, 46907, 17), (37787, 46882, 17), (37787, 46883, 17), (37787, 46898, 17), (37787, 46907, 17), (37787, 46928, 17), (37788, 46882, 17), (37788, 46883, 17), (37788, 46908, 17), (37788, 46909, 17), (37789, 46883, 17), (37789, 46900, 17), (37789, 46901, 17), (37789, 46909, 17), (37789, 46930, 17), (37790, 46884, 17), (37790, 46885, 17), (37790, 46902, 17), (37790, 46911, 17), (37791, 46885, 17), (37791, 46902, 17), (37791, 46903, 17), (37791, 46932, 17), (37792, 46886, 17), (37792, 46887, 17), (37792, 46904, 17), (37792, 46932, 17), (37793, 46904, 17), (37793, 46905, 17), (37793, 46911, 17), (37794, 46889, 17), (37794, 46930, 17), (37795, 46908, 17), (37795, 46909, 17), (37796, 46891, 17), (37796, 46928, 17), (37798, 46893, 17), (37798, 46926, 17), (37798, 46927, 17), (37799, 46926, 17), (37800, 46895, 17), (37800, 46896, 17), (37800, 46897, 17), (37800, 46924, 17), (37800, 46925, 17), (37801, 46897, 17), (37801, 46924, 17), (37802, 46898, 17), (37802, 46899, 17), (37802, 46922, 17), (37802, 46923, 17), (37803, 46899, 17), (37803, 46922, 17), (37804, 46900, 17), (37804, 46901, 17), (37804, 46920, 17), (37804, 46921, 17), (37805, 46901, 17), (37805, 46920, 17), (37806, 46903, 17), (37806, 46920, 17), (37808, 46905, 17), (37808, 46918, 17), (37810, 46908, 17), (37810, 46909, 17), (37810, 46916, 17), (37811, 46909, 17), (37811, 46916, 17), (37812, 46910, 17), (37812, 46911, 17), (37812, 46914, 17), (37812, 46915, 17), (37813, 46911, 17), (37813, 46914, 17), (37814, 46913, 17) ])
334 |
--------------------------------------------------------------------------------
/tests/test_utmtiling.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tiletanic.tileschemes import UTM10kmTiling
4 |
5 | @pytest.fixture
6 | def tiler():
7 | return UTM10kmTiling()
8 |
9 | def test_bounds(tiler):
10 | """Bounds should encompass the bounds of the UTM zone."""
11 | assert tiler.bounds.xmin <= 0.
12 | assert tiler.bounds.xmax >= 1_000_000.
13 | assert tiler.bounds.ymin <= -10_000_000.
14 | assert tiler.bounds.ymax >= 10_000_000.
15 |
16 | def test_tile(tiler):
17 | """Tile generation from UTM coordinates and zoom."""
18 |
19 | # Center of the zone should be at 500_000, 0 at every zoom, so
20 | # check that's the center tile.
21 | for z in range(0, 19):
22 | assert tiler.tile(500_000, 0, z) == (2**z // 2, 2**z // 2, z)
23 |
24 | z = 11 # The 10km zoom level.
25 | assert tiler.tile(500_000 - 5_000, 0, z) == (2**z // 2 - 1, 2**z // 2, z)
26 | assert tiler.tile(500_000 + 10_000, 0, z) == (2**z // 2 + 1, 2**z // 2, z)
27 | assert tiler.tile(500_000, 5_000, z) == (2**z // 2, 2**z // 2 - 1, z)
28 | assert tiler.tile(500_000, -10_000, z) == (2**z // 2, 2**z // 2 + 1, z)
29 |
30 | def test_parent(tiler):
31 | """Parent of a tile."""
32 | assert tiler.parent(0, 0, 1) == (0, 0, 0)
33 | assert tiler.parent(1, 0, 1) == (0, 0, 0)
34 |
35 | assert tiler.parent(2, 3, 2) == (1, 1, 1)
36 | assert tiler.parent(3, 0, 2) == (1, 0, 1)
37 |
38 |
39 | def test_children1(tiler):
40 | """Children of a level 0 tile."""
41 | assert set(tiler.children(0, 0, 0)) == {(0, 0, 1),
42 | (1, 0, 1),
43 | (0, 1, 1),
44 | (1, 1, 1)}
45 |
46 |
47 | def test_children2(tiler):
48 | """Children of a level > 0 tile."""
49 | assert set(tiler.children(1, 1, 1)) == {(2, 2, 2),
50 | (2, 3, 2),
51 | (3, 2, 2),
52 | (3, 3, 2)}
53 |
54 |
55 | def test_quadkey(tiler):
56 | """Quadkey generation from tile coordinates."""
57 | assert tiler.quadkey(0, 0, 1) == '0'
58 | assert tiler.quadkey(1, 0, 1) == '1'
59 |
60 | assert tiler.quadkey(0, 0, 2) == '00'
61 | assert tiler.quadkey(1, 0, 2) == '01'
62 | assert tiler.quadkey(0, 1, 2) == '02'
63 | assert tiler.quadkey(1, 1, 2) == '03'
64 | assert tiler.quadkey(2, 0, 2) == '10'
65 | assert tiler.quadkey(3, 0, 2) == '11'
66 | assert tiler.quadkey(2, 1, 2) == '12'
67 | assert tiler.quadkey(3, 1, 2) == '13'
68 |
69 | assert tiler.quadkey(20, 35, 9) == '000210122'
70 |
71 |
72 | def test_ul(tiler):
73 | """Upper left coordinates of input tile."""
74 | z = 11
75 |
76 | assert tiler.ul(2**z // 2, 2**z // 2 - 1, z) == (500_000, 10_000)
77 | assert tiler.ul(2**z // 2, 2**z // 2, z) == (500_000, 0)
78 | assert tiler.ul(2**z // 2, 2**z // 2 + 1, z) == (500_000, -10_000)
79 |
80 | assert tiler.ul(2**z // 2 - 1, 2**z // 2, z) == (490_000, 0)
81 | assert tiler.ul(2**z // 2 + 1, 2**z // 2, z) == (510_000, 0)
82 |
83 |
84 | def test_br(tiler):
85 | """Upper left coordinates of input tile."""
86 | z = 11
87 |
88 | assert tiler.br(2**z // 2 - 1, 2**z // 2 - 1, z) == (500_000, 0)
89 | assert tiler.br(2**z // 2 - 1, 2**z // 2, z) == (500_000, -10_000)
90 | assert tiler.br(2**z // 2 - 1, 2**z // 2 + 1, z) == (500_000, -20_000)
91 |
92 | assert tiler.br(2**z // 2, 2**z // 2 - 1, z) == (510_000, 0)
93 | assert tiler.br(2**z // 2 + 1, 2**z // 2 - 1, z) == (520_000, 0)
94 |
95 |
96 | def test_bbox(tiler):
97 | """Bounding boxes of tiles."""
98 | z = 11
99 |
100 | assert tiler.bbox(2**z // 2, 2**z // 2 - 1, z) == (500000, 0, 510000, 10000)
101 | assert tiler.bbox(2**z // 2, 2**z // 2, z) == (500000, -10000, 510000, 0)
102 | assert tiler.bbox(2**z // 2 + 1, 2**z // 2 - 1, z) == (510000, 0, 520000, 10000)
103 | assert tiler.bbox(2**z // 2 - 1, 2**z // 2 + 1, z) == (490000, -20000, 500000, -10000)
104 |
105 | z = 10
106 | assert tiler.bbox(2**z // 2, 2**z // 2 - 1, z) == (500000, 0, 520000, 20000)
107 |
108 | z = 8
109 | assert tiler.bbox(2**z // 2, 2**z // 2 - 1, z) == (500000, 0, 580000, 80000)
110 |
111 | def test_quadkey_to_tile1(tiler):
112 | """Quadkey to tile exceptions."""
113 | with pytest.raises(ValueError):
114 | tiler.quadkey_to_tile('4')
115 |
116 |
117 | def test_quadkey_to_tile2(tiler):
118 | """Quadkey to tile."""
119 | assert tiler.quadkey_to_tile('0') == (0, 0, 1)
120 | assert tiler.quadkey_to_tile('130232101') == (405, 184, 9)
121 |
--------------------------------------------------------------------------------
/tests/test_webmercator.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from tiletanic.tileschemes import WebMercator
4 |
5 | @pytest.fixture
6 | def tiler():
7 | return WebMercator()
8 |
9 |
10 | def test_bounds(tiler):
11 | """Web Mercator bounds."""
12 | assert tiler.bounds.xmin == -20037508.342789244
13 | assert tiler.bounds.xmax == 20037508.342789244
14 | assert tiler.bounds.ymin == -20037508.342789244
15 | assert tiler.bounds.ymax == 20037508.342789244
16 |
17 |
18 | def test_tile(tiler):
19 | """Tile generation from gespatial coordinates and zoom."""
20 | assert tiler.tile(0., 0., 0) == (0, 0, 0)
21 |
22 | assert tiler.tile((-20037508.342789244 + -10018754.171394622)/2.,
23 | (-20037508.342789244 + -10018754.171394622)/2.,
24 | 2) == (0, 3, 2)
25 |
26 | assert tiler.tile((-12523442.714243278 + -10018754.171394622)/2.,
27 | (5009377.085697312 + 7514065.628545966)/2.,
28 | 4) == (3, 5, 4)
29 |
30 | assert tiler.tile((14763964.887338366 + 14766410.87224349)/2.,
31 | (-3030575.297450669 + -3028129.3125455417)/2.,
32 | 14) == (14228, 9430, 14)
33 |
34 |
35 | def test_parent(tiler):
36 | """Parent of a tile."""
37 | assert tiler.parent(0, 0, 1) == (0, 0, 0)
38 | assert tiler.parent(1, 0, 1) == (0, 0, 0)
39 |
40 | assert tiler.parent(2, 3, 2) == (1, 1, 1)
41 | assert tiler.parent(3, 0, 2) == (1, 0, 1)
42 |
43 |
44 | def test_children1(tiler):
45 | """Children of a level 0 tile."""
46 | assert set(tiler.children(0, 0, 0)) == {(0, 0, 1),
47 | (1, 0, 1),
48 | (0, 1, 1),
49 | (1, 1, 1)}
50 |
51 |
52 | def test_children2(tiler):
53 | """Children of a level > 0 tile."""
54 | assert set(tiler.children(1, 1, 1)) == {(2, 2, 2),
55 | (2, 3, 2),
56 | (3, 2, 2),
57 | (3, 3, 2)}
58 |
59 |
60 | def test_quadkey(tiler):
61 | """Quadkey generation from tile coordinates."""
62 | assert tiler.quadkey(0, 0, 1) == '0'
63 | assert tiler.quadkey(1, 0, 1) == '1'
64 |
65 | assert tiler.quadkey(0, 0, 2) == '00'
66 | assert tiler.quadkey(1, 0, 2) == '01'
67 | assert tiler.quadkey(0, 1, 2) == '02'
68 | assert tiler.quadkey(1, 1, 2) == '03'
69 | assert tiler.quadkey(2, 0, 2) == '10'
70 | assert tiler.quadkey(3, 0, 2) == '11'
71 | assert tiler.quadkey(2, 1, 2) == '12'
72 | assert tiler.quadkey(3, 1, 2) == '13'
73 |
74 | assert tiler.quadkey(20, 35, 9) == '000210122'
75 |
76 |
77 | def test_ul(tiler):
78 | """Upper left coordinates of input tile."""
79 | assert tiler.ul(0, 0, 0) == (-20037508.342789244, 20037508.342789244)
80 |
81 | assert tiler.ul(0, 0, 1) == (-20037508.342789244, 20037508.342789244)
82 | assert tiler.ul(1, 1, 1) == (0.0, 0.0)
83 |
84 | assert tiler.ul(3, 6, 3) == (-5009377.085697312, -10018754.17139462)
85 | assert tiler.ul(4, 6, 3) == (0.0, -10018754.17139462)
86 | assert tiler.ul(118, 35, 7) == (16906647.66422843, 9079495.967826376)
87 | assert tiler.ul(84, 124, 8) == (-6887893.4928338025, 626172.1357121654)
88 |
89 |
90 | def test_br(tiler):
91 | """Bottom right coordinates of input tile."""
92 | assert tiler.br(0, 0, 0) == (20037508.342789244, -20037508.342789244)
93 |
94 | assert tiler.br(0, 0, 1) == (0.0, 0.0)
95 | assert tiler.br(1, 1, 1) == (20037508.342789244, -20037508.342789244)
96 |
97 | assert tiler.br(2, 5, 3) == (-5009377.085697312, -10018754.17139462)
98 | assert tiler.br(3, 5, 3) == (0.0, -10018754.17139462)
99 | assert tiler.br(117, 34, 7) == (16906647.66422843, 9079495.967826376)
100 | assert tiler.br(83, 123, 8) == (-6887893.4928338025, 626172.1357121654)
101 |
102 |
103 | def test_bbox(tiler):
104 | """Bounding boxes of tiles."""
105 | assert tiler.bbox(1, 5, 3) == (-15028131.257091932, -10018754.17139462, -10018754.171394622, -5009377.085697312)
106 | assert tiler.bbox(77, 93, 8) == (-7983694.730330089, 5322463.153553393, -7827151.696402049, 5479006.187481433)
107 | assert tiler.bbox(27685, 19041, 15) == (13821037.70641243, -3250713.9389119744, 13822260.698864993, -3249490.9464594126)
108 |
109 |
110 | def test_quadkey_to_tile1(tiler):
111 | """Quadkey to tile exceptions."""
112 | with pytest.raises(ValueError):
113 | tiler.quadkey_to_tile('4')
114 |
115 |
116 | def test_quadkey_to_tile2(tiler):
117 | """Quadkey to tile."""
118 | assert tiler.quadkey_to_tile('0') == (0, 0, 1)
119 | assert tiler.quadkey_to_tile('130232101') == (405, 184, 9)
120 |
--------------------------------------------------------------------------------
/tests/test_webmercatorbl.py:
--------------------------------------------------------------------------------
1 | """Many of the tests were derived from here:
2 |
3 | http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/
4 | """
5 | import pytest
6 |
7 | from tiletanic.tileschemes import WebMercatorBL
8 |
9 | @pytest.fixture
10 | def tiler():
11 | return WebMercatorBL()
12 |
13 |
14 | def test_bounds(tiler):
15 | """Web Mercator bounds."""
16 | assert tiler.bounds.xmin == -20037508.342789244
17 | assert tiler.bounds.xmax == 20037508.342789244
18 | assert tiler.bounds.ymin == -20037508.342789244
19 | assert tiler.bounds.ymax == 20037508.342789244
20 |
21 |
22 | def test_tile(tiler):
23 | """Tile generation from gespatial coordinates and zoom."""
24 | assert tiler.tile(0., 0., 0) == (0, 0, 0)
25 |
26 | assert tiler.tile((-20037508.342789244 + -10018754.171394622)/2.,
27 | (-20037508.342789244 + -10018754.171394622)/2.,
28 | 2) == (0, 0, 2)
29 |
30 | assert tiler.tile((-12523442.714243278 + -10018754.171394622)/2.,
31 | (5009377.085697312 + 7514065.628545966)/2.,
32 | 4) == (3, 10, 4)
33 |
34 | assert tiler.tile((13492052.736673031 + 13494498.721578155)/2.,
35 | (1849164.5882749856 + 1851610.5731801093)/2.,
36 | 14) == (13708, 8948, 14)
37 |
38 |
39 | def test_parent(tiler):
40 | """Parent of a tile."""
41 | assert tiler.parent(0, 0, 1) == (0, 0, 0)
42 | assert tiler.parent(1, 0, 1) == (0, 0, 0)
43 |
44 | assert tiler.parent(2, 3, 2) == (1, 1, 1)
45 | assert tiler.parent(3, 0, 2) == (1, 0, 1)
46 |
47 |
48 | def test_children1(tiler):
49 | """Children of a level 0 tile."""
50 | assert set(tiler.children(0, 0, 0)) == {(0, 0, 1),
51 | (1, 0, 1),
52 | (0, 1, 1),
53 | (1, 1, 1)}
54 |
55 |
56 | def test_children2(tiler):
57 | """Children of a level > 0 tile."""
58 | assert set(tiler.children(1, 1, 1)) == {(2, 2, 2),
59 | (2, 3, 2),
60 | (3, 2, 2),
61 | (3, 3, 2)}
62 |
63 |
64 | def test_quadkey(tiler):
65 | "Quadkey generation from tile coordinates."""
66 | assert tiler.quadkey(0, 0, 1) == '0'
67 | assert tiler.quadkey(1, 0, 1) == '1'
68 |
69 | assert tiler.quadkey(0, 0, 2) == '00'
70 | assert tiler.quadkey(1, 0, 2) == '01'
71 | assert tiler.quadkey(0, 1, 2) == '02'
72 | assert tiler.quadkey(1, 1, 2) == '03'
73 | assert tiler.quadkey(2, 0, 2) == '10'
74 | assert tiler.quadkey(3, 0, 2) == '11'
75 | assert tiler.quadkey(2, 1, 2) == '12'
76 | assert tiler.quadkey(3, 1, 2) == '13'
77 |
78 | assert tiler.quadkey(20, 35, 9) == '000210122'
79 |
80 |
81 | def test_ul(tiler):
82 | """Upper left coordinates of input tile."""
83 | assert tiler.ul(0, 0, 1) == (-20037508.342789244, 0.0)
84 | assert tiler.ul(1, 0, 1) == (0.0, 0.0)
85 |
86 | assert tiler.ul(3, 1, 3) == (-5009377.085697312, -10018754.171394622)
87 | assert tiler.ul(4, 1, 3) == (0.0, -10018754.171394622)
88 | assert tiler.ul(3, 2, 3) == (-5009377.085697312, -5009377.085697312)
89 | assert tiler.ul(4, 2, 3) == (0.0, -5009377.085697312)
90 |
91 |
92 | def test_br(tiler):
93 | """Bottom right coordinates of input tile."""
94 | assert tiler.br(0, 1, 1) == (0.0, 0.0)
95 | assert tiler.br(1, 1, 1) == (20037508.342789244, 0.0)
96 |
97 | assert tiler.br(3, 1, 3) == (0.0, -15028131.257091932)
98 | assert tiler.br(4, 1, 3) == (5009377.085697312, -15028131.257091932)
99 | assert tiler.br(3, 2, 3) == (0.0, -10018754.171394622)
100 | assert tiler.br(4, 2, 3) == (5009377.085697312, -10018754.171394622)
101 |
102 |
103 | def test_bbox(tiler):
104 | """Bounding boxes of tiles."""
105 | assert tiler.bbox(0, 0, 1) == (-20037508.342789244, -20037508.342789244, 0.0, 0.0)
106 | assert tiler.bbox(1, 0, 1) == (0.0, -20037508.342789244, 20037508.342789244, 0.0)
107 |
108 | assert tiler.bbox(3, 1, 3) == (-5009377.085697312, -15028131.257091932, 0.0, -10018754.171394622)
109 | assert tiler.bbox(4, 1, 3) == (0, -15028131.257091932, 5009377.085697312, -10018754.171394622)
110 | assert tiler.bbox(3, 2, 3) == (-5009377.085697312, -10018754.171394622, 0.0, -5009377.085697312)
111 | assert tiler.bbox(4, 2, 3) == (0.0, -10018754.171394622, 5009377.085697312, -5009377.085697312)
112 |
113 |
114 |
115 | def test_quadkey(tiler):
116 | """Tile to quadkey."""
117 | assert not tiler.quadkey(0, 0, 0)
118 |
119 | assert tiler.quadkey(3, 1, 3) == '231'
120 | assert tiler.quadkey(4, 1, 3) == '320'
121 | assert tiler.quadkey(3, 2, 3) == '213'
122 | assert tiler.quadkey(4, 2, 3) == '302'
123 |
124 | assert tiler.quadkey(199744, 179200, 18) == '130200112223222222'
125 | assert tiler.quadkey(84201, 103979, 18) == '210320300233121201'
126 |
127 | def test_quadkey_to_tile1(tiler):
128 | """Quadkey to tile exceptions."""
129 | with pytest.raises(ValueError):
130 | tiler.quadkey_to_tile('4')
131 |
132 |
133 | def test_quadkey_to_tile2(tiler):
134 | """Quadkey to tile."""
135 | assert tiler.quadkey_to_tile('0') == (0, 1, 1)
136 | assert tiler.quadkey_to_tile('130232101') == (405, 327, 9)
137 | assert tiler.quadkey_to_tile('130200112223222222') == (199744, 179200, 18)
138 | assert tiler.quadkey_to_tile('210320300233121201') == (84201, 103979, 18)
139 |
--------------------------------------------------------------------------------
/tiletanic.info.yml:
--------------------------------------------------------------------------------
1 | data_classification:
2 | - public
3 |
4 | poc:
5 | - name: Pete Schmitt
6 | email: Peter.Schmitt@maxar.com
7 |
--------------------------------------------------------------------------------
/tiletanic/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import Tile, Coords, CoordsBbox
2 | from . import tileschemes, tilecover
3 |
--------------------------------------------------------------------------------
/tiletanic/base.py:
--------------------------------------------------------------------------------
1 | """Common data structures and classes for Tiletanic."""
2 | from collections import namedtuple
3 |
4 | Tile = namedtuple('Tile', ['x', 'y', 'z'])
5 | Coords = namedtuple('Coords', ['x', 'y'])
6 | CoordsBbox = namedtuple('CoordsBbox', ['xmin', 'ymin', 'xmax', 'ymax'])
7 |
8 |
--------------------------------------------------------------------------------
/tiletanic/cli.py:
--------------------------------------------------------------------------------
1 | #TODO:
2 | # Add click tests
3 | # Add documentation - how to run with a file vs stdout, document arguments, etc.
4 |
5 | import click
6 | import geojson
7 | from shapely import geometry, ops, prepared
8 | import tiletanic
9 |
10 | @click.group()
11 | @click.version_option()
12 | def cli():
13 | """CLI entry point for tiletanic"""
14 | pass
15 |
16 | @cli.command()
17 | @click.option('--tilescheme', default="DGTiling",
18 | type=click.Choice(['DGTiling']),
19 | help="DGTiling is the only supported Tiling Scheme at "
20 | "this time.")
21 | @click.argument('aoi_geojson', type=click.File('r'))
22 | @click.option('--zoom', default=9, type=click.IntRange(0,26),
23 | help="Zoom level at which to generate tile covering of "
24 | "AOI_GEOJSON. Default=9")
25 | @click.option('--adjacent/--no-adjacent', default = False,
26 | help="Include all tiles that have at least one boundary "
27 | "point in common, but not necessarily interior "
28 | "points. Default=do not include adjacent tiles")
29 | @click.option('--quadkey/--no-quadkey', default=True,
30 | help="Output option to prints the quadkeys of the tile "
31 | "covering generated. Default prints quadkeys")
32 | def cover_geometry(tilescheme, aoi_geojson, zoom, adjacent, quadkey):
33 | """Calculate a tile covering for an input AOI_GEOJSON at a particular
34 | ZOOM level using the given TILESCHEME.
35 |
36 | AOI_GEOJSON - Area of Interest which needs to be chopped into a
37 | tile covering encoded as GeoJSON. Should be either a single
38 | Feature or a FeatureCollection. Read in from positional argument
39 | or stdin.
40 |
41 | Example with geojson file input:
42 |
43 | \b
44 | $ tiletanic cover_geometry aoi.geojson
45 | 021323330
46 |
47 | Example with stdin input:
48 |
49 | \b
50 | $ cat << EOF | tiletanic cover_geometry -
51 | > {
52 | > "geometry": {
53 | > "coordinates": [
54 | > [
55 | > [ -101.953125, 43.59375],
56 | > [ -101.953125, 44.296875],
57 | > [ -102.65625, 44.296875],
58 | > [ -102.65625, 43.59375],
59 | > [ -101.953125, 43.59375]
60 | > ]
61 | > ],
62 | > "type": "Polygon"
63 | > },
64 | > "properties": {},
65 | > "type": "Feature"
66 | > }
67 | > EOF
68 | 021323330
69 |
70 | """
71 |
72 | if tilescheme == 'DGTiling':
73 | scheme = tiletanic.tileschemes.DGTiling()
74 | else:
75 | raise ValueError("tilescheme '{}' is unsupported.").format(tilescheme)
76 |
77 | aoi = geojson.loads( aoi_geojson.read() )
78 |
79 | if 'type' not in aoi:
80 | raise ValueError("The 'AOI_GEOJSON' doesn't have a 'type' member. Is it valid GeoJSON?")
81 | elif aoi['type'] == 'FeatureCollection':
82 | geom = ops.unary_union([geometry.shape(f['geometry'])
83 | for f in aoi['features']
84 | if f['geometry']['type'].endswith('Polygon')])
85 | elif aoi['type'] == 'Feature':
86 | geom = geometry.shape(aoi['geometry'])
87 | else:
88 | raise ValueError("The AOI_GEOJSON 'type' %s is unsupported, " % aoi['type'] +
89 | "it must be 'Feature' or 'FeatureCollection'")
90 |
91 | tiles = tiletanic.tilecover.cover_geometry(scheme, geom, zoom)
92 |
93 | if not adjacent:
94 | tiles = _tiles_inside_geom(scheme, tiles, geom)
95 |
96 | if quadkey:
97 | qks = [scheme.quadkey(t) for t in tiles]
98 | click.echo( "\n".join( qks ) )
99 |
100 |
101 | def _tiles_inside_geom(tilescheme, tiles, geom):
102 | """Filters out tiles do not contain the geometry geom
103 |
104 | Consider the nine adjacent tiles:
105 |
106 | -------------
107 | | 1 | 2 | 3 |
108 | -------------
109 | | 4 | 5 | 6 |
110 | -------------
111 | | 7 | 8 | 9 |
112 | -------------
113 |
114 | if the AOI is 5, _tiles_inside_geom will only return 5. Note that
115 | tiletanic.tilecover.cover_geometry returns all 9 tiles
116 |
117 | Args:
118 | tilescheme: The tile scheme to use.
119 | tiles: list iterable collection of tiles
120 | geom: Shapely Geometry area of interest
121 | """
122 | prep_geom = prepared.prep(geom)
123 | for t in tiles:
124 | coords = tilescheme.bbox(t)
125 | tile_geom = geometry.Polygon(((coords.xmin, coords.ymin),
126 | (coords.xmax, coords.ymin),
127 | (coords.xmax, coords.ymax),
128 | (coords.xmin, coords.ymax),
129 | (coords.xmin, coords.ymin)))
130 |
131 | # Touches: The Geometries have at least one boundary point in
132 | # common, but no interior points
133 | if not prep_geom.touches(tile_geom):
134 | yield t
135 |
--------------------------------------------------------------------------------
/tiletanic/tilecover.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 |
3 | from shapely import geometry, ops, prepared
4 |
5 | from .base import Tile
6 |
7 |
8 | def cover_geometry(tilescheme, geom, zooms):
9 | """Covers the provided geometry with tiles.
10 |
11 | Args:
12 | tilescheme: The tile scheme to use. This needs to implement
13 | the public protocal of the schemes defined within
14 | tiletanic.
15 | geom: The geometry we would like to cover. This should be a
16 | shapely geometry.
17 | zooms: The zoom levels of the tiles to cover geom with. If
18 | you provide an iterable of zoom levels, you'll get the
19 | biggest tiles available that cover the geometry at
20 | those levels.
21 |
22 | Yields:
23 | An iterator of Tile objects ((x, y, z) named tuples) that
24 | cover the input geometry.
25 | """
26 | # Only shapely geometries allowed.
27 | if not isinstance(geom, geometry.base.BaseGeometry):
28 | raise ValueError("Input 'geom' is not a known shapely geometry type")
29 |
30 | if geom.is_empty:
31 | return
32 |
33 | zooms = zooms if isinstance(zooms, Iterable) else [zooms]
34 |
35 | # Generate the covering.
36 | prep_geom = prepared.prep(geom)
37 | if isinstance(geom, (geometry.Polygon, geometry.MultiPolygon)):
38 | for tile in _cover_polygonal(tilescheme, Tile(0, 0, 0), prep_geom, geom, zooms):
39 | yield tile
40 | else:
41 | for tile in _cover_geometry(tilescheme, Tile(0, 0, 0), prep_geom, geom, zooms):
42 | yield tile
43 |
44 |
45 | def _cover_geometry(tilescheme, curr_tile, prep_geom, geom, zooms):
46 | """Covers geometries with tiles by recursion.
47 |
48 | Args:
49 | tilescheme: The tile scheme to use. This needs to implement
50 | the public protocal of the schemes defined within
51 | tiletanic.
52 | curr_tile: The current tile in the recursion scheme.
53 | prep_geom: The prepared version of the geometry we would like to cover.
54 | geom: The shapely geometry we would like to cover.
55 | zooms: The zoom levels to recurse to.
56 |
57 | Yields:
58 | An iterator of Tile objects ((x, y, z) tuples) that
59 | cover the input geometry.
60 | """
61 | if prep_geom.intersects(geometry.box(*tilescheme.bbox(curr_tile))):
62 | if curr_tile.z in zooms:
63 | yield curr_tile
64 | else:
65 | for tile in (tile for child_tile in tilescheme.children(curr_tile)
66 | for tile in _cover_geometry(tilescheme, child_tile,
67 | prep_geom, geom,
68 | zooms)):
69 | yield tile
70 |
71 |
72 | def _cover_polygonal(tilescheme, curr_tile, prep_geom, geom, zooms):
73 | """Covers polygonal geometries with tiles by recursion.
74 |
75 | This is method is slightly more efficient than _cover_geometry in
76 | that we can check if a tile is completely covered by a geometry
77 | and if so, skip directly to the max zoom level to fetch the
78 | covered tiles.
79 |
80 | Args:
81 | tilescheme: The tile scheme to use. This needs to implement
82 | the public protocal of the schemes defined within
83 | tiletanic.
84 | curr_tile: The current tile in the recursion scheme.
85 | prep_geom: The prepared version of the polygonal geometry we
86 | would like to cover.
87 | geom: The shapely polygonal geometry we would like to cover.
88 | zooms: The zoom levels to recurse to.
89 |
90 | Yields:
91 | An iterator of Tile objects ((x, y, z) tuples) that
92 | cover the input polygonal geometry.
93 | """
94 | tile_geom = geometry.box(*tilescheme.bbox(curr_tile))
95 | if prep_geom.intersects(tile_geom):
96 | if curr_tile.z == max(zooms):
97 | yield curr_tile
98 | elif prep_geom.contains(tile_geom):
99 | if curr_tile.z in zooms:
100 | yield curr_tile
101 | else:
102 | for tile in (tile for child_tile in tilescheme.children(curr_tile)
103 | for tile in _containing_tiles(tilescheme, child_tile, zooms)):
104 | yield tile
105 | else:
106 | tiles = []
107 | coverage = 0
108 | for tile in (tile for child_tile in tilescheme.children(curr_tile)
109 | for tile in _cover_polygonal(tilescheme, child_tile,
110 | prep_geom, geom, zooms)):
111 | if curr_tile.z in zooms:
112 | tiles.append(tile)
113 | coverage += 4 ** (max(zooms) - tile.z)
114 | else:
115 | yield tile
116 |
117 | if curr_tile.z in zooms and coverage == 4 ** (max(zooms) - curr_tile.z):
118 | yield curr_tile
119 | else:
120 | for tile in tiles:
121 | yield tile
122 |
123 |
124 | def _containing_tiles(tilescheme, curr_tile, zooms):
125 | """Given a Tile, returns the tiles that compose that tile at the
126 | zoom level provided.
127 |
128 | Args:
129 | tilescheme: The tile scheme to use. This needs to implement
130 | the public protocal of the schemes defined within
131 | tiletanic.
132 | curr_tile: The current tile in the recursion scheme.
133 | zooms: The zoom levels to recurse to.
134 |
135 | Yields:
136 | An iterator of Tile objects ((x, y, z) tuples) that
137 | compose the input tile.
138 | """
139 | if curr_tile.z in zooms:
140 | yield curr_tile
141 | else:
142 | for tile in (tile for child_tile in tilescheme.children(curr_tile)
143 | for tile in _containing_tiles(tilescheme, child_tile, zooms)):
144 | yield tile
145 |
--------------------------------------------------------------------------------
/tiletanic/tileschemes.py:
--------------------------------------------------------------------------------
1 | """Tiling schemes precanned for Tiletanic.
2 |
3 | The public APIs of these classes are all that another class would need
4 | to implement in order to use any of the algorithms defined in the
5 | Tiletanic package.
6 | """
7 | from math import floor, ceil, log2
8 | import re
9 |
10 | from . import Tile, Coords, CoordsBbox
11 |
12 | qk_regex = re.compile(r'[0-3]+$')
13 |
14 | class BasicTilingBottomLeft(object):
15 | """BasicTilingBottomLeft is a class for representing a tiling
16 | scheme defined by some bounding box. The x direction considered
17 | "left" to "right" and the y direction is considered "bottom" to
18 | "top" when we thing of both geospatial and tile-centric
19 | coordinates.
20 |
21 | Note that quadkey indices are defined like this for each
22 | tile's children::
23 |
24 | ---------
25 | | 2 | 3 |
26 | --------
27 | | 0 | 1 |
28 | ---------
29 |
30 | The origin of a tile (row, column) is the bottom left of the
31 | bounds, as opposed to some of the web mercator schemes!
32 |
33 | Attributes:
34 | bounds: The bounding box for the projection that the tiling
35 | scheme is defined for.
36 | """
37 | def __init__(self, xmin, ymin, xmax, ymax):
38 | """Constructs an object that generates tile bounds for you.
39 |
40 | BasicTilingBottomLeft(-180, -90, 180, 270) would give you a
41 | square tiling scheme defined over those bounds. Note that if
42 | we think of long/lat, we are covering the earth twice with
43 | such a scheme!
44 |
45 | Args:
46 | xmin: Minimum geospatial extent in the x direction of
47 | tiling scheme.
48 | ymin: Minimum geospatial extent in the y direction of
49 | tiling scheme.
50 | xmax: Maximum geospatial extent in the x direction of
51 | tiling scheme.
52 | ymax: Maximum geospatial extent in the y direction of
53 | tiling scheme.
54 | """
55 | if xmax <= xmin:
56 | raise ValueError("xmax must be greater than xmin")
57 | if ymax <= ymin:
58 | raise ValueError("ymax must be greater than ymin")
59 |
60 | # Sometimes, the public bounds aren't the same as the
61 | # functional ones, as in the case of the dg tiling scheme.
62 | self._bounds = CoordsBbox(float(xmin), float(ymin),
63 | float(xmax), float(ymax))
64 | self.bounds = CoordsBbox(float(xmin), float(ymin),
65 | float(xmax), float(ymax))
66 |
67 | def tile(self, xcoord, ycoord, zoom):
68 | """Returns the (x, y, z) tile at the given zoom level that
69 | contains the input coordinates.
70 |
71 | Args:
72 | xcoord: x direction geospatial coordinate within the tile
73 | we want.
74 | ycoord: y direction geospatial coordinate within the tile
75 | we want.
76 | zoom: zoom level of the tile we want.
77 |
78 | Returns:
79 | A Tile object that covers the given coordinates at the
80 | provided zoom level.
81 | """
82 | return Tile(x=self._x(xcoord, zoom),
83 | y=self._y(ycoord, zoom),
84 | z=zoom)
85 |
86 |
87 | def parent(self, *tile):
88 | """Returns the parent of the (x, y, z) tile.
89 |
90 | Args:
91 | *tile: (x, y, z) tile coordinates or a Tile object we want
92 | the parent of.
93 |
94 | Returns:
95 | A Tile object representing the parent of the input.
96 | """
97 | if len(tile) == 1: # Handle if a Tile object was inputted.
98 | tile = tile[0]
99 | x, y, z = tile
100 |
101 | if x % 2 == 0 and y % 2 == 0: # x and y even
102 | return Tile(x//2, y//2, z - 1)
103 | elif x % 2 == 0: # x even, y odd
104 | return Tile(x//2, (y - 1)//2, z - 1)
105 | elif y % 2 == 0: # x odd, y even
106 | return Tile((x - 1)//2, y//2, z - 1)
107 | else: # x odd, y odd
108 | return Tile((x - 1)//2, (y - 1)//2, z - 1)
109 |
110 |
111 | def children(self, *tile):
112 | """Returns the children of the (x, y, z) tile.
113 |
114 | Args:
115 | *tile: (x, y, z) tile coordinates or a Tile object we want
116 | the children of.
117 |
118 | Yields:
119 | An iterable of Tile objects representing the children of
120 | this tile.
121 | """
122 | if len(tile) == 1: # Handle if a Tile object was inputted.
123 | tile = tile[0]
124 | x, y, z = tile
125 |
126 | return [Tile(2*x, 2*y, z + 1),
127 | Tile(2*x + 1, 2*y, z + 1),
128 | Tile(2*x, 2*y + 1, z + 1),
129 | Tile(2*x + 1, 2*y + 1, z + 1)]
130 |
131 |
132 | def ul(self, *tile):
133 | """Returns the upper left coordinate of the (x, y, z) tile.
134 |
135 | Args:
136 | *tile: (x, y, z) tile coordinates or a Tile object we want
137 | the upper left geospatial coordinates of.
138 |
139 | Returns:
140 | The upper left geospatial coordiantes of the input tile.
141 | """
142 | if len(tile) == 1: # Handle if a Tile object was inputted.
143 | tile = tile[0]
144 | x, y, z = tile
145 |
146 | return Coords(self._xcoord(x, z), self._ycoord(y + 1, z))
147 |
148 |
149 | def br(self, *tile):
150 | """Returns the bottom right coordinate of the (x, y, z) tile.
151 |
152 | Args:
153 | *tile: (x, y, z) tile coordinates or a Tile object we want
154 | the bottom right geospatial coordinates of.
155 |
156 | Returns:
157 | The bottom right geospatial coordiantes of the input tile.
158 | """
159 | if len(tile) == 1: # Handle if a Tile object was inputted.
160 | tile = tile[0]
161 | x, y, z = tile
162 |
163 | return Coords(self._xcoord(x + 1, z),
164 | self._ycoord(y, z))
165 |
166 |
167 | def bbox(self, *tile):
168 | """Returns the bounding box of the (x, y, z) tile.
169 |
170 | Args:
171 | *tile: A tuple of (x, y, z) tile coordinates or a Tile
172 | object we want the bounding box of.
173 |
174 | Returns:
175 | The bounding box of the input tile.
176 | """
177 | if len(tile) == 1: # Handle if a Tile object was inputted.
178 | tile = tile[0]
179 | x, y, z = tile
180 |
181 | west, north = self.ul(tile)
182 | east, south = self.br(tile)
183 | return CoordsBbox(west, south, east, north)
184 |
185 |
186 | def quadkey(self, *tile):
187 | """Returns the quadkey of the (x, y, z) tile.
188 |
189 | Args:
190 | *tile: A tuple of (x, y, z) tile coordinates or a Tile
191 | object we want the quadkey of.
192 |
193 | Returns:
194 | The quadkey of the input tile.
195 | """
196 | if len(tile) == 1: # Handle if a Tile object was inputted.
197 | tile = tile[0]
198 | x, y, z = [int(i) for i in tile]
199 |
200 | quadkey = []
201 | for zoom in range(z, 0, -1):
202 | digit = 0
203 | mask = 1 << (zoom - 1)
204 | if int(x) & mask:
205 | digit += 1
206 | if int(y) & mask:
207 | digit += 2
208 | quadkey.append(digit)
209 | return ''.join(str(d) for d in quadkey)
210 |
211 |
212 | def quadkey_to_tile(self, qk):
213 | """Returns the Tile object represented by the input quadkey.
214 |
215 | Args:
216 | qk: A string representing the quadkey.
217 |
218 | Returns:
219 | The Tile object represented by the input quadkey.
220 | """
221 | if not qk_regex.match(qk):
222 | raise ValueError("Input quadkey is invalid.")
223 |
224 | x = 0
225 | y = 0
226 | for i, digit in enumerate(reversed(qk)):
227 | mask = 1 << i
228 | if digit == '1':
229 | x = x | mask
230 | elif digit == '2':
231 | y = y | mask
232 | elif digit == '3':
233 | x = x | mask
234 | y = y | mask
235 | return Tile(x, y, len(qk))
236 |
237 | def _xcoord(self, x, z):
238 | """Left geospatial coordinate of tile at given column and zoom.
239 |
240 | Args:
241 | x: The tile's column coordinate.
242 | z: The zoom level.
243 |
244 | Returns:
245 | The left geospatial coordinate of this tile.
246 | """
247 | return ((x/(2.**z)*(self._bounds.xmax - self._bounds.xmin)) + self._bounds.xmin)
248 |
249 |
250 | def _ycoord(self, y, z):
251 | """Bottom geospatial coordinate of tile at given row and zoom.
252 |
253 | Args:
254 | y: The tile's row coordinate.
255 | z: The zoom level.
256 |
257 | Returns:
258 | The bottom geospatial coordinate of this tile.
259 | """
260 |
261 | return ((y/(2.**z)*(self._bounds.ymax - self._bounds.ymin)) + self._bounds.ymin)
262 |
263 |
264 | def _x(self, xcoord, zoom):
265 | """Get the x coordinate (column) of this tile at this zoom level.
266 |
267 | Args:
268 | xcoord: x coordinate to covert to tile index.
269 | zoom: zoom level of th tile we want.
270 |
271 | Returns:
272 | The x coordinate (column) of the tile.
273 | """
274 | return int(floor((2.**zoom)*(xcoord - self._bounds.xmin)/(self._bounds.xmax - self._bounds.xmin)))
275 |
276 |
277 | def _y(self, ycoord, zoom):
278 | """Get the y coordinate (row) of this tile at this zoom level.
279 |
280 | Note that this function assumes that the origin is on the
281 | bottom, not the top!
282 |
283 | Args:
284 | ycoord: y coordinate to covert to tile index.
285 | zoom: zoom level of th tile we want.
286 |
287 | Returns:
288 | The y coordinate (row) of the tile.
289 | """
290 | return int(floor((2.**zoom)*(ycoord - self._bounds.ymin)/(self._bounds.ymax - self._bounds.ymin)))
291 |
292 |
293 | class BasicTilingTopLeft(object):
294 | """BasicTilingTopLeft is a class for representing a tiling
295 | scheme defined by some bounding box. The x direction considered
296 | "left" to "right" and the y direction is considered "bottom" to
297 | "top" when we thing of both geospatial and tile-centric
298 | coordinates.
299 |
300 | Note that quadkey indices are defined like this for each
301 | tile's children::
302 |
303 | ---------
304 | | 0 | 1 |
305 | --------
306 | | 2 | 3 |
307 | ---------
308 |
309 | The origin of a tile (row, column) is the top left of the
310 | bounds like Google, Bing, etc do.
311 |
312 | Attributes:
313 | bounds: The bounding box for the projection that the tiling
314 | scheme is defined for.
315 | """
316 | def __init__(self, xmin, ymin, xmax, ymax):
317 | """Constructs an object that generates tile bounds for you.
318 |
319 | BasicTilingTopLeft(-180, -90, 180, 270) would give you a
320 | square tiling scheme defined over those bounds. Note that if
321 | we think of long/lat, we are covering the earth twice with
322 | such a scheme!
323 |
324 | Args:
325 | xmin: Minimum geospatial extent in the x direction of
326 | tiling scheme.
327 | ymin: Minimum geospatial extent in the y direction of
328 | tiling scheme.
329 | xmax: Maximum geospatial extent in the x direction of
330 | tiling scheme.
331 | ymax: Maximum geospatial extent in the y direction of
332 | tiling scheme.
333 | """
334 | if xmax <= xmin:
335 | raise ValueError("xmax must be greater than xmin")
336 | if ymax <= ymin:
337 | raise ValueError("ymax must be greater than ymin")
338 |
339 | # Sometimes, the public bounds aren't the same as the
340 | # functional ones, as in the case of the dg tiling scheme.
341 | self._bounds = CoordsBbox(float(xmin), float(ymin),
342 | float(xmax), float(ymax))
343 | self.bounds = CoordsBbox(float(xmin), float(ymin),
344 | float(xmax), float(ymax))
345 |
346 | def tile(self, xcoord, ycoord, zoom):
347 | """Returns the (x, y, z) tile at the given zoom level that
348 | contains the input coordinates.
349 |
350 | Args:
351 | xcoord: x direction geospatial coordinate within the tile
352 | we want.
353 | ycoord: y direction geospatial coordinate within the tile
354 | we want.
355 | zoom: zoom level of the tile we want.
356 |
357 | Returns:
358 | A Tile object that covers the given coordinates at the
359 | provided zoom level.
360 | """
361 | return Tile(x=self._x(xcoord, zoom),
362 | y=self._y(ycoord, zoom),
363 | z=zoom)
364 |
365 |
366 | def parent(self, *tile):
367 | """Returns the parent of the (x, y, z) tile.
368 |
369 | Args:
370 | *tile: (x, y, z) tile coordinates or a Tile object we want
371 | the parent of.
372 |
373 | Returns:
374 | A Tile object representing the parent of the input.
375 | """
376 | if len(tile) == 1: # Handle if a Tile object was inputted.
377 | tile = tile[0]
378 | x, y, z = tile
379 |
380 | if x % 2 == 0 and y % 2 == 0: # x and y even
381 | return Tile(x//2, y//2, z - 1)
382 | elif x % 2 == 0: # x even, y odd
383 | return Tile(x//2, (y - 1)//2, z - 1)
384 | elif y % 2 == 0: # x odd, y even
385 | return Tile((x - 1)//2, y//2, z - 1)
386 | else: # x odd, y odd
387 | return Tile((x - 1)//2, (y - 1)//2, z - 1)
388 |
389 |
390 | def children(self, *tile):
391 | """Returns the children of the (x, y, z) tile.
392 |
393 | Args:
394 | *tile: (x, y, z) tile coordinates or a Tile object we want
395 | the children of.
396 |
397 | Yields:
398 | An iterable of Tile objects representing the children of
399 | this tile.
400 | """
401 | if len(tile) == 1: # Handle if a Tile object was inputted.
402 | tile = tile[0]
403 | x, y, z = tile
404 |
405 | return [Tile(2*x, 2*y, z + 1),
406 | Tile(2*x + 1, 2*y, z + 1),
407 | Tile(2*x, 2*y + 1, z + 1),
408 | Tile(2*x + 1, 2*y + 1, z + 1)]
409 |
410 |
411 | def ul(self, *tile):
412 | """Returns the upper left coordinate of the (x, y, z) tile.
413 |
414 | Args:
415 | *tile: (x, y, z) tile coordinates or a Tile object we want
416 | the upper left geospatial coordinates of.
417 |
418 | Returns:
419 | The upper left geospatial coordiantes of the input tile.
420 | """
421 | if len(tile) == 1: # Handle if a Tile object was inputted.
422 | tile = tile[0]
423 | x, y, z = tile
424 |
425 | return Coords(self._xcoord(x, z), self._ycoord(y, z))
426 |
427 |
428 | def br(self, *tile):
429 | """Returns the bottom right coordinate of the (x, y, z) tile.
430 |
431 | Args:
432 | *tile: (x, y, z) tile coordinates or a Tile object we want
433 | the bottom right geospatial coordinates of.
434 |
435 | Returns:
436 | The bottom right geospatial coordiantes of the input tile.
437 | """
438 | if len(tile) == 1: # Handle if a Tile object was inputted.
439 | tile = tile[0]
440 | x, y, z = tile
441 |
442 | return Coords(self._xcoord(x + 1, z),
443 | self._ycoord(y + 1, z))
444 |
445 |
446 | def bbox(self, *tile):
447 | """Returns the bounding box of the (x, y, z) tile.
448 |
449 | Args:
450 | *tile: A tuple of (x, y, z) tile coordinates or a Tile
451 | object we want the bounding box of.
452 |
453 | Returns:
454 | The bounding box of the input tile.
455 | """
456 | if len(tile) == 1: # Handle if a Tile object was inputted.
457 | tile = tile[0]
458 | x, y, z = tile
459 |
460 | west, north = self.ul(tile)
461 | east, south = self.br(tile)
462 | return CoordsBbox(west, south, east, north)
463 |
464 |
465 | def quadkey(self, *tile):
466 | """Returns the quadkey of the (x, y, z) tile.
467 |
468 | Args:
469 | *tile: A tuple of (x, y, z) tile coordinates or a Tile
470 | object we want the quadkey of.
471 |
472 | Returns:
473 | The quadkey of the input tile.
474 | """
475 | if len(tile) == 1: # Handle if a Tile object was inputted.
476 | tile = tile[0]
477 | x, y, z = [int(i) for i in tile]
478 |
479 | quadkey = []
480 | for zoom in range(z, 0, -1):
481 | digit = 0
482 | mask = 1 << (zoom - 1)
483 | if int(x) & mask:
484 | digit += 1
485 | if int(y) & mask:
486 | digit += 2
487 | quadkey.append(digit)
488 | return ''.join(str(d) for d in quadkey)
489 |
490 |
491 | def quadkey_to_tile(self, qk):
492 | """Returns the Tile object represented by the input quadkey.
493 |
494 | Args:
495 | qk: A string representing the quadkey.
496 |
497 | Returns:
498 | The Tile object represented by the input quadkey.
499 | """
500 | if not qk_regex.match(qk):
501 | raise ValueError("Input quadkey is invalid.")
502 |
503 | x = 0
504 | y = 0
505 | for i, digit in enumerate(reversed(qk)):
506 | mask = 1 << i
507 | if digit == '1':
508 | x = x | mask
509 | elif digit == '2':
510 | y = y | mask
511 | elif digit == '3':
512 | x = x | mask
513 | y = y | mask
514 | return Tile(x, y, len(qk))
515 |
516 | def _xcoord(self, x, z):
517 | """Left geospatial coordinate of tile at given column and zoom.
518 |
519 | Args:
520 | x: The tile's column coordinate.
521 | z: The zoom level.
522 |
523 | Returns:
524 | The left geospatial coordinate of this tile.
525 | """
526 | return ((x/(2.**z)*(self._bounds.xmax - self._bounds.xmin)) + self._bounds.xmin)
527 |
528 |
529 | def _ycoord(self, y, z):
530 | """Top geospatial coordinate of tile at given row and zoom.
531 |
532 | Args:
533 | y: The tile's row coordinate.
534 | z: The zoom level.
535 |
536 | Returns:
537 | The bottom geospatial coordinate of this tile.
538 | """
539 |
540 | return (self._bounds.ymax - (y/(2.**z)*(self._bounds.ymax - self._bounds.ymin)))
541 |
542 |
543 | def _x(self, xcoord, zoom):
544 | """Get the x coordinate (column) of this tile at this zoom level.
545 |
546 | Args:
547 | xcoord: x coordinate to covert to tile index.
548 | zoom: zoom level of th tile we want.
549 |
550 | Returns:
551 | The x coordinate (column) of the tile.
552 | """
553 | return int(floor((2.**zoom)*(xcoord - self._bounds.xmin)/(self._bounds.xmax - self._bounds.xmin)))
554 |
555 |
556 | def _y(self, ycoord, zoom):
557 | """Get the y coordinate (row) of this tile at this zoom level.
558 |
559 | Note that this function assumes that the origin is on the
560 | top, not the bottom!
561 |
562 | Args:
563 | ycoord: y coordinate to covert to tile index.
564 | zoom: zoom level of th tile we want.
565 |
566 | Returns:
567 | The y coordinate (row) of the tile.
568 | """
569 | return int(floor((2.**zoom)*(self._bounds.ymax - ycoord)/(self._bounds.ymax - self._bounds.ymin)))
570 |
571 |
572 |
573 | class DGTiling(BasicTilingBottomLeft):
574 | """Tiler for the DG tiling scheme.
575 |
576 | The DG tiling scheme is a subdivision of the WGS84 ellipsoid.
577 | Long/lat coordinates are directly mapped to the rectange [-180,
578 | 180] and [-90, 90] in this scheme. In practice, level 0 is a
579 | square whose latitude goes from -90 to 270, so half of this square
580 | is undefined! Because of this, the tiling really starts at level
581 | 0, with the bottom two tiles being valid. The children method
582 | handles this oddity for you.
583 | """
584 | def __init__(self):
585 | """Construct a DG tiling scheme object for you.
586 |
587 | Returns:
588 | Tiling object that functions as the usual DG tiling scheme.
589 | """
590 | super(DGTiling, self).__init__(-180, -90, 180, 270)
591 | self.bounds = CoordsBbox(-180., -90., 180., 90.)
592 |
593 | def children(self, *tile):
594 | """Returns the children of the (x, y, z) tile.
595 |
596 | For DGTiling, note that level 0 only returns two tiles because
597 | the level 0 tile is twice the size of the map!
598 |
599 | Args:
600 | *tile: (x, y, z) tile coordinates or a Tile object we want
601 | the children of.
602 |
603 | Yields:
604 | An iterable of Tile objects representing the children of
605 | this tile.
606 | """
607 | if len(tile) == 1: # Handle if a Tile object was inputted.
608 | tile = tile[0]
609 | x, y, z = tile
610 |
611 | # DGTiling is weird at level zero, so deal with it.
612 | if z == 0:
613 | return [Tile(0, 0, 1), Tile(1, 0, 1)]
614 | return super(DGTiling, self).children(x, y, z)
615 |
616 |
617 | class WebMercatorBL(BasicTilingBottomLeft):
618 | """Tile scheme for Web Mercator with the tile origin in the bottom
619 | left corner.
620 |
621 | Web Mercator (EPSG 3857) is commonly used in online mapping
622 | applications. This scheme has the tile coordinates originating in
623 | the bottom left, which is what the `TMS specification
624 | `_
625 | calls for (as opposed to what Google, Bing, slippy maps, etc do).
626 |
627 | Note that quadkey indices are defined like this for each
628 | tile's children (as the Bing scheme labels them)::
629 |
630 | ---------
631 | | 0 | 1 |
632 | --------
633 | | 2 | 3 |
634 | ---------
635 | """
636 | def __init__(self):
637 | """Construct a Web Mercator tiling scheme object for you where
638 | the origin is in the bottom left corner.
639 |
640 | Returns:
641 | Tiling object for web mercator, with tile origin in the
642 | bottom left corner.
643 | """
644 | super(WebMercatorBL, self).__init__(-20037508.342789244,
645 | -20037508.342789244,
646 | 20037508.342789244,
647 | 20037508.342789244)
648 |
649 | def quadkey(self, *tile):
650 | """Returns the quadkey of the (x, y, z) tile.
651 |
652 | Args:
653 | *tile: A tuple of (x, y, z) tile coordinates or a Tile
654 | object we want the quadkey of.
655 |
656 | Returns:
657 | The quadkey of the input tile.
658 | """
659 | if len(tile) == 1: # Handle if a Tile object was inputted.
660 | tile = tile[0]
661 | x, y, z = [int(i) for i in tile]
662 |
663 | quadkey = []
664 | for zoom in range(z, 0, -1):
665 | digit = 0
666 | mask = 1 << (zoom - 1)
667 | if int(x) & mask:
668 | digit += 1
669 | if not (int(y) & mask):
670 | digit += 2
671 | quadkey.append(digit)
672 | return ''.join(str(d) for d in quadkey)
673 |
674 |
675 | def quadkey_to_tile(self, qk):
676 | """Returns the Tile object represented by the input quadkey.
677 |
678 | Args:
679 | qk: A string representing the quadkey.
680 |
681 | Returns:
682 | The Tile object represented by the input quadkey.
683 | """
684 | if not qk_regex.match(qk):
685 | raise ValueError("Input quadkey is invalid.")
686 |
687 | x = 0
688 | y = 0
689 | for i, digit in enumerate(reversed(qk)):
690 | mask = 1 << i
691 | if digit == '1':
692 | x = x | mask
693 | elif digit == '2':
694 | y = y | mask
695 | elif digit == '3':
696 | x = x | mask
697 | y = y | mask
698 |
699 | return Tile(x, 2**len(qk) - y - 1, len(qk))
700 |
701 |
702 | class WebMercator(BasicTilingTopLeft):
703 | """Tile scheme for Web Mercator with the tile origin in the top
704 | left corner.
705 |
706 | Web Mercator (EPSG 3857) is commonly used in online mapping
707 | applications. This scheme has the tile coordinates originating in
708 | the top left, which is what Google, Bing, and most others do for
709 | Web Mercator.
710 |
711 | Note that quadkey indices are defined like this for each
712 | tile's children (as the Bing scheme labels them)::
713 |
714 | ---------
715 | | 0 | 1 |
716 | --------
717 | | 2 | 3 |
718 | ---------
719 | """
720 | def __init__(self):
721 | """Construct a Web Mercator tiling scheme object for you where
722 | the origin is in the top left corner.
723 |
724 | Returns:
725 | Tiling object for web mercator, with tile origin in the
726 | top left corner.
727 | """
728 | super(WebMercator, self).__init__(-20037508.342789244,
729 | -20037508.342789244,
730 | 20037508.342789244,
731 | 20037508.342789244)
732 |
733 |
734 | class UTMTiling(BasicTilingTopLeft):
735 | """A tiling of a UTM projection.
736 |
737 | We are building a hierarchical grid valid for a UTM projection.
738 | Recall that such a projection is roughtly 1,000,000 meters across
739 | and 20,000,000 meters tall (inclusive of both north and south
740 | zones) , so given a desired tile size, we build out our grid to
741 | cover these bounds.
742 |
743 | Attributes:
744 | zoom: The zoom level of the hierarchical grid that corresponds
745 | to the user provided tile_size.
746 |
747 | """
748 | def __init__(self, tile_size):
749 | """Constructs a UTMTiling object.
750 |
751 | Args:
752 | tile_size: The size of the tile grid (meters) you want to
753 | use as a covering of the UTM bounds.
754 | E.G. 100_000 would correspond to the 100km MGRS
755 | tiling for this zone.
756 |
757 | Returns:
758 | A tiling object valid for a UTM projection.
759 | """
760 | if tile_size <= 0.0:
761 | raise ValueError('tile_size must be positive')
762 | self.tile_size = tile_size
763 |
764 | # For this tile size, figure out the size of the bounding box
765 | # that covers the UTM projection bounds. Remember that a UTM
766 | # zone is 10_000_000 meters tall as measured from the origin
767 | # so we need to have a map size that exceeds that dimension.
768 | zoom = ceil(log2(10_000_000.0/self.tile_size))
769 | map_size = self.tile_size * 2**zoom
770 | self.zoom = zoom + 1
771 |
772 | super().__init__(-map_size + 500_000.0, -map_size,
773 | map_size + 500_000.0, map_size)
774 |
775 |
776 | class UTM5kmTiling(UTMTiling):
777 | """A 5km tiling of a UTM zone.
778 |
779 | Note that the zoom attribute tells you what zoom level corresponds
780 | to the 5km tiles.
781 | """
782 | def __init__(self):
783 | super().__init__(5_000)
784 |
785 |
786 | class UTM10kmTiling(UTMTiling):
787 | """A 10km tiling of a UTM zone.
788 |
789 | Note that the zoom attribute tells you what zoom level corresponds
790 | to the 10km tiles.
791 | """
792 | def __init__(self):
793 | super().__init__(10_000)
794 |
795 |
796 | class UTM100kmTiling(UTMTiling):
797 | """A 100km tiling of a UTM zone.
798 |
799 | Note that the zoom attribute tells you what zoom level corresponds
800 | to the 100km tiles.
801 | """
802 | def __init__(self):
803 | super().__init__(100_000)
804 |
--------------------------------------------------------------------------------