├── .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 | --------------------------------------------------------------------------------