├── .coveragerc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Documentation ├── release │ ├── Instructions_For_gdal2tiles.pdf │ └── Instructions_For_tiles2gpkg.pdf └── src │ ├── Instructions_For_gdal2tiles.tex │ └── Instructions_For_tiles2gpkg.tex ├── LICENSE.txt ├── Packaging └── tiles2gpkg_parallel.py ├── README.md ├── Testing ├── rgb_tiles │ ├── geodetic │ │ ├── 1 │ │ │ └── 0 │ │ │ │ └── 0.png │ │ └── 2 │ │ │ ├── 0 │ │ │ ├── 0.png │ │ │ └── 1.png │ │ │ └── 1 │ │ │ ├── 0.png │ │ │ └── 1.png │ └── mercator │ │ └── 1 │ │ ├── 0 │ │ ├── 0.png │ │ └── 1.png │ │ └── 1 │ │ ├── 0.png │ │ └── 1.png ├── test_gdal2tiles.py ├── test_gdal2tiles_parallel.py └── test_tiles2gpkg.py ├── Tiling └── gdal2tiles_parallel.py ├── Tools ├── generate_wms_aligned.py ├── generate_wms_aligned_relative.py └── generate_wmts_urls.py └── dependencies.txt /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | if __name__ == '__main__': 4 | def sqlite_worker 5 | def main 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Answer the following when submitting a ticket: 2 | 3 | * What is your platform (Windows/Linux) 4 | * What is your architecture? (32 v. 64) 5 | * What version of GDAL are you using? 6 | * What are the steps to reproduce your issue? 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | *.swp 3 | 4 | ### Start of TeX section 5 | ## Core latex/pdflatex auxiliary files: 6 | *.aux 7 | *.lof 8 | *.log 9 | *.lot 10 | *.fls 11 | *.out 12 | *.toc 13 | 14 | ## Intermediate documents: 15 | *.dvi 16 | # these rules might exclude image files for figures etc. 17 | # *.ps 18 | # *.eps 19 | #*.pdf 20 | 21 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 22 | *.bbl 23 | *.bcf 24 | *.blg 25 | *-blx.aux 26 | *-blx.bib 27 | *.brf 28 | *.run.xml 29 | 30 | ## Build tool auxiliary files: 31 | *.fdb_latexmk 32 | *.synctex.gz 33 | *.synctex.gz(busy) 34 | *.pdfsync 35 | 36 | ## Auxiliary and intermediate files from other packages: 37 | 38 | # algorithms 39 | *.alg 40 | *.loa 41 | 42 | # amsthm 43 | *.thm 44 | 45 | # beamer 46 | *.nav 47 | *.snm 48 | *.vrb 49 | 50 | #(e)ledmac/(e)ledpar 51 | *.end 52 | *.[1-9] 53 | *.[1-9][0-9] 54 | *.[1-9][0-9][0-9] 55 | *.[1-9]R 56 | *.[1-9][0-9]R 57 | *.[1-9][0-9][0-9]R 58 | 59 | # glossaries 60 | *.acn 61 | *.acr 62 | *.glg 63 | *.glo 64 | *.gls 65 | 66 | # hyperref 67 | *.brf 68 | 69 | # listings 70 | *.lol 71 | 72 | # makeidx 73 | *.idx 74 | *.ilg 75 | *.ind 76 | *.ist 77 | 78 | # minitoc 79 | *.maf 80 | *.mtc 81 | *.mtc0 82 | 83 | # minted 84 | *.pyg 85 | 86 | # morewrites 87 | *.mw 88 | 89 | # nomencl 90 | *.nlo 91 | 92 | # sagetex 93 | *.sagetex.sage 94 | *.sagetex.py 95 | *.sagetex.scmd 96 | 97 | # sympy 98 | *.sout 99 | *.sympy 100 | sympy-plots-for-*.tex/ 101 | 102 | # todonotes 103 | *.tdo 104 | 105 | # xindy 106 | *.xdy 107 | ### End of TeX section 108 | 109 | *.vrt 110 | # Vim swap files 111 | *.swp 112 | 113 | ### Start of TeX section 114 | ## Core latex/pdflatex auxiliary files: 115 | *.aux 116 | *.lof 117 | *.log 118 | *.lot 119 | *.fls 120 | *.out 121 | *.toc 122 | 123 | ## Intermediate documents: 124 | *.dvi 125 | # these rules might exclude image files for figures etc. 126 | # *.ps 127 | # *.eps 128 | *.pdf 129 | 130 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 131 | *.bbl 132 | *.bcf 133 | *.blg 134 | *-blx.aux 135 | *-blx.bib 136 | *.brf 137 | *.run.xml 138 | 139 | ## Build tool auxiliary files: 140 | *.fdb_latexmk 141 | *.synctex.gz 142 | *.synctex.gz(busy) 143 | *.pdfsync 144 | 145 | ## Auxiliary and intermediate files from other packages: 146 | 147 | # algorithms 148 | *.alg 149 | *.loa 150 | 151 | # amsthm 152 | *.thm 153 | 154 | # beamer 155 | *.nav 156 | *.snm 157 | *.vrb 158 | 159 | #(e)ledmac/(e)ledpar 160 | *.end 161 | *.[1-9] 162 | *.[1-9][0-9] 163 | *.[1-9][0-9][0-9] 164 | *.[1-9]R 165 | *.[1-9][0-9]R 166 | *.[1-9][0-9][0-9]R 167 | 168 | # glossaries 169 | *.acn 170 | *.acr 171 | *.glg 172 | *.glo 173 | *.gls 174 | 175 | # hyperref 176 | *.brf 177 | 178 | # listings 179 | *.lol 180 | 181 | # makeidx 182 | *.idx 183 | *.ilg 184 | *.ind 185 | *.ist 186 | 187 | # minitoc 188 | *.maf 189 | *.mtc 190 | *.mtc0 191 | 192 | # minted 193 | *.pyg 194 | 195 | # morewrites 196 | *.mw 197 | 198 | # nomencl 199 | *.nlo 200 | 201 | # sagetex 202 | *.sagetex.sage 203 | *.sagetex.py 204 | *.sagetex.scmd 205 | 206 | # sympy 207 | *.sout 208 | *.sympy 209 | sympy-plots-for-*.tex/ 210 | 211 | # todonotes 212 | *.tdo 213 | 214 | # xindy 215 | *.xdy 216 | ### End of TeX section 217 | 218 | ### Start of Haskell section 219 | dist 220 | cabal-dev 221 | *.o 222 | *.hi 223 | *.chi 224 | *.chs.h 225 | .virtualenv 226 | .hsenv 227 | .cabal-sandbox/ 228 | cabal.sandbox.config 229 | cabal.config 230 | ### End of Haskell section 231 | 232 | ### Start of Android section 233 | # Built application files 234 | *.apk 235 | *.ap_ 236 | 237 | # Files for the Dalvik VM 238 | *.dex 239 | 240 | # Java class files 241 | *.class 242 | 243 | # Generated files 244 | bin/ 245 | gen/ 246 | 247 | # Gradle files 248 | .gradle/ 249 | build/ 250 | 251 | # Local configuration file (sdk path, etc) 252 | local.properties 253 | 254 | # Proguard folder generated by Eclipse 255 | proguard/ 256 | 257 | # Log Files 258 | *.log 259 | ### End of Android section 260 | 261 | ### Start of Python section 262 | # Byte-compiled / optimized / DLL files 263 | __pycache__/ 264 | *.py[cod] 265 | 266 | # C extensions 267 | *.so 268 | 269 | # Distribution / packaging 270 | .Python 271 | env/ 272 | build/ 273 | develop-eggs/ 274 | dist/ 275 | eggs/ 276 | lib/ 277 | lib64/ 278 | parts/ 279 | sdist/ 280 | var/ 281 | *.egg-info/ 282 | .installed.cfg 283 | *.egg 284 | 285 | # PyInstaller 286 | # Usually these files are written by a python script from a template 287 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 288 | *.manifest 289 | *.spec 290 | 291 | # Installer logs 292 | pip-log.txt 293 | pip-delete-this-directory.txt 294 | 295 | # Unit test / coverage reports 296 | htmlcov/ 297 | .tox/ 298 | .coverage 299 | .cache 300 | nosetests.xml 301 | coverage.xml 302 | 303 | # Translations 304 | *.mo 305 | *.pot 306 | 307 | # Django stuff: 308 | *.log 309 | 310 | # Sphinx documentation 311 | docs/_build/ 312 | 313 | # PyBuilder 314 | target/ 315 | ### End of python section 316 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | install: 6 | - pip install -r dependencies.txt 7 | - pip install coveralls 8 | script: 9 | - py.test Testing/test_tiles2gpkg.py --doctest-modules -v --cov Packaging --cov-report term-missing 10 | after_success: 11 | - coveralls 12 | -------------------------------------------------------------------------------- /Documentation/release/Instructions_For_gdal2tiles.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Documentation/release/Instructions_For_gdal2tiles.pdf -------------------------------------------------------------------------------- /Documentation/release/Instructions_For_tiles2gpkg.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Documentation/release/Instructions_For_tiles2gpkg.pdf -------------------------------------------------------------------------------- /Documentation/src/Instructions_For_gdal2tiles.tex: -------------------------------------------------------------------------------- 1 | % Copyright (C) 2014 Reinventing Geospatial, Inc 2 | % 3 | % This program is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % This program is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with this program. If not, see , 15 | % or write to the Free Software Foundation, Inc., 59 Temple Place - 16 | % Suite 330, Boston, MA 02111-1307, USA. 17 | 18 | \documentclass{article} 19 | \usepackage{multirow} 20 | \usepackage[margin=1in]{geometry} 21 | \usepackage[hidelinks]{hyperref} 22 | \usepackage{listings} 23 | \usepackage{courier} 24 | \usepackage{fancyhdr} 25 | 26 | \lstset{basicstyle=\ttfamily} 27 | 28 | \pagestyle{fancy} 29 | \rfoot{Version Number: 1.0} 30 | 31 | \title{Instructions For gdal2tiles\_parallel.py} 32 | \author{Steven D. Lander, Reinventing Geospatial} 33 | 34 | \begin{document} 35 | \maketitle 36 | 37 | \section{Purpose} 38 | This document covers installation of dependencies, in-depth explanations of 39 | command-line arguments, and usage examples for gdal2tiles\_parallel.py. 40 | 41 | The file gdal2tiles\_parallel.py is a Python script that converts 42 | GDAL-supported raster imagery files into a folder of tiles in TMS format 43 | (z/x/y). This version improves upon the standard gdal2tiles.py file included 44 | with GDAL by adding multiprocessing improvements to obtain even better tile 45 | generation performance. 46 | 47 | \section{Installing Dependencies} 48 | Gdal2tiles\_parallel.py was written for Python 2.7.x and has not been tested on 49 | any other version. It will run on either 32 or 64 bit systems with no issues. 50 | 51 | The script relies on Python 2.7.x, GDAL core, and the GDAL Python bindings. 52 | Some imagery formats such as MrSID will require additional driver installers to 53 | work correctly. 54 | \subsection{Windows} 55 | In order to run gdal2tiles\_parallel.py on a Windows environment, install 56 | the following packages. For convenience, they have been included in all 57 | PythonGeopackage releases and are viewable in source under Dependencies. 58 | \begin{itemize} 59 | \item 60 | The latest version of Python 2.7 for either x86 (32 bit) or x86\_64 61 | (64 bit): 62 | \url{https://www.python.org/downloads/windows/}. 63 | \item 64 | The lastest stable release of the Geospatial Data Abstraction Library 65 | (GDAL) in either x86 (32 bit) or x86\_64 (64 bit): 66 | \url{http://www.gisinternals.com/sdk} 67 | \end{itemize} 68 | \subsection{Linux} 69 | In order to run gdal2tiles\_parallel.py on a Linux environment, instructions 70 | will differ slightly by distribution. Most newer Linux distributions have 71 | Python 2.7 as either an option or the default python version in their package 72 | repository, but others such as CentOS only have older versions. In that case, 73 | the user will need to find out how to get Python 2.7 from a reputable source or 74 | compile it themselves. The following packages are needed: 75 | \begin{itemize} 76 | \item 77 | The latest version of Python 2.7 for your system. For Debian-based 78 | distributions such as Ubuntu and RedHat type the following into a 79 | command line: 80 | 81 | \lstinline|sudo apt-get install python2.7 python2.7-dev python-pip base-devel| 82 | 83 | This will install the Python 2.7 environment plus PIP, a Python 84 | module manager. 85 | \item 86 | The latest GDAL binaries and python bindings for your system. For 87 | Debian-based distributions such as Ubuntu and RedHat, type the 88 | following into a command line: 89 | 90 | \lstinline|sudo apt-get install gdal-bin python-gdal| 91 | \end{itemize} 92 | \section{Usage} 93 | \subsection{Command Line Arguments} 94 | Gdal2tiles\_parallel.py supports additional functionality via command line 95 | arguments provided to the script at the time it is executed. Following is a 96 | outline of the important flags: 97 | \\\\ 98 | \begin{tabular}{ | c | p{14cm} | } 99 | \hline 100 | -h, --help & Print the listing of commands available for the script.\\ 101 | \hline 102 | -p, --profile & Specify the tiling profile you would like these tiles to be 103 | created in. Valid options are mercator or geodetic.\\ 104 | \hline 105 | -e, --resume & Instruct the script to not overwrite tiles that have already 106 | been created. This is a {\bf mandatory} flag when using default 107 | multiprocessing.\\ 108 | \hline 109 | -z, --zoom & The zoom levels to create. Allows the tiler to make tiles 110 | past the default zoom level that GDAL detects. (Format: '2-5' or '10') 111 | \\ 112 | \hline 113 | -a, --srcnodata & Specify the RGB value that gdal2tiles should convert to 114 | transparency. Typical value should be '0,0,0'. 115 | \\ 116 | \hline 117 | \end{tabular} 118 | 119 | \subsection{Examples} 120 | \begin{itemize} 121 | \item 122 | Create a folder of tiles in the mercator projection based on a GeoTiff 123 | image named WhiteHorse.tif and name the folder 'WhiteHorse\_tiles':\\ 124 | \\ 125 | \lstinline|python gdal2tiles_parallel.py|\\ 126 | \lstinline| -p mercator -e|\\ 127 | \lstinline| /data/raw/WhiteHorse.tif /data/tiles/mercator/WhiteHorse_tiles|\\ 128 | \item 129 | Create a folder of tiles for zoom level 15 in the mercator projection 130 | based on a GeoTiff image named WhiteHorse.tif and name the folder 131 | 'WhiteHorse\_tiles':\\ 132 | \\ 133 | \lstinline|python gdal2tiles_parallel.py|\\ 134 | \lstinline| -p mercator -e -z 15|\\ 135 | \lstinline| /data/raw/WhiteHorse.tif /data/tiles/mercator/WhiteHorse_tiles|\\ 136 | \item 137 | Create a folder of tiles in the geodetic projection based on a MrSID 138 | image named FortBelvoir\_201307\_A6.sid and name the folder 139 | belvoir\_tiles. Also, assigns the NODATA transparency to the RGB color 140 | value of 0,0,0:\\ 141 | \\ 142 | \lstinline|python gdal2tiles_parallel.py|\\ 143 | \lstinline| -p geodetic -e -a 0,0,0|\\ 144 | \lstinline| /data/raw/FortBelvoir_201307_A6.sid /data/tiles/belvoir_tiles| 145 | \item 146 | Create a folder of tiles in the geodetic projection based on a MrSID 147 | image named FortBelvoir\_201307\_A6.sid and name the folder 148 | belvoir\_tiles. Also, assigns the NODATA transparency to the RGB color 149 | value of 255,0,0:\\ 150 | \\ 151 | \lstinline|python gdal2tiles_parallel.py|\\ 152 | \lstinline| -p geodetic -e -a 255,0,0|\\ 153 | \lstinline| /data/raw/FortBelvoir_201307_A6.sid /data/tiles/belvoir_tiles| 154 | \item 155 | Create a folder of tiles for zoom levels 10 through 13 in the geodetic 156 | projection based on a MrSID image named FortBelvoir\_201307\_A6.sid 157 | and name the folder belvoir\_tiles. Also, assigns the NODATA 158 | transparency to the RGB color value of 0,0,0:\\ 159 | \\ 160 | \lstinline|python gdal2tiles_parallel.py|\\ 161 | \lstinline| -p geodetic -e -a 0,0,0|\\ 162 | \lstinline| -z 10-13|\\ 163 | \lstinline| /data/raw/FortBelvoir_201307_A6.sid /data/tiles/belvoir_tiles| 164 | \end{itemize} 165 | 166 | \section{Caveats \& Known Issues} 167 | \begin{itemize} 168 | \item 169 | Gdal2tiles\_parallel.py currently only outputs tiles as full-color 170 | PNGs. This is so that transparency can be preserved. When used in 171 | conjunction with tiles2gpkg\_parallel.py a user can make a geopackage 172 | with either PNGs, JPEGs, or a mixture of both. 173 | \item 174 | Since this script utilizes multiprocessing, it does not exit cleanly 175 | when interrupted. For example, if Ctrl+D (KeyboardInterrupt) while the 176 | script is executing, it will not completely stop but instead produce 177 | profuse error messages. To kill the process, close the terminal 178 | windows on Windows or, on Linux, send the process to the background 179 | (Ctrl+Z) then kill the job with \lstinline|kill -9 $(jobs -p)|. 180 | \item 181 | Gdal2tiles\_parallel.py only creates tiles with a lower-left tile 182 | origin. 183 | \end{itemize} 184 | 185 | \end{document} 186 | -------------------------------------------------------------------------------- /Documentation/src/Instructions_For_tiles2gpkg.tex: -------------------------------------------------------------------------------- 1 | % Copyright (C) 2014 Reinventing Geospatial, Inc 2 | % 3 | % This program is free software: you can redistribute it and/or modify 4 | % it under the terms of the GNU General Public License as published by 5 | % the Free Software Foundation, either version 3 of the License, or 6 | % (at your option) any later version. 7 | % 8 | % This program is distributed in the hope that it will be useful, 9 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | % GNU General Public License for more details. 12 | % 13 | % You should have received a copy of the GNU General Public License 14 | % along with this program. If not, see , 15 | % or write to the Free Software Foundation, Inc., 59 Temple Place - 16 | % Suite 330, Boston, MA 02111-1307, USA. 17 | 18 | \documentclass{article} 19 | 20 | \usepackage{multirow} 21 | \usepackage[margin=1in]{geometry} 22 | \usepackage[hidelinks]{hyperref} 23 | \usepackage{listings} 24 | \usepackage{courier} 25 | \usepackage{fancyhdr} 26 | 27 | \lstset{basicstyle=\ttfamily} 28 | 29 | \pagestyle{fancy} 30 | \rfoot{Version Number: 1.0} 31 | 32 | \title{Instructions For tiles2gpkg\_parallel.py} 33 | \author{Steven D. Lander, Reinventing Geospatial} 34 | 35 | \begin{document} 36 | \maketitle 37 | 38 | \section{Purpose} 39 | This document covers installation of dependencies, in-depth explanations of 40 | command-line arguments, and usage examples for tiles2gpkg\_parallel.py. 41 | 42 | The file tiles2gpkg\_parallel.py is a Python script that accepts a folder of 43 | tiles in TMS format (z/x/y) and outputs an Open Geospatial Consortium (OGC)- 44 | compliant Geopackage. The script leverages as much hardware capability as 45 | possible in order package this data. 46 | 47 | For more information about the OGC Specification for Geopackage, please visit 48 | the Open Geospatial Consortium Website: 49 | \url{http://www.opengeospatial.org/standards/geopackage}. 50 | 51 | \section{Installing Dependencies} 52 | Tiles2gpkg\_parallel.py was written for Python 2.7.x and has not been tested on 53 | any other version. It will run on either 32 or 64 bit systems with no issues. 54 | 55 | The script relies on the Python Imaging Library (PIL) in order to allow 56 | fine-grain control of the MIME type (PNG/JPEG) of each individual tile that 57 | resides within the output Geopackage. Without PIL, the script will simply 58 | detect the image type of the input tile and maintain that MIME type when it 59 | is stored to the Geopackage. 60 | \subsection{Windows} 61 | In order to run tiles2gpkg\_parallel.py on a Windows environment, install 62 | the following: 63 | \begin{itemize} 64 | \item 65 | The latest version of Python 2.7 for either x86 (32 bit) or x86\_64 66 | (64 bit): 67 | \url{https://www.python.org/downloads/windows/}. 68 | \item 69 | The Python Imaging Library pre-compilied binary installer for your 70 | correct Python version (2.7) AND architecture (32/64 bit): 71 | \url{http://www.lfd.uci.edu/~gohlke/pythonlibs/#pillow}. 72 | 73 | \bf{NOTE: PIL has been deprecated in favor of Pillow, a continuation of 74 | the project. They are 100\% interchangable but prefer Pillow 75 | whenever you can.} 76 | \end{itemize} 77 | \subsection{Linux} 78 | In order to run tiles2gpkg\_parallel.py on a Linux environment, instructions 79 | will differ slightly by distribution. Most newer Linux distributions have 80 | Python 2.7 as an option in their default package repository, but others such as 81 | CentOS only have older versions. In that case, the user will need to find out 82 | how to get Python 2.7 from a reputable source or compile it themselves. Install 83 | the following: 84 | \begin{itemize} 85 | \item 86 | The latest version of Python 2.7 for your system. For Debian-based 87 | distributions such as Ubuntu and RedHat type the following into a 88 | command line: 89 | 90 | \lstinline|sudo apt-get install python2.7 python2.7-dev python-pip base-devel| 91 | 92 | This will install the Python 2.7 environment plus PIP, a Python 93 | module manager. PIP is necessary for the next step. 94 | \item 95 | The Python Imaging Library python module. On Linux, PIP will 96 | download the source code for PIL or Pillow and then install it on 97 | your system. For Debian-based distributions type the following 98 | into a command line: 99 | 100 | \lstinline|sudo pip install Pillow| 101 | 102 | This will install Pillow, the successor to the deprecated version 103 | of the Python Imaging Library. PIP will attempt to compile the 104 | Pillow source code for your system using the python2.7-dev headers 105 | and the compilation tools contained within the base-devel package. 106 | \end{itemize} 107 | \section{Usage} 108 | \subsection{Command Line Arguments} 109 | Tiles2gpkg\_parallel.py supports additional functionality via command line 110 | arguments provided to the script at the time it is executed. Following is a 111 | outline of each one and their purpose: 112 | \\\\ 113 | \begin{tabular}{ | c | p{14cm} | } 114 | \hline 115 | -h & Print the listing of commands available for the script.\\ 116 | \hline 117 | -tileorigin & Specify the origin of the tiles contained within the input 118 | data folder. Gdal2tiles.py creates tiles referenced by the bottom-left 119 | corner which follows TMS convention. Other tile providers can create tiles 120 | with a tile origin of upper-left. Valid options are ul, ll, nw, or sw. 121 | The default option is ll for lower-left.\\ 122 | \hline 123 | -srs & Specify the spatial reference system of the tiles contained within 124 | the input data folder. This could also be called the tile grid profile. 125 | Valid options are 3857 (mercator), 4326 (geodetic), and 3395 (global 126 | mercator). The default value for this field is 3857.\\ 127 | \hline 128 | -imagery & Convert the MIME type of the tiles on-disk to a new type when 129 | they are stored in the Geopackage. Valid options are source, mixed, png, 130 | and jpeg. Specifying mixed mode will convert all tile images in the source 131 | folder that do not have transparency to JPEG with compression enabled for 132 | space savings. Specifying source mode will preserve the file type of the 133 | input tile images. The default value for this field is source.\\ 134 | \hline 135 | -q & When the -imagery flag is set to either mixed or jpeg, this flag 136 | specifies the jpeg quality value. Acceptable values are from 1-100 137 | inclusive. Lower numbers result in smaller size images but greatly reduced 138 | image quality.\\ 139 | \hline 140 | -T & By default, tiles2gpkg\_parallel.py takes advantage of all the 141 | processors available to the hardware that it is executed on. This can mean 142 | that other computing tasks on the machine may suffer depending on the size 143 | of the packaging job. The -T flag disables this behavoir and only uses a 144 | single-core process to execute the job.\\ 145 | \hline 146 | \end{tabular} 147 | 148 | \subsection{Examples} 149 | \begin{itemize} 150 | \item 151 | Create a geopackage from a folder of tiles named WhiteHorse in the 152 | geodetic tile profile, and name the new Geopackage whitehorse.gpkg:\\ 153 | \\ 154 | \lstinline|python tiles2gpkg_parallel.py|\\ 155 | \lstinline| -srs 4326|\\ 156 | \lstinline| /data/tiles/geodetic/WhiteHorse /data/geopackage/whitehorse.gpkg| 157 | \item 158 | Create a geopackage from a folder of tiles named belvoir in the 159 | mercator profile and changing the tile images to a mix of PNG and JPEG 160 | images:\\ 161 | \\ 162 | \lstinline|python tiles2gpkg_parallel.py|\\ 163 | \lstinline| -srs 3857 -imagery mixed|\\ 164 | \lstinline| /data/tiles/mercator/belvoir /data/geopackage/belvoir-3857.gpkg| 165 | \item 166 | Create a geopackage from a folder of tiles named gnc in the world 167 | mercator profile with a tile origin of upper-left and also converting 168 | the tile images to JPEG with 50\% quality:\\ 169 | \\ 170 | \lstinline|python tiles2gpkg_parallel.py|\\ 171 | \lstinline| -srs 3395 -tileorigin ul -imagery jpeg -q 50|\\ 172 | \lstinline| /data/tiles/world-mercator/gnc /data/geopackage/gnc-wm.gpkg| 173 | \end{itemize} 174 | 175 | \section{Caveats \& Known Issues} 176 | \begin{itemize} 177 | \item 178 | Currently it is possible to provide tiles2gpkg\_parallel.py with a 179 | folder of tiles with different types of data in it. For example, an 180 | input folder of tiles could have tiles for the world at very high zoom 181 | level (0-3) but also tiles at a very low zoom level (18-19). The 182 | script would only describe these tiles for the world and thus confuse a 183 | Geopackage viewer about where the rest of the tiles in the package are 184 | located. 185 | \item 186 | Currently the script does not support making a Geopackage with multiple 187 | raster tiles tables. 188 | \item 189 | The script only creates raster tiles at the moment and does not support 190 | vector features nor vector tiles. Plans for inclusion of vector tiles 191 | in the future are pending. 192 | \end{itemize} 193 | 194 | \end{document} 195 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Packaging/tiles2gpkg_parallel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | """ 3 | Copyright (C) 2014 Reinventing Geospatial, Inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see , 17 | or write to the Free Software Foundation, Inc., 59 Temple Place - 18 | Suite 330, Boston, MA 02111-1307, USA. 19 | 20 | Author: Steven D. Lander, Reinventing Geospatial Inc (RGi) 21 | Date: 2013-07-12 22 | Requires: sqlite3, argparse 23 | Optional: Python Imaging Library (PIL or Pillow) 24 | Description: Converts a TMS folder into a geopackage with 25 | PNGs for images with transparency and JPEGs for those 26 | without. 27 | Credits: 28 | MapProxy imaging functions: http://mapproxy.org 29 | gdal2mb on github: https://github.com/developmentseed/gdal2mb 30 | 31 | Version: 32 | """ 33 | 34 | from glob import glob 35 | try: 36 | from cStringIO import StringIO as ioBuffer 37 | except ImportError: 38 | from io import BytesIO as ioBuffer 39 | from time import sleep 40 | from uuid import uuid4 41 | from sys import stdout 42 | from sys import version_info 43 | if version_info[0] == 3: 44 | xrange = range 45 | from operator import attrgetter 46 | from sqlite3 import connect, Error 47 | from argparse import ArgumentParser 48 | from sqlite3 import Binary as sbinary 49 | from os import walk, remove 50 | from os.path import split, join, exists 51 | from multiprocessing import cpu_count, Pool 52 | from math import pi, sin, log, tan, atan, sinh, degrees 53 | try: 54 | from PIL.Image import open as IOPEN 55 | except ImportError: 56 | IOPEN = None 57 | 58 | # JPEGs @ 75% provide good quality images with low footprint, use as a default 59 | # PNGs should be used sparingly (mixed mode) due to their high disk usage RGBA 60 | # Options are mixed, jpeg, and png 61 | IMAGE_TYPES = '.png', '.jpeg', '.jpg' 62 | 63 | 64 | class Mercator(object): 65 | """ 66 | Mercator projection class that holds specific calculations and formulas 67 | for EPSG3857. 68 | """ 69 | 70 | def __init__(self, tile_size=256): 71 | """ 72 | Constructor 73 | """ 74 | self.tile_size = tile_size 75 | self.radius = 6378137 76 | self.origin_shift = pi * self.radius 77 | self.initial_resolution = 2 * self.origin_shift / self.tile_size 78 | 79 | @staticmethod 80 | def invert_y(z, y): 81 | """ 82 | Inverts the Y tile value. 83 | 84 | Inputs: 85 | z -- the zoom level associated with the tile 86 | y -- the Y tile number 87 | 88 | Returns: 89 | The flipped tile value 90 | """ 91 | return (1 << z) - y - 1 92 | 93 | @staticmethod 94 | def tile_to_lat_lon(z, x, y): 95 | """ 96 | Returns the lat/lon coordinates of the bottom-left corner of the input 97 | tile. 98 | 99 | Inputs: 100 | z -- zoom level value for input tile 101 | x -- tile column (longitude) value for input tile 102 | y -- tile row (latitude) value for input tile 103 | """ 104 | n = 2.0**z 105 | lon = x / n * 360.0 - 180.0 106 | lat_rad = atan(sinh(pi * (2 * y / n - 1))) 107 | #lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * y / n))) 108 | lat = degrees(lat_rad) 109 | return lat, lon 110 | 111 | def tile_to_meters(self, z, x, y): 112 | """ 113 | Returns the meter coordinates of the bottom-left corner of the input 114 | tile. 115 | 116 | Inputs: 117 | z -- zoom level value for input tile 118 | x -- tile column (longitude) value for input tile 119 | y -- tile row (latitude) value for input tile 120 | """ 121 | # Mercator Upper left, add 1 to both x and y to get Lower right 122 | lat, lon = self.tile_to_lat_lon(z, x, y) 123 | meters_x = lon * self.origin_shift / 180.0 124 | meters_y = log(tan((90 + lat) * pi / 360.0)) / \ 125 | (pi / 180.0) 126 | meters_y = meters_y * self.origin_shift / 180.0 127 | return meters_x, meters_y 128 | 129 | @staticmethod 130 | def pixel_size(z): 131 | """ 132 | Returns the pixel resolution of the input zoom level. 133 | 134 | Inputs: 135 | z -- zoom level value for the input tile 136 | """ 137 | return 156543.033928041 / 2**z 138 | 139 | def get_coord(self, z, x, y): 140 | """ 141 | Returns the coordinates (in meters) of the bottom-left corner of the 142 | input tile. 143 | 144 | Inputs: 145 | z -- zoom level value for input tile 146 | x -- tile column (longitude) value for input tile 147 | y -- tile row (latitude) value for input tile 148 | """ 149 | return self.tile_to_meters(z, x, y) 150 | 151 | @staticmethod 152 | def truncate(coord): 153 | """ 154 | Formats a coordinate to within an acceptable degree of accuracy (2 155 | decimal places for mercator). 156 | """ 157 | return '%.2f' % (int(coord * 100) / float(100)) 158 | 159 | 160 | class Geodetic(object): 161 | """ 162 | Geodetic projection class that holds specific calculations and formulas for 163 | EPSG4326. 164 | """ 165 | 166 | def __init__(self, tile_size=256): 167 | """ 168 | Constructor 169 | """ 170 | self.tile_size = tile_size 171 | self.resolution_factor = 360.0 / self.tile_size 172 | 173 | def pixel_size(self, zoom): 174 | """ 175 | Return the size of a pixel in lat/long at the given zoom level 176 | 177 | z -- zoom level of the tile 178 | """ 179 | return self.resolution_factor / 2**zoom 180 | 181 | def get_coord(self, z, x, y): 182 | """ 183 | Return the coordinates (in lat/long) of the bottom left corner of 184 | the tile 185 | 186 | z -- zoom level for input tile 187 | x -- tile column 188 | y -- tile row 189 | """ 190 | res = self.resolution_factor / 2**z 191 | return x * self.tile_size * res - 180, y * self.tile_size * res - 90 192 | 193 | @staticmethod 194 | def invert_y(z, y): 195 | """ 196 | Return the inverted Y value of the tile 197 | 198 | z -- zoom level 199 | """ 200 | if z == 0: 201 | return 0 202 | else: 203 | return (1 << (z - 1)) - y - 1 204 | 205 | @staticmethod 206 | def truncate(coord): 207 | """ 208 | Formats a coordinate to an acceptable degree of accuracy (7 decimal 209 | places for Geodetic). 210 | """ 211 | return '%.7f' % (int(coord * 10000000) / float(10000000)) 212 | 213 | 214 | class EllipsoidalMercator(Mercator): 215 | """ 216 | Ellipsoidal Mercator projection class that holds specific calculations and 217 | formulas for EPSG3395. 218 | """ 219 | 220 | def __init__(self): 221 | """ 222 | Constructor 223 | """ 224 | super(EllipsoidalMercator, self).__init__() 225 | 226 | @staticmethod 227 | def lat_to_northing(lat): 228 | """ 229 | Convert a latitude to a northing 230 | / / pi phi \ / 1 - e sin(phi) \ e/2 \ 231 | y(phi) = R ln| tan| --- + --- | | -------------- | | 232 | \ \ 4 2 / \ 1 + e sin(phi) / / 233 | """ 234 | r = 6378137.0 235 | e = 0.081819190842621 236 | return r * log(tan((pi / 2 + lat) / 2) * ((1 - e * sin(lat)) / 237 | (1 + e * sin(lat)))**(e / 2)) 238 | 239 | @staticmethod 240 | def tile_to_lat_lon(z, x, y): 241 | """ 242 | Returns the lat/lon coordinates of the bottom-left corner of the input 243 | tile. Finds the value numerically (using the secant method). 244 | 245 | Inputs: 246 | z -- zoom level value for input tile 247 | x -- tile column value for input tile 248 | y -- tile row value for input tile 249 | """ 250 | n = 2.0**z 251 | lon = x / n * 360.0 - 180.0 252 | my = (y - 2**(z - 1)) * 6378137 * pi * 2 / 2**z 253 | 254 | def f(phi): 255 | return EllipsoidalMercator.lat_to_northing(phi) - my 256 | 257 | lat = 0.0 258 | oldLat = 1.0 259 | diff = 1.0 260 | while abs(diff) > 0.0001: 261 | newLat = lat - f(lat) * (lat - oldLat) / (f(lat) - f(oldLat)) 262 | if newLat > 1.48499697138: 263 | newLat = 1.48499697138 264 | elif newLat < -1.48499697138: 265 | newLat = -1.48499697138 266 | oldLat = lat 267 | lat = newLat 268 | diff = lat - oldLat 269 | lat = lat * 180.0 / pi 270 | return lat, lon 271 | 272 | def tile_to_meters(self, z, x, y): 273 | """ 274 | Returns the meter coordinates of the bottom-left corner of the input 275 | tile. 276 | 277 | Inputs: 278 | z -- zoom level value for input tile 279 | x -- tile column (longitude) value for input tile 280 | y -- tile row (latitude) value for input tile 281 | """ 282 | lat, lon = self.tile_to_lat_lon(z, x, y) 283 | meters_x = lon * self.origin_shift / 180.0 284 | meters_y = self.lat_to_northing(lat * pi / 180.0) 285 | return meters_x, meters_y 286 | 287 | 288 | class ScaledWorldMercator(EllipsoidalMercator): 289 | """ 290 | Scaled World Mercator projection class that holds specific calculations 291 | and formulas for EPSG9804/9805 projection proposed by NGA Craig Rollins. 292 | """ 293 | 294 | def __init__(self): 295 | """ 296 | Constructor 297 | """ 298 | super(ScaledWorldMercator, self).__init__() 299 | 300 | @staticmethod 301 | def pixel_size(z): 302 | """ 303 | Calculates the pixel size for a given zoom level. 304 | """ 305 | return 125829.12 / 2**z 306 | 307 | @staticmethod 308 | def lat_to_northing(lat): 309 | """ 310 | Convert a latitude to a northing 311 | / / pi phi \ / 1 - e sin(phi) \ e/2 \ 312 | y(phi) = R ln| tan| --- + --- | | -------------- | | 313 | \ \ 4 2 / \ 1 + e sin(phi) / / 314 | """ 315 | r = 6378137.0 * 0.857385503731176 316 | e = 0.081819190842621 317 | return r * log(tan((pi / 2 + lat) / 2) * ((1 - e * sin(lat)) / 318 | (1 + e * sin(lat)))**(e / 2)) 319 | 320 | @staticmethod 321 | def tile_to_lat_lon(z, x, y): 322 | """ 323 | Returns the lat/lon coordinates of the bottom-left corner of the input 324 | tile. Finds the value numerically (using the secant method). A scale 325 | factor has been added specifically for scaled world mercator. 326 | 327 | Inputs: 328 | z -- zoom level value for input tile 329 | x -- tile column value for input tile 330 | y -- tile row value for input tile 331 | """ 332 | n = 2.0**z 333 | r = 6378137.0 * 0.857385503731176 334 | lon = x / n * 360.0 - 180.0 335 | my = (y - 2**(z - 1)) * r * pi * 2 / 2**z 336 | 337 | def f(phi): 338 | return ScaledWorldMercator.lat_to_northing(phi) - my 339 | 340 | lat = 0.0 341 | oldLat = 1.0 342 | diff = 1.0 343 | while abs(diff) > 0.0001: 344 | newLat = lat - f(lat) * (lat - oldLat) / (f(lat) - f(oldLat)) 345 | if newLat > 1.4849969713855238: 346 | newLat = 1.4849969713855238 347 | elif newLat < -1.4849969713855238: 348 | newLat = -1.4849969713855238 349 | oldLat = lat 350 | lat = newLat 351 | diff = lat - oldLat 352 | lat = lat * 180.0 / pi 353 | return lat, lon 354 | 355 | def tile_to_meters(self, z, x, y): 356 | """ 357 | Returns the meter coordinates of the bottom-left corner of the input 358 | tile. A scale factor has been added to the longitude meters 359 | calculation. 360 | 361 | Inputs: 362 | z -- zoom level value for input tile 363 | x -- tile column (longitude) value for input tile 364 | y -- tile row (latitude) value for input tile 365 | """ 366 | lat, lon = self.tile_to_lat_lon(z, x, y) 367 | meters_x = lon * (pi * (6378137.0 * 0.857385503731176)) / 180.0 368 | meters_y = self.lat_to_northing(lat * pi / 180.0) 369 | # Instituting a 2 decimal place round to ensure accuracy 370 | return meters_x, round(meters_y, 2) 371 | 372 | 373 | class ZoomMetadata(object): 374 | """Return an object containing metadata about a given zoom level.""" 375 | 376 | @property 377 | def zoom(self): 378 | """Return the zoom level of this metadata object.""" 379 | return self.__zoom 380 | 381 | @zoom.setter 382 | def zoom(self, value): 383 | """Set the zoom level of this metadata object.""" 384 | self.__zoom = value 385 | 386 | @property 387 | def min_tile_col(self): 388 | """Return the minimum tile column of this metadata object.""" 389 | return self.__min_tile_col 390 | 391 | @min_tile_col.setter 392 | def min_tile_col(self, value): 393 | """Set the minimum tile column of this metadata object.""" 394 | self.__min_tile_col = value 395 | 396 | @property 397 | def max_tile_col(self): 398 | """Return the maximum tile column of this metadata object.""" 399 | return self.__max_tile_col 400 | 401 | @max_tile_col.setter 402 | def max_tile_col(self, value): 403 | """Set the maximum tile column of this metadata object.""" 404 | self.__max_tile_col = value 405 | 406 | @property 407 | def min_tile_row(self): 408 | """Return the minimum tile row of this metadata object.""" 409 | return self.__min_tile_row 410 | 411 | @min_tile_row.setter 412 | def min_tile_row(self, value): 413 | """Set the minimum tile row of this metadata object.""" 414 | self.__min_tile_row = value 415 | 416 | @property 417 | def max_tile_row(self): 418 | """Return the maximum tile row of this metadata object.""" 419 | return self.__max_tile_row 420 | 421 | @max_tile_row.setter 422 | def max_tile_row(self, value): 423 | """Set the maximum tile row of this metadata object.""" 424 | self.__max_tile_row = value 425 | 426 | @property 427 | def min_x(self): 428 | """Return the minimum x coordinate of the bounding box.""" 429 | return self.__min_x 430 | 431 | @min_x.setter 432 | def min_x(self, value): 433 | """Set the minimum x coordinate of the bounding box.""" 434 | self.__min_x = value 435 | 436 | @property 437 | def max_x(self): 438 | """Return the maximum x coordinate of the bounding box.""" 439 | return self.__max_x 440 | 441 | @max_x.setter 442 | def max_x(self, value): 443 | """Set the maximum x coordinate of the bounding box.""" 444 | self.__max_x = value 445 | 446 | @property 447 | def min_y(self): 448 | """Return the minimum y coordinate of the bounding box.""" 449 | return self.__min_y 450 | 451 | @min_y.setter 452 | def min_y(self, value): 453 | """Set the minimum y coordinate of the bounding box.""" 454 | self.__min_y = value 455 | 456 | @property 457 | def max_y(self): 458 | """Return the maximum y coordinate of the bounding box.""" 459 | return self.__max_y 460 | 461 | @max_y.setter 462 | def max_y(self, value): 463 | """Set the maximum y coordinate of the bounding box.""" 464 | self.__max_y = value 465 | 466 | @property 467 | def matrix_width(self): 468 | """Number of tiles wide this matrix should be.""" 469 | #return (self.__matrix_width if hasattr(self, 'matrix_width') else None) 470 | return self.__matrix_width or None 471 | 472 | @matrix_width.setter 473 | def matrix_width(self, value): 474 | """Set the number of tiles wide this matrix should be.""" 475 | self.__matrix_width = value 476 | 477 | @property 478 | def matrix_height(self): 479 | """Number of tiles high this matrix should be.""" 480 | return self.__matrix_height or None 481 | 482 | @matrix_height.setter 483 | def matrix_height(self, value): 484 | """Set the number of tiles high this matrix should be.""" 485 | self.__matrix_height = value 486 | 487 | 488 | class Geopackage(object): 489 | """Object representing a GeoPackage container.""" 490 | 491 | def __enter__(self): 492 | """With-statement caller""" 493 | return self 494 | 495 | def __init__(self, file_path, srs): 496 | """Constructor.""" 497 | self.__file_path = file_path 498 | self.__srs = srs 499 | if self.__srs == 3857: 500 | self.__projection = Mercator() 501 | elif self.__srs == 3395: 502 | self.__projection = EllipsoidalMercator() 503 | elif self.__srs == 9804: 504 | self.__projection = ScaledWorldMercator() 505 | else: 506 | self.__projection = Geodetic() 507 | self.__db_con = connect(self.__file_path) 508 | self.__create_schema() 509 | 510 | def __create_schema(self): 511 | """Create default geopackage schema on the database.""" 512 | with self.__db_con as db_con: 513 | cursor = db_con.cursor() 514 | cursor.execute(""" 515 | CREATE TABLE gpkg_contents ( 516 | table_name TEXT NOT NULL PRIMARY KEY, 517 | data_type TEXT NOT NULL, 518 | identifier TEXT UNIQUE, 519 | description TEXT DEFAULT '', 520 | last_change DATETIME NOT NULL DEFAULT 521 | (strftime('%Y-%m-%dT%H:%M:%fZ','now')), 522 | min_x DOUBLE, 523 | min_y DOUBLE, 524 | max_x DOUBLE, 525 | max_y DOUBLE, 526 | srs_id INTEGER, 527 | CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) 528 | REFERENCES gpkg_spatial_ref_sys(srs_id)); 529 | """) 530 | cursor.execute(""" 531 | CREATE TABLE gpkg_spatial_ref_sys ( 532 | srs_name TEXT NOT NULL, 533 | srs_id INTEGER NOT NULL PRIMARY KEY, 534 | organization TEXT NOT NULL, 535 | organization_coordsys_id INTEGER NOT NULL, 536 | definition TEXT NOT NULL, 537 | description TEXT); 538 | """) 539 | cursor.execute(""" 540 | CREATE TABLE gpkg_tile_matrix ( 541 | table_name TEXT NOT NULL, 542 | zoom_level INTEGER NOT NULL, 543 | matrix_width INTEGER NOT NULL, 544 | matrix_height INTEGER NOT NULL, 545 | tile_width INTEGER NOT NULL, 546 | tile_height INTEGER NOT NULL, 547 | pixel_x_size DOUBLE NOT NULL, 548 | pixel_y_size DOUBLE NOT NULL, 549 | CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level), 550 | CONSTRAINT fk_ttm_table_name FOREIGN KEY (table_name) 551 | REFERENCES gpkg_contents(table_name)); 552 | """) 553 | cursor.execute(""" 554 | CREATE TABLE gpkg_tile_matrix_set ( 555 | table_name TEXT NOT NULL PRIMARY KEY, 556 | srs_id INTEGER NOT NULL, 557 | min_x DOUBLE NOT NULL, 558 | min_y DOUBLE NOT NULL, 559 | max_x DOUBLE NOT NULL, 560 | max_y DOUBLE NOT NULL, 561 | CONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name) 562 | REFERENCES gpkg_contents(table_name), 563 | CONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id) 564 | REFERENCES gpkg_spatial_ref_sys(srs_id)); 565 | """) 566 | cursor.execute(""" 567 | CREATE TABLE tiles ( 568 | id INTEGER PRIMARY KEY AUTOINCREMENT, 569 | zoom_level INTEGER NOT NULL, 570 | tile_column INTEGER NOT NULL, 571 | tile_row INTEGER NOT NULL, 572 | tile_data BLOB NOT NULL, 573 | UNIQUE (zoom_level, tile_column, tile_row)); 574 | """) 575 | cursor.execute("pragma foreign_keys = 1;") 576 | # Insert EPSG values for tiles table 577 | wkt = """ 578 | PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984" 579 | ,SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]] 580 | ,AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG", 581 | "8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]] 582 | ,AUTHORITY["EPSG","9122"]]AUTHORITY["EPSG","4326"]],PROJECTION[ 583 | "Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER[ 584 | "scale_factor",1],PARAMETER["false_easting",0],PARAMETER[ 585 | "false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS[ 586 | "X",EAST],AXIS["Y",NORTH] 587 | """ 588 | 589 | cursor.execute(""" 590 | INSERT INTO gpkg_spatial_ref_sys ( 591 | srs_id, 592 | organization, 593 | organization_coordsys_id, 594 | srs_name, 595 | definition) 596 | VALUES (3857, ?, 3857, ?, ?) 597 | """, ("epsg", "WGS 84 / Pseudo-Mercator", wkt)) 598 | wkt = """ 599 | GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137, 600 | 298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG", 601 | "6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT 602 | ["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]], 603 | AUTHORITY["EPSG","4326"]] 604 | """ 605 | 606 | cursor.execute(""" 607 | INSERT INTO gpkg_spatial_ref_sys ( 608 | srs_id, 609 | organization, 610 | organization_coordsys_id, 611 | srs_name, 612 | definition) 613 | VALUES (4326, ?, 4326, ?, ?) 614 | """, ("epsg", "WGS 84", wkt)) 615 | wkt = """ 616 | PROJCS["WGS 84 / World Mercator",GEOGCS["WGS 84", 617 | DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563, 618 | AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]], 619 | PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]], 620 | UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]], 621 | AUTHORITY["EPSG","4326"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]], 622 | PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0], 623 | PARAMETER["scale_factor",1],PARAMETER["false_easting",0], 624 | PARAMETER["false_northing",0],AUTHORITY["EPSG","3395"], 625 | AXIS["Easting",EAST],AXIS["Northing",NORTH]] 626 | """ 627 | 628 | cursor.execute(""" 629 | INSERT INTO gpkg_spatial_ref_sys ( 630 | srs_id, 631 | organization, 632 | organization_coordsys_id, 633 | srs_name, 634 | definition) 635 | VALUES (3395, ?, 3395, ?, ?) 636 | """, ("epsg", "WGS 84 / World Mercator", wkt)) 637 | wkt = """ 638 | PROJCS["unnamed",GEOGCS["WGS 84", 639 | DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563, 640 | AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]], 641 | PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433], 642 | AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"], 643 | PARAMETER["central_meridian",0], 644 | PARAMETER["scale_factor",0.803798909747978], 645 | PARAMETER["false_easting",0], 646 | PARAMETER["false_northing",0], 647 | UNIT["metre",1,AUTHORITY["EPSG","9001"]]] 648 | """ 649 | 650 | cursor.execute(""" 651 | INSERT INTO gpkg_spatial_ref_sys ( 652 | srs_id, 653 | organization, 654 | organization_coordsys_id, 655 | srs_name, 656 | definition) 657 | VALUES (9804, ?, 9804, ?, ?) 658 | """, ("epsg", "WGS 84 / Scaled World Mercator", wkt)) 659 | wkt = """undefined""" 660 | cursor.execute(""" 661 | INSERT INTO gpkg_spatial_ref_sys ( 662 | srs_id, 663 | organization, 664 | organization_coordsys_id, 665 | srs_name, 666 | definition) 667 | VALUES (-1, ?, -1, ?, ?) 668 | """, ("NONE", " ", wkt)) 669 | cursor.execute(""" 670 | INSERT INTO gpkg_spatial_ref_sys ( 671 | srs_id, 672 | organization, 673 | organization_coordsys_id, 674 | srs_name, 675 | definition) 676 | VALUES (0, ?, 0, ?, ?) 677 | """, ("NONE", " ", wkt)) 678 | cursor.execute(""" 679 | INSERT INTO gpkg_contents ( 680 | table_name, 681 | data_type, 682 | identifier, 683 | description, 684 | min_x, 685 | max_x, 686 | min_y, 687 | max_y, 688 | srs_id) 689 | VALUES (?, ?, ?, ?, 0, 0, 0, 0, ?); 690 | """, ("tiles", "tiles", "Raster Tiles", 691 | "Created by tiles2gpkg_parallel.py, written by S. Lander", 692 | self.__srs)) 693 | # Add GP10 to the Sqlite header 694 | cursor.execute("pragma application_id = 1196437808;") 695 | 696 | @property 697 | def file_path(self): 698 | """Return the path of the geopackage database on the file system.""" 699 | return self.__file_path 700 | 701 | def update_metadata(self, metadata): 702 | """Update the metadata of the geopackage database after tile merge.""" 703 | # initialize a new projection 704 | with self.__db_con as db_con: 705 | cursor = db_con.cursor() 706 | tile_matrix_stmt = """ 707 | INSERT OR REPLACE INTO gpkg_tile_matrix ( 708 | table_name, 709 | zoom_level, 710 | matrix_width, 711 | matrix_height, 712 | tile_width, 713 | tile_height, 714 | pixel_x_size, 715 | pixel_y_size) 716 | VALUES (?, ?, ?, ?, ?, ?, ?, ?); 717 | """ 718 | 719 | # iterate through each zoom level object and assign 720 | # matrix data to table 721 | for level in metadata: 722 | cursor.execute( 723 | tile_matrix_stmt, 724 | ("tiles", level.zoom, level.matrix_width, 725 | level.matrix_height, self.__projection.tile_size, 726 | self.__projection.tile_size, 727 | self.__projection.pixel_size(level.zoom), 728 | self.__projection.pixel_size(level.zoom))) 729 | contents_stmt = """ 730 | UPDATE gpkg_contents SET 731 | min_x = ?, 732 | min_y = ?, 733 | max_x = ?, 734 | max_y = ? 735 | WHERE table_name = 'tiles'; 736 | """ 737 | 738 | tile_matrix_set_stmt = """ 739 | INSERT OR REPLACE INTO gpkg_tile_matrix_set ( 740 | table_name, 741 | srs_id, 742 | min_x, 743 | min_y, 744 | max_x, 745 | max_y) 746 | VALUES (?, ?, ?, ?, ?, ?); 747 | """ 748 | 749 | # get bounding box info based on 750 | top_level = min(metadata, key=attrgetter('zoom')) 751 | #top_level.min_x = self.__projection.truncate(top_level.min_x) 752 | #top_level.min_y = self.__projection.truncate(top_level.min_y) 753 | #top_level.max_x = self.__projection.truncate(top_level.max_x) 754 | #top_level.max_y = self.__projection.truncate(top_level.max_y) 755 | top_level.min_x = top_level.min_x 756 | top_level.min_y = top_level.min_y 757 | top_level.max_x = top_level.max_x 758 | top_level.max_y = top_level.max_y 759 | # write bounds and matrix set info to table 760 | cursor.execute(contents_stmt, (top_level.min_x, top_level.min_y, 761 | top_level.max_x, top_level.max_y)) 762 | cursor.execute(tile_matrix_set_stmt, 763 | ('tiles', self.__srs, top_level.min_x, 764 | top_level.min_y, top_level.max_x, top_level.max_y)) 765 | 766 | def execute(self, statement, inputs=None): 767 | """Execute a prepared SQL statement on this geopackage database.""" 768 | with self.__db_con as db_con: 769 | cursor = db_con.cursor() 770 | if inputs is not None: 771 | result_cursor = cursor.execute(statement, inputs) 772 | else: 773 | result_cursor = cursor.execute(statement) 774 | return result_cursor 775 | 776 | def assimilate(self, source): 777 | """Assimilate .gpkg.part tiles into this geopackage database.""" 778 | if not exists(source): 779 | raise IOError 780 | with self.__db_con as db_con: 781 | cursor = db_con.cursor() 782 | cursor.execute("pragma synchronous = off;") 783 | cursor.execute("pragma journal_mode = off;") 784 | cursor.execute("pragma page_size = 65536;") 785 | #print "Merging", source, "into", self.__file_path, "..." 786 | query = "attach '" + source + "' as source;" 787 | cursor.execute(query) 788 | try: 789 | cursor.execute("""INSERT OR REPLACE INTO tiles 790 | (zoom_level, tile_column, tile_row, tile_data) 791 | SELECT zoom_level, tile_column, tile_row, tile_data 792 | FROM source.tiles;""") 793 | cursor.execute("detach source;") 794 | except Error as err: 795 | print("Error: {}".format(type(err))) 796 | print("Error msg:".format(err)) 797 | raise 798 | remove(source) 799 | 800 | def __exit__(self, type, value, traceback): 801 | """Resource cleanup on destruction.""" 802 | self.__db_con.close() 803 | 804 | 805 | class TempDB(object): 806 | """ 807 | Returns a temporary sqlite database to hold tiles for async workers. 808 | Has a .gpkg.part file format. 809 | """ 810 | 811 | def __enter__(self): 812 | """With-statement caller.""" 813 | return self 814 | 815 | def __init__(self, filename): 816 | """ 817 | Constructor. 818 | 819 | Inputs: 820 | filename -- the filename this database will be created with 821 | """ 822 | uid = uuid4() 823 | self.name = uid.hex + '.gpkg.part' 824 | self.__file_path = join(filename, self.name) 825 | self.__db_con = connect(self.__file_path) 826 | with self.__db_con as db_con: 827 | cursor = db_con.cursor() 828 | stmt = """ 829 | CREATE TABLE tiles ( 830 | id INTEGER PRIMARY KEY AUTOINCREMENT, 831 | zoom_level INTEGER NOT NULL, 832 | tile_column INTEGER NOT NULL, 833 | tile_row INTEGER NOT NULL, 834 | tile_data BLOB NOT NULL, 835 | UNIQUE (zoom_level, tile_column, tile_row)); 836 | """ 837 | 838 | cursor.execute(stmt) 839 | # Enable pragma for fast sqlite creation 840 | cursor.execute("pragma synchronous = off;") 841 | cursor.execute("pragma journal_mode = off;") 842 | cursor.execute("pragma page_size = 80000;") 843 | cursor.execute("pragma foreign_keys = 1;") 844 | self.image_blob_stmt = """ 845 | INSERT INTO tiles 846 | (zoom_level, tile_column, tile_row, tile_data) 847 | VALUES (?,?,?,?) 848 | """ 849 | 850 | def execute(self, statement, inputs=None): 851 | with self.__db_con as db_con: 852 | cursor = db_con.cursor() 853 | if inputs is not None: 854 | result_cursor = cursor.execute(statement, inputs) 855 | else: 856 | result_cursor = cursor.execute(statement) 857 | return result_cursor 858 | 859 | def insert_image_blob(self, z, x, y, data): 860 | """ 861 | Inserts a binary data array containing an image into a sqlite3 862 | database. 863 | 864 | Inputs: 865 | z -- the zoom level of the binary data 866 | x -- the row number of the data 867 | y -- the column number of the data 868 | data -- the image data containing in a binary array 869 | """ 870 | with self.__db_con as db_con: 871 | cursor = db_con.cursor() 872 | cursor.execute(self.image_blob_stmt, (z, x, y, data)) 873 | 874 | def __exit__(self, type, value, traceback): 875 | """Resource cleanup on destruction.""" 876 | self.__db_con.close() 877 | 878 | 879 | def img_to_buf(img, img_type, jpeg_quality=75): 880 | """ 881 | Returns a buffer array with image binary data for the input image. 882 | This code is based on logic implemented in MapProxy to convert PNG 883 | images to JPEG then return the buffer. 884 | 885 | Inputs: 886 | img -- an image on the filesystem to be converted to binary 887 | img_type -- the MIME type of the image (JPG, PNG) 888 | """ 889 | defaults = {} 890 | buf = ioBuffer() 891 | if img_type == 'jpeg': 892 | img.convert('RGB') 893 | # Hardcoding a default compression of 75% for JPEGs 894 | defaults['quality'] = jpeg_quality 895 | elif img_type == 'source': 896 | img_type = img.format 897 | img.save(buf, img_type, **defaults) 898 | buf.seek(0) 899 | return buf 900 | 901 | 902 | def img_has_transparency(img): 903 | """ 904 | Returns a 0 if the input image has no transparency, 1 if it has some, 905 | and -1 if the image is fully transparent. Tiles *should be a perfect 906 | square (e.g, 256x256), so it can be safe to assume the first dimension 907 | will match the second. This will ensure compatibility with different 908 | tile sizes other than 256x256. This code is based on logic implemented 909 | in MapProxy to check for images that have transparency. 910 | 911 | Inputs: 912 | img -- an Image object from the PIL library 913 | """ 914 | size = img.size[0] 915 | if img.mode == 'P': 916 | # For paletted images 917 | if img.info.get('transparency', False): 918 | return True 919 | # Convert to RGBA to check alpha 920 | img = img.convert('RGBA') 921 | if img.mode == 'RGBA': 922 | # Returns the number of pixels in this image that are transparent 923 | # Assuming a tile size of 256, 65536 would be fully transparent 924 | transparent_pixels = img.histogram()[-size] 925 | if transparent_pixels == 0: 926 | # No transparency 927 | return 0 928 | elif 0 < transparent_pixels < (size * size): 929 | # Image has some transparency 930 | return 1 931 | else: 932 | # Image is fully transparent, and can be discarded 933 | return -1 934 | #return img.histogram()[-size] 935 | return False 936 | 937 | 938 | def file_count(base_dir): 939 | """ 940 | A function that finds all image tiles in a base directory. The base 941 | directory should be arranged in TMS format, i.e. z/x/y. 942 | 943 | Inputs: 944 | base_dir -- the name of the TMS folder containing tiles. 945 | 946 | Returns: 947 | A list of dictionary objects containing the full file path and TMS 948 | coordinates of the image tile. 949 | """ 950 | print("Calculating number of tiles, this could take a while...") 951 | file_list = [] 952 | # Avoiding dots (functional references) will increase performance of 953 | # the loop because they will not be reevaluated each iteration. 954 | for root, sub_folders, files in walk(base_dir): 955 | temp_list = [join(root, f) for f in files if f.endswith(IMAGE_TYPES)] 956 | file_list += temp_list 957 | print("Found {} total tiles.".format(len(file_list))) 958 | return [split_all(item) for item in file_list] 959 | 960 | 961 | def split_all(path): 962 | """ 963 | Function that parses TMS coordinates from a full images file path. 964 | 965 | Inputs: 966 | path -- a full file path to an image tile. 967 | 968 | Returns: 969 | A dictionary containing the TMS coordinates of the tile and its full 970 | file path. 971 | """ 972 | parts = [] 973 | full_path = path 974 | # Parse out the tms coordinates 975 | for i in xrange(3): 976 | head, tail = split(path) 977 | parts.append(tail) 978 | path = head 979 | file_dict = dict(y=int(parts[0].split('.')[0]), 980 | x=int(parts[1]), 981 | z=int(parts[2]), 982 | path=full_path) 983 | return file_dict 984 | 985 | 986 | def worker_map(temp_db, tile_dict, extra_args, invert_y): 987 | """ 988 | Function responsible for sending the correct oriented tile data to a 989 | temporary sqlite3 database. 990 | 991 | Inputs: 992 | temp_db -- a temporary sqlite3 database that will hold this worker's tiles 993 | tile_dict -- a dictionary with TMS coordinates and file path for a tile 994 | tile_info -- a list of ZoomMetadata objects pre-generated for this tile set 995 | imagery -- the type of image format to send to the sqlite3 database 996 | invert_y -- a function that will flip the Y axis of the tile if present 997 | """ 998 | tile_info = extra_args['tile_info'] 999 | imagery = extra_args['imagery'] 1000 | jpeg_quality = extra_args['jpeg_quality'] 1001 | zoom = tile_dict['z'] 1002 | level = next((item for item in tile_info if item.zoom == zoom), None) 1003 | x_row = tile_dict['x'] - level.min_tile_row 1004 | if invert_y is not None: 1005 | y_offset = invert_y(tile_dict['z'], level.max_tile_col) 1006 | y_column = invert_y(tile_dict['z'], tile_dict['y']) 1007 | y_column -= y_offset 1008 | else: 1009 | y_column = tile_dict['y'] - level.min_tile_col 1010 | if IOPEN is not None: 1011 | img = IOPEN(tile_dict['path'], 'r') 1012 | data = ioBuffer() 1013 | if imagery == 'mixed': 1014 | if img_has_transparency(img): 1015 | data = img_to_buf(img, 'png', jpeg_quality).read() 1016 | else: 1017 | data = img_to_buf(img, 'jpeg', jpeg_quality).read() 1018 | else: 1019 | data = img_to_buf(img, imagery, jpeg_quality).read() 1020 | temp_db.insert_image_blob(zoom, x_row, y_column, sbinary(data)) 1021 | else: 1022 | file_handle = open(tile_dict['path'], 'rb') 1023 | data = buffer(file_handle.read()) 1024 | temp_db.insert_image_blob(zoom, x_row, y_column, data) 1025 | file_handle.close() 1026 | 1027 | 1028 | def sqlite_worker(file_list, extra_args): 1029 | """ 1030 | Worker function called by asynchronous processes. This function 1031 | iterates through a set of tiles to process them into a TempDB object. 1032 | 1033 | Inputs: 1034 | file_list -- an array containing a subset of tiles that will be processed 1035 | by this function into a TempDB object 1036 | base_dir -- the directory in which the geopackage will be created, 1037 | .gpkg.part files will be generated here 1038 | metadata -- a ZoomLevelMetadata object containing information about 1039 | the tiles in the TMS directory 1040 | """ 1041 | temp_db = TempDB(extra_args['root_dir']) 1042 | with TempDB(extra_args['root_dir']) as temp_db: 1043 | invert_y = None 1044 | if extra_args['lower_left']: 1045 | if extra_args['srs'] == 3857: 1046 | invert_y = Mercator.invert_y 1047 | elif extra_args['srs'] == 4326: 1048 | invert_y = Geodetic.invert_y 1049 | elif extra_args['srs'] == 3395: 1050 | invert_y = EllipsoidalMercator.invert_y 1051 | elif extra_args['srs'] == 9804: 1052 | invert_y = ScaledWorldMercator.invert_y 1053 | [worker_map(temp_db, item, extra_args, invert_y) for item in file_list] 1054 | 1055 | 1056 | def allocate(cores, pool, file_list, extra_args): 1057 | """ 1058 | Recursive function that fairly distributes tiles to asynchronous worker 1059 | processes. For N processes and C cores, N=C if C is divisible by 2. If 1060 | not, then N is the largest factor of 8 that is still less than C. 1061 | """ 1062 | if cores is 1: 1063 | print("Spawning worker with {} files".format(len(file_list))) 1064 | return [pool.apply_async(sqlite_worker, [file_list, extra_args])] 1065 | else: 1066 | files = len(file_list) 1067 | head = allocate( 1068 | int(cores / 2), pool, file_list[:int(files / 2)], extra_args) 1069 | tail = allocate( 1070 | int(cores / 2), pool, file_list[int(files / 2):], extra_args) 1071 | return head + tail 1072 | 1073 | 1074 | def build_lut(file_list, lower_left, srs): 1075 | """ 1076 | Build a lookup table that aids in metadata generation. 1077 | 1078 | Inputs: 1079 | file_list -- the file_list dict made with file_count() 1080 | lower_left -- bool indicating tile grid numbering scheme (tms or wmts) 1081 | srs -- the spatial reference system of the tile grid 1082 | 1083 | Returns: 1084 | An array of ZoomLevelMetadata objects that describe each zoom level of the 1085 | tile grid. 1086 | """ 1087 | # Initialize a projection class 1088 | if srs == 3857: 1089 | projection = Mercator() 1090 | elif srs == 4326: 1091 | projection = Geodetic() 1092 | elif srs == 9804: 1093 | projection = ScaledWorldMercator() 1094 | else: 1095 | projection = EllipsoidalMercator() 1096 | # Create a list of zoom levels from the base directory 1097 | zoom_levels = list(set([int(item['z']) for item in file_list])) 1098 | zoom_levels.sort() 1099 | matrix = [] 1100 | # For every zoom in the list... 1101 | for zoom in zoom_levels: 1102 | # create a new ZoomMetadata object... 1103 | level = ZoomMetadata() 1104 | level.zoom = zoom 1105 | # Sometimes, tiling programs do not generate the folders responsible 1106 | # for the X axis if no tiles are being made within them. This results 1107 | # in tiles "shifting" because of the way they are renumbered when 1108 | # placed into a geopackage. 1109 | # To fix, is there a zoom level preceding this one... 1110 | if zoom - 1 in [item for item in zoom_levels if item == (zoom - 1)]: 1111 | # there is, now retrieve it.... 1112 | (prev, ) = ([item for item in matrix if item.zoom == (zoom - 1)]) 1113 | # and fix the grid alignment values 1114 | level.min_tile_row = 2 * prev.min_tile_row 1115 | level.min_tile_col = 2 * prev.min_tile_col 1116 | level.max_tile_row = 2 * prev.max_tile_row + 1 1117 | level.max_tile_col = 2 * prev.max_tile_col + 1 1118 | # Calculate the width and height 1119 | level.matrix_width = prev.matrix_width * 2 1120 | level.matrix_height = prev.matrix_height * 2 1121 | else: 1122 | # Get all possible x and y values... 1123 | x_vals = [int(item['x']) 1124 | for item in file_list if int(item['z']) == zoom] 1125 | y_vals = [int(item['y']) 1126 | for item in file_list if int(item['z']) == zoom] 1127 | # then get the min/max values for each. 1128 | level.min_tile_row, level.max_tile_row = min(x_vals), max(x_vals) 1129 | level.min_tile_col, level.max_tile_col = min(y_vals), max(y_vals) 1130 | # Fill in the matrix width and height for this top level 1131 | x_width_max = max([item[ 1132 | 'x'] for item in file_list if item['z'] == level.zoom]) 1133 | x_width_min = min([item[ 1134 | 'x'] for item in file_list if item['z'] == level.zoom]) 1135 | level.matrix_width = (x_width_max - x_width_min) + 1 1136 | y_height_max = max([item[ 1137 | 'y'] for item in file_list if item['z'] == level.zoom]) 1138 | y_height_min = min([item[ 1139 | 'y'] for item in file_list if item['z'] == level.zoom]) 1140 | level.matrix_height = (y_height_max - y_height_min) + 1 1141 | if lower_left: 1142 | # TMS-style tile grid, so to calc the top left corner of the grid, 1143 | # you must get the min x (row) value and the max y (col) value + 1. 1144 | # You are adding 1 to the y value because the math to calc the 1145 | # coord assumes you want the bottom left corner, not the top left. 1146 | # Similarly, to get the bottom right corner, add 1 to x value. 1147 | level.min_x, level.max_y = projection.get_coord( 1148 | level.zoom, level.min_tile_row, level.max_tile_col + 1) 1149 | level.max_x, level.min_y = projection.get_coord( 1150 | level.zoom, level.max_tile_row + 1, level.min_tile_col) 1151 | else: 1152 | # WMTS-style tile grid, so to calc the top left corner of the grid, 1153 | # you must get the min x (row value and the min y (col) value + 1. 1154 | # You are adding 1 to the y value because the math to calc the 1155 | # coord assumes you want the bottom left corner, not the top left. 1156 | # Similarly, to get the bottom right corner, add 1 to x value. 1157 | # -- Since this is WMTS, we must invert the Y axis before we calc 1158 | inv_min_y = projection.invert_y(level.zoom, level.min_tile_col) 1159 | inv_max_y = projection.invert_y(level.zoom, level.max_tile_col) 1160 | level.min_x, level.max_y = projection.get_coord( 1161 | level.zoom, level.min_tile_row, inv_min_y + 1) 1162 | level.max_x, level.min_y = projection.get_coord( 1163 | level.zoom, level.max_tile_row + 1, inv_max_y) 1164 | # Finally, add this ZoomMetadata object to the list 1165 | matrix.append(level) 1166 | return matrix 1167 | 1168 | 1169 | def combine_worker_dbs(out_geopackage): 1170 | """ 1171 | Searches for .gpkg.part files in the base directory and merges them 1172 | into one Geopackage file 1173 | 1174 | Inputs: 1175 | out_geopackage -- the final output geopackage file 1176 | """ 1177 | base_dir = split(out_geopackage.file_path)[0] 1178 | if base_dir == "": 1179 | base_dir = "." 1180 | glob_path = join(base_dir + '/*.gpkg.part') 1181 | file_list = glob(glob_path) 1182 | print("Merging temporary databases...") 1183 | #[out_geopackage.assimilate(f) for f in file_list] 1184 | itr = len(file_list) 1185 | status = ["|", "/", "-", "\\"] 1186 | counter = 0 1187 | for tdb in file_list: 1188 | comp = len(file_list) - itr 1189 | itr -= 1 1190 | out_geopackage.assimilate(tdb) 1191 | if tdb == file_list[-1]: 1192 | stdout.write("\r[X] Progress: [" + "==" * comp + " " * itr + "]") 1193 | else: 1194 | stdout.write("\r[" + status[counter] + "] Progress: [" + "==" * 1195 | comp + " " * itr + "]") 1196 | stdout.flush() 1197 | if counter != len(status) - 1: 1198 | counter += 1 1199 | else: 1200 | counter = 0 1201 | print(" All geopackages merged!") 1202 | 1203 | 1204 | def main(arg_list): 1205 | """ 1206 | Create a geopackage from a directory of tiles arranged in TMS or WMTS 1207 | format. 1208 | 1209 | Inputs: 1210 | arg_list -- an ArgumentParser object containing command-line options and 1211 | flags 1212 | """ 1213 | # Build the file dictionary 1214 | files = file_count(arg_list.source_folder) 1215 | if len(files) == 0: 1216 | # If there are no files, exit the script 1217 | print(" Ensure the correct source tile directory was specified.") 1218 | exit(1) 1219 | # Is the input tile grid aligned to lower-left or not? 1220 | lower_left = arg_list.tileorigin == 'll' or arg_list.tileorigin == 'sw' 1221 | # Get the output file destination directory 1222 | root_dir, _ = split(arg_list.output_file) 1223 | # Build the tile matrix info object 1224 | tile_info = build_lut(files, lower_left, arg_list.srs) 1225 | # Initialize the output file 1226 | if arg_list.threading: 1227 | # Enable tiling on multiple CPU cores 1228 | cores = cpu_count() 1229 | pool = Pool(cores) 1230 | # Build allocate dictionary 1231 | extra_args = dict(root_dir=root_dir, 1232 | tile_info=tile_info, 1233 | lower_left=lower_left, 1234 | srs=arg_list.srs, 1235 | imagery=arg_list.imagery, 1236 | jpeg_quality=arg_list.q) 1237 | results = allocate(cores, pool, files, extra_args) 1238 | status = ["|", "/", "-", "\\"] 1239 | counter = 0 1240 | try: 1241 | while True: 1242 | rem = sum([1 for item in results if not item.ready()]) 1243 | if rem == 0: 1244 | stdout.write("\r[X] Progress: [" + "==" * (cores - rem) + 1245 | " " * rem + "]") 1246 | stdout.flush() 1247 | print(" All Done!") 1248 | break 1249 | else: 1250 | stdout.write("\r[" + status[counter] + "] Progress: [" + 1251 | "==" * (cores - rem) + " " * rem + "]") 1252 | stdout.flush() 1253 | if counter != len(status) - 1: 1254 | counter += 1 1255 | else: 1256 | counter = 0 1257 | sleep(.25) 1258 | pool.close() 1259 | pool.join() 1260 | except KeyboardInterrupt: 1261 | print(" Interrupted!") 1262 | pool.terminate() 1263 | exit(1) 1264 | else: 1265 | # Debugging call to bypass multiprocessing (-T) 1266 | extra_args = dict(root_dir=root_dir, 1267 | tile_info=tile_info, 1268 | lower_left=lower_left, 1269 | srs=arg_list.srs, 1270 | imagery=arg_list.imagery, 1271 | jpeg_quality=arg_list.q) 1272 | sqlite_worker(files, extra_args) 1273 | # Combine the individual temp databases into the output file 1274 | with Geopackage(arg_list.output_file, arg_list.srs) as gpkg: 1275 | combine_worker_dbs(gpkg) 1276 | # Using the data in the output file, create the metadata for it 1277 | gpkg.update_metadata(tile_info) 1278 | print("Complete") 1279 | 1280 | 1281 | if __name__ == '__main__': 1282 | print(""" 1283 | tiles2gpkg_parallel.py Copyright (C) 2014 Reinventing Geospatial, Inc 1284 | This program comes with ABSOLUTELY NO WARRANTY. 1285 | This is free software, and you are welcome to redistribute it 1286 | under certain conditions. 1287 | """) 1288 | PARSER = ArgumentParser(description="Convert TMS folder into geopackage") 1289 | PARSER.add_argument("source_folder", 1290 | metavar="source", 1291 | help="Source folder of TMS files.") 1292 | PARSER.add_argument("output_file", 1293 | metavar="dest", 1294 | help="Destination file path.") 1295 | PARSER.add_argument("-tileorigin", 1296 | metavar="tile_origin", 1297 | help="Tile point of origin location. Valid options " + 1298 | "are ll, ul, nw, or sw.", 1299 | choices=["ll", "ul", "sw", "nw"], 1300 | default="ll") 1301 | PARSER.add_argument("-srs", 1302 | metavar="srs", 1303 | help="Spatial reference " + "system. Valid options are" 1304 | + "3857, 4326, 3395, and 9804.", 1305 | type=int, 1306 | choices=[3857, 4326, 3395, 9804], 1307 | default=3857) 1308 | PARSER.add_argument("-imagery", 1309 | metavar="imagery", 1310 | help="Imagery type. Valid options are mixed, " + 1311 | "jpeg, png, or source.", 1312 | choices=["mixed", "jpeg", "png", "source"], 1313 | default="source") 1314 | PARSER.add_argument("-q", 1315 | metavar="quality", 1316 | type=int, 1317 | default=75, 1318 | help="Quality for jpeg images, 0-100. Default is 75", 1319 | choices=list(range(100))) 1320 | PARSER.add_argument("-a", 1321 | dest="append", 1322 | action="store_true", 1323 | default=False, 1324 | help="Append tile set to existing geopackage") 1325 | PARSER.add_argument("-T", 1326 | dest="threading", 1327 | action="store_false", 1328 | default=True, 1329 | help="Disable multiprocessing.") 1330 | ARG_LIST = PARSER.parse_args() 1331 | if not exists(ARG_LIST.source_folder) or exists(ARG_LIST.output_file): 1332 | PARSER.print_usage() 1333 | print("Ensure that TMS directory exists and out file does not.") 1334 | exit(1) 1335 | if ARG_LIST.q is not None and ARG_LIST.imagery == 'png': 1336 | PARSER.print_usage() 1337 | print("-q cannot be used with png") 1338 | exit(1) 1339 | main(ARG_LIST) 1340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | geopackage-python : Python-based tools for creating OGC GeoPackages. 2 | ================= 3 | 4 | [GeoPackage Specification from the Open Geospatial 5 | Consortium](http://opengeospatial.org/standards/geopackage) 6 | 7 | [![Build Status](https://travis-ci.org/GitHubRGI/geopackage-python.svg?branch=master)](https://travis-ci.org/GitHubRGI/geopackage-python) 8 | [![Coverage Status](https://img.shields.io/coveralls/GitHubRGI/geopackage-python.svg)](https://coveralls.io/r/GitHubRGI/geopackage-python) 9 | [![Stories in Ready](https://badge.waffle.io/GitHubRGI/geopackage-python.png?label=ready&title=Ready)](https://waffle.io/GitHubRGI/geopackage-python) 10 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/GitHubRGI/geopackage-python/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/GitHubRGI/geopackage-python/?branch=master) 11 | 12 | ### Table Of Contents 13 | 14 | * Installing Dependencies 15 | ([Windows](https://github.com/GitHubRGI/geopackage-python/wiki/Installing-dependencies-on-Windows), [Linux](https://github.com/GitHubRGI/geopackage-python/wiki/Installing-dependencies-on-Linux)) 16 | * [How to use the tiling script, 17 | gdal2tiles_parallel.py](https://github.com/GitHubRGI/geopackage-python/wiki/Usage-Instructions-for-gdal2tiles_parallel.py) 18 | * [How to use the packaging script, 19 | tiles2gpkg_parallel.py](https://github.com/GitHubRGI/geopackage-python/wiki/Usage-Instructions-for-tiles2gpkg_parallel.py) 20 | * [Running unit tests on 21 | tiles2gpkg_parallel.py](https://github.com/GitHubRGI/geopackage-python/wiki/Running-Unit-Tests-On-tiles2gpkg_parallel.py) 22 | 23 | ### Download stable versions 24 | 25 | * [Release 5.0 - Archived on 2015-05-06](https://github.com/GitHubRGI/geopackage-python/releases/tag/v5.0) - Python 3.4 support and better SQLite3 resource usage 26 | * [Release 4.0 - Archived on 2015-04-30](https://github.com/GitHubRGI/geopackage-python/releases/tag/v4.0) - Fix for incorrect tile matrix width and height values 27 | * [Release 3.0 - Archived on 2015-02-11](https://github.com/GitHubRGI/geopackage-python/archive/geopackage-python_release3.0.zip) - Fix to include proper matrix sizes for height and width in gpkg_tile_matrix when those tiles do not exist 28 | * [Release 2.0 - Archived on 2015-01-13](https://github.com/GitHubRGI/geopackage-python/archive/geopackage-python_release2.0.zip) 29 | -------------------------------------------------------------------------------- /Testing/rgb_tiles/geodetic/1/0/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/geodetic/1/0/0.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/geodetic/2/0/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/geodetic/2/0/0.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/geodetic/2/0/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/geodetic/2/0/1.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/geodetic/2/1/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/geodetic/2/1/0.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/geodetic/2/1/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/geodetic/2/1/1.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/mercator/1/0/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/mercator/1/0/0.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/mercator/1/0/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/mercator/1/0/1.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/mercator/1/1/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/mercator/1/1/0.png -------------------------------------------------------------------------------- /Testing/rgb_tiles/mercator/1/1/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHubRGI/geopackage-python/24963d2cf02d5fd1d3251e3d939739e080e1355c/Testing/rgb_tiles/mercator/1/1/1.png -------------------------------------------------------------------------------- /Testing/test_gdal2tiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | """ 3 | Copyright (C) 2014 Reinventing Geospatial, Inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see , 17 | or write to the Free Software Foundation, Inc., 59 Temple Place - 18 | Suite 330, Boston, MA 02111-1307, USA. 19 | 20 | Authors: 21 | Steven D. Lander 22 | Date: 2015-11 23 | Requires: Python Imaging Library (PIL or Pillow), Sqlite3 24 | Description: Test cases for gdal2tiles_parallel.py 25 | 26 | Version: 27 | """ 28 | 29 | from os.path import abspath 30 | from sys import path 31 | 32 | path.append(abspath("Tiling")) 33 | from gdal2tiles_parallel import Tile 34 | from gdal2tiles_parallel import GlobalMercatorProfile 35 | 36 | 37 | class TestGlobalMercatorProfile: 38 | """Test the GlobalMercatorProfile object""" 39 | 40 | def test_tile_size(self): 41 | """Test the default tile size""" 42 | gmp = GlobalMercatorProfile() 43 | assert gmp.tile_size == 256 44 | 45 | def test_tile_size_custom(self): 46 | """Test a custom tile size""" 47 | gmp = GlobalMercatorProfile(512) 48 | assert gmp.tile_size == 512 49 | 50 | def test_lower_left_tile(self): 51 | """Test lower left tile method""" 52 | gmp = GlobalMercatorProfile() 53 | tile = Tile(0, 1) 54 | zoom = 1 55 | result_tile, result_zoom = gmp.lower_left_tile(tile, zoom) 56 | assert result_tile.tx == tile.tx and \ 57 | result_tile.ty == tile.ty and \ 58 | result_zoom == zoom 59 | 60 | def test_upper_left_tile(self): 61 | """Test upper left tile method""" 62 | gmp = GlobalMercatorProfile() 63 | tile = Tile(0, 1) 64 | zoom = 1 65 | result_tile, result_zoom = gmp.upper_left_tile(tile, zoom) 66 | assert result_tile.tx == tile.tx and \ 67 | result_tile.ty == 0 and \ 68 | result_zoom == zoom 69 | 70 | def test_tile_from_pixels(self): 71 | """Test making a tile from pixels""" 72 | gmp = GlobalMercatorProfile() 73 | zoom = 1 74 | point = MetersPoint(-20037508.3204, -20037508.3204) 75 | x, y = gmp.pixels_from_units(point, zoom) 76 | -------------------------------------------------------------------------------- /Testing/test_gdal2tiles_parallel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from sys import path 4 | from os.path import abspath 5 | path.append(abspath("Tiling")) 6 | 7 | from gdal2tiles_parallel import Tile 8 | from gdal2tiles_parallel import MetersPoint 9 | from gdal2tiles_parallel import PixelsPoint 10 | from gdal2tiles_parallel import LonLatPoint 11 | from gdal2tiles_parallel import ITileProfile 12 | from gdal2tiles_parallel import GlobalMercatorProfile 13 | 14 | 15 | class TestITileProfile: 16 | def test_lower_left_tile_1(self): 17 | itp = ITileProfile() 18 | tile = Tile(0, 0) 19 | zoom = 1 20 | result = itp.lower_left_tile(tile, zoom) 21 | assert result.tx == 0 and result.ty == 0 22 | 23 | def test_upper_left_tile_1(self): 24 | itp = ITileProfile() 25 | tile = Tile(0, 0) 26 | zoom = 1 27 | result = itp.upper_left_tile(tile, zoom) 28 | assert result.tx == 0 and result.ty == 1 29 | 30 | def test_upper_left_tile_2(self): 31 | itp = ITileProfile() 32 | tile = Tile(0, 31) 33 | zoom = 13 34 | result = itp.upper_left_tile(tile, zoom) 35 | assert result.tx == 0 and result.ty == 8160 36 | 37 | def test_quad_tree_1(self): 38 | itp = ITileProfile() 39 | tile = Tile(0, 31) 40 | zoom = 13 41 | result = itp.quad_tree(tile, zoom) 42 | assert result == '2222222200000' 43 | 44 | 45 | class TestGlobalMercatorProfile: 46 | def test_init_1(self): 47 | gmp = GlobalMercatorProfile() 48 | assert gmp.tile_size == 256 and \ 49 | gmp.initial_resolution == 156543.03392804097 and \ 50 | gmp.origin_shift == 20037508.342789244 51 | 52 | def test_init_2(self): 53 | gmp = GlobalMercatorProfile(tile_size=512) 54 | assert gmp.tile_size == 512 and \ 55 | gmp.initial_resolution == 78271.51696402048 56 | 57 | def test_resolution_1(self): 58 | gmp = GlobalMercatorProfile() 59 | result = gmp.resolution(13) 60 | assert result == 19.109257071294063 61 | 62 | def test_zoom_for_pixel_size_1(self): 63 | gmp = GlobalMercatorProfile() 64 | pixel_size = 10 65 | result = gmp.zoom_for_pixel_size(pixel_size) 66 | assert result == 13 67 | 68 | def test_pixels_to_tile_1(self): 69 | gmp = GlobalMercatorProfile() 70 | point = PixelsPoint(256, 256) 71 | result = gmp.to_tile(point) 72 | assert result.tx == 0 and result.ty == 0 73 | 74 | def test_pixels_to_tile_2(self): 75 | gmp = GlobalMercatorProfile() 76 | point = PixelsPoint(256, 257) 77 | result = gmp.to_tile(point) 78 | assert result.tx == 0 and result.ty == 1 79 | 80 | def test_meters_to_tile_1(self): 81 | gmp = GlobalMercatorProfile() 82 | point = MetersPoint(-20037508.34, 20037508.34) 83 | result = gmp.to_tile(point, zoom=0) 84 | assert result.tx == 0 and result.ty == 0 85 | 86 | def test_meters_to_tile_2(self): 87 | gmp = GlobalMercatorProfile() 88 | point = MetersPoint(7682838.58, 4106808.65) 89 | result = gmp.to_tile(point, zoom=14) 90 | assert result.tx == 11332 and result.ty == 9870 91 | 92 | def test_meters_to_tile_3(self): 93 | gmp = GlobalMercatorProfile() 94 | point = MetersPoint(7682838.58, 4106808.65) 95 | try: 96 | gmp.to_tile(point) 97 | assert False 98 | except KeyError as e: 99 | assert e is not None and type(e) == KeyError 100 | 101 | def test_meters_to_tile_4(self): 102 | gmp = GlobalMercatorProfile() 103 | point = MetersPoint(7682838.58, 4106808.65) 104 | try: 105 | gmp.to_tile(point, foo=12) 106 | assert False 107 | except KeyError as e: 108 | assert e is not None and type(e) == KeyError 109 | 110 | def test_tile_to_tile_1(self): 111 | gmp = GlobalMercatorProfile() 112 | tile = Tile(34, 100) 113 | result = gmp.to_tile(tile) 114 | assert result.tx == tile.tx and \ 115 | result.ty == tile.ty 116 | 117 | def test_lon_lat_to_tile_1(self): 118 | gmp = GlobalMercatorProfile() 119 | point = LonLatPoint(-34.123, 78.1234) 120 | try: 121 | gmp.to_tile(point) 122 | assert False 123 | except NotImplementedError as e: 124 | assert e is not None and type(e) == NotImplementedError 125 | 126 | def test_lon_lat_to_map_coords_1(self): 127 | gmp = GlobalMercatorProfile() 128 | point = LonLatPoint(180, 85) 129 | result = gmp.to_map_coordinates(point) 130 | assert result.mx == 20037508.342789244 and \ 131 | result.my == 19971868.88040853 132 | 133 | def test_lon_lat_to_map_coords_2(self): 134 | gmp = GlobalMercatorProfile() 135 | point = LonLatPoint(-148.99, 61.60) 136 | result = gmp.to_map_coordinates(point) 137 | assert result.mx == -16585490.933289833 and \ 138 | result.my == 8764912.67945232 139 | 140 | def test_pixels_to_map_coords_1(self): 141 | gmp = GlobalMercatorProfile() 142 | point = PixelsPoint(256, 256) 143 | result = gmp.to_map_coordinates(point, zoom=0) 144 | assert result.mx == 20037508.342789244 and \ 145 | result.my == 20037508.342789244 146 | 147 | def test_pixels_to_map_coords_2(self): 148 | gmp = GlobalMercatorProfile() 149 | point = PixelsPoint(0, 0) 150 | result = gmp.to_map_coordinates(point, zoom=0) 151 | assert result.mx == -20037508.342789244 and \ 152 | result.my == -20037508.342789244 153 | 154 | def test_pixels_to_map_coords_3(self): 155 | gmp = GlobalMercatorProfile() 156 | point = PixelsPoint(0, 0) 157 | try: 158 | gmp.to_map_coordinates(point) 159 | assert False 160 | except KeyError as e: 161 | assert e is not None and type(e) == KeyError 162 | 163 | def test_pixels_to_map_coords_4(self): 164 | gmp = GlobalMercatorProfile() 165 | point = PixelsPoint(0, 0) 166 | try: 167 | gmp.to_map_coordinates(point, foo=12) 168 | assert False 169 | except KeyError as e: 170 | assert e is not None and type(e) == KeyError 171 | 172 | def test_meters_to_map_coords_1(self): 173 | gmp = GlobalMercatorProfile() 174 | point = MetersPoint(41, 182) 175 | result = gmp.to_map_coordinates(point) 176 | assert point.mx == result.mx and \ 177 | point.my == result.my 178 | 179 | def test_tile_to_map_coordinates_1(self): 180 | gmp = GlobalMercatorProfile() 181 | tile = Tile(1, 2) 182 | try: 183 | gmp.to_map_coordinates(tile) 184 | assert False 185 | except NotImplementedError as e: 186 | assert e is not None and type(e) == NotImplementedError 187 | 188 | def test_meters_to_lon_lat_1(self): 189 | gmp = GlobalMercatorProfile() 190 | point = MetersPoint(20037508.342789244, 20037508.342789244) 191 | result = gmp.to_lon_lat(point) 192 | assert result.lon == 180.0 and \ 193 | result.lat == 85.0511287798066 194 | 195 | def test_lon_lat_to_lon_lat_1(self): 196 | gmp = GlobalMercatorProfile() 197 | point = LonLatPoint(123.4567890, 98.7654321) 198 | result = gmp.to_lon_lat(point) 199 | assert point.lon == result.lon and \ 200 | point.lat == result.lat 201 | 202 | def test_pixels_to_lon_lat_1(self): 203 | gmp = GlobalMercatorProfile() 204 | point = PixelsPoint(1, 2) 205 | try: 206 | gmp.to_lon_lat(point) 207 | assert False 208 | except NotImplementedError as e: 209 | assert e is not None and type(e) == NotImplementedError 210 | 211 | def test_meters_to_pixels_1(self): 212 | gmp = GlobalMercatorProfile() 213 | point = MetersPoint(20037508.342789244, 20037508.342789244) 214 | result = gmp.to_pixels(point, zoom=0) 215 | assert result.x == 256 and result.y == 256 216 | 217 | def test_meters_to_pixels_2(self): 218 | gmp = GlobalMercatorProfile() 219 | point = MetersPoint(-20037508.342789244, -20037508.342789244) 220 | result = gmp.to_pixels(point, zoom=0) 221 | assert result.x == 0 and result.y == 0 222 | 223 | def test_meters_to_pixels_3(self): 224 | gmp = GlobalMercatorProfile() 225 | point = MetersPoint(-20037508.342789244, -20037508.342789244) 226 | try: 227 | gmp.to_pixels(point) 228 | assert False 229 | except KeyError as e: 230 | assert e is not None and type(e) == KeyError 231 | 232 | def test_meters_to_pixels_4(self): 233 | gmp = GlobalMercatorProfile() 234 | point = MetersPoint(-20037508.342789244, -20037508.342789244) 235 | try: 236 | gmp.to_pixels(point, foo=12) 237 | assert False 238 | except KeyError as e: 239 | assert e is not None and type(e) == KeyError 240 | 241 | def test_pixels_to_pixels_1(self): 242 | gmp = GlobalMercatorProfile() 243 | point = PixelsPoint(1, 2) 244 | result = gmp.to_pixels(point, zoom=12) 245 | assert point.x == result.x and point.y == result.y 246 | 247 | def test_lon_lat_to_pixels_1(self): 248 | gmp = GlobalMercatorProfile() 249 | point = LonLatPoint(123.4567890, 98.7654321) 250 | try: 251 | gmp.to_pixels(point, zoom=18) 252 | assert False 253 | except NotImplementedError as e: 254 | assert e is not None and type(e) == NotImplementedError 255 | 256 | def test_pixels_to_raster_1(self): 257 | gmp = GlobalMercatorProfile() 258 | point = PixelsPoint(256, 256) 259 | [x, y] = gmp.to_raster(point, zoom=0) 260 | assert x == 256 and y == 0 261 | 262 | def test_pixels_to_raster_2(self): 263 | gmp = GlobalMercatorProfile() 264 | point = PixelsPoint(512, 0) 265 | [x, y] = gmp.to_raster(point, zoom=1) 266 | assert x == 512 and y == 512 267 | 268 | def test_pixels_to_raster_3(self): 269 | gmp = GlobalMercatorProfile() 270 | point = PixelsPoint(512, 0) 271 | try: 272 | gmp.to_raster(point, foo=1) 273 | assert False 274 | except KeyError as e: 275 | assert e is not None and type(e) == KeyError 276 | 277 | def test_lon_lat_to_raster_1(self): 278 | gmp = GlobalMercatorProfile() 279 | point = LonLatPoint(180.0, 90.0) 280 | try: 281 | gmp.to_raster(point, zoom=0) 282 | assert False 283 | except NotImplementedError as e: 284 | assert e is not None and type(e) == NotImplementedError 285 | -------------------------------------------------------------------------------- /Testing/test_tiles2gpkg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | """ 3 | Copyright (C) 2014 Reinventing Geospatial, Inc. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see , 17 | or write to the Free Software Foundation, Inc., 59 Temple Place - 18 | Suite 330, Boston, MA 02111-1307, USA. 19 | 20 | Authors: 21 | Steven D. Lander 22 | Jason Hall 23 | Jenifer Cochran 24 | Date: 2014-08 25 | Requires: Python Imaging Library (PIL or Pillow), Sqlite3 26 | Description: Test cases for tiles2gpkg_parallel.py 27 | 28 | Version: 29 | """ 30 | 31 | from math import pi 32 | from os import chdir 33 | from os import getcwd 34 | from os import listdir 35 | from os import mkdir 36 | from os import remove 37 | from os import walk 38 | 39 | from os.path import abspath 40 | from os.path import join 41 | from random import randint 42 | from sqlite3 import Binary 43 | from sys import path 44 | from sys import version_info 45 | if version_info[0] == 3: 46 | xrange = range 47 | from tempfile import gettempdir 48 | from uuid import uuid4 49 | 50 | from PIL import ImageDraw 51 | from PIL.Image import new 52 | from PIL.Image import open as iopen 53 | 54 | from pytest import raises 55 | 56 | path.append(abspath("Packaging")) 57 | from tiles2gpkg_parallel import EllipsoidalMercator 58 | from tiles2gpkg_parallel import Geodetic 59 | from tiles2gpkg_parallel import Geopackage 60 | from tiles2gpkg_parallel import Mercator 61 | from tiles2gpkg_parallel import ScaledWorldMercator 62 | from tiles2gpkg_parallel import TempDB 63 | from tiles2gpkg_parallel import ZoomMetadata 64 | from tiles2gpkg_parallel import allocate 65 | from tiles2gpkg_parallel import build_lut 66 | from tiles2gpkg_parallel import combine_worker_dbs 67 | from tiles2gpkg_parallel import file_count 68 | from tiles2gpkg_parallel import img_has_transparency 69 | from tiles2gpkg_parallel import img_to_buf 70 | from tiles2gpkg_parallel import split_all 71 | from tiles2gpkg_parallel import sqlite_worker 72 | from tiles2gpkg_parallel import worker_map 73 | 74 | GEODETIC_FILE_PATH = join(getcwd(), "rgb_tiles", "geodetic") 75 | MERCATOR_FILE_PATH = join(getcwd(), "Testing", "rgb_tiles", "mercator") 76 | 77 | # testing commands: 78 | # py.test --cov-report term-missing \ 79 | # --cov tiles2gpkg_parallel test_tiles2gpkg.py 80 | 81 | 82 | class TestMercator: 83 | 84 | """Test the Mercator object.""" 85 | 86 | def test_tile_size(self): 87 | """Test tile size default.""" 88 | merc = Mercator() 89 | assert merc.tile_size == 256 90 | 91 | def test_radius(self): 92 | """Test radius for mercator.""" 93 | merc = Mercator() 94 | assert merc.radius == 6378137 95 | 96 | def test_origin_shift(self): 97 | """Test origin shift calculation.""" 98 | merc = Mercator() 99 | assert merc.origin_shift == pi * merc.radius 100 | 101 | def test_init_res(self): 102 | """Test initial resolution calculation.""" 103 | merc = Mercator() 104 | assert merc.initial_resolution == 2 * \ 105 | merc.origin_shift / merc.tile_size 106 | 107 | def test_invert_y_one(self): 108 | """Test inverted Y axis calculation.""" 109 | z = 1 110 | y = 0 111 | assert Mercator.invert_y(z, y) == 1 112 | 113 | def test_invert_y_two(self): 114 | """Test a more complicated Y axis inversion.""" 115 | z = 13 116 | y = 31 117 | assert Mercator.invert_y(z, y) == 8160 118 | 119 | def test_pixel_size(self): 120 | """Test pixel size calculation.""" 121 | z = randint(0, 21) 122 | result = Mercator.pixel_size(z) 123 | assert result * 2**z == 156543.033928041 124 | 125 | def test_tile_to_lat_lon_one(self): 126 | """Test conversion from tile coordinate to lat/lon.""" 127 | z = x = y = 0 128 | lat, lon = Mercator.tile_to_lat_lon(z, x, y) 129 | assert lon == -180.0 and lat == -85.0511287798066 130 | 131 | def test_tile_to_lat_lon_two(self): 132 | """Test the top right corner of zoom 0.""" 133 | z = 0 134 | x = y = 1 135 | lat, lon = Mercator.tile_to_lat_lon(z, x, y) 136 | assert lon == 180.0 and lat == 85.0511287798066 137 | z = 14 138 | x = 11332 139 | y = 9870 140 | lat, lon = Mercator.tile_to_lat_lon(z, x, y) 141 | assert lon == 68.994140625 and \ 142 | lat == 34.56085936708385 143 | 144 | def test_tile_to_lat_lon_four(self): 145 | """Test a random tile to lat/lon.""" 146 | z = 14 147 | x = 11333 148 | y = 9871 149 | lat, lon = Mercator.tile_to_lat_lon(z, x, y) 150 | assert lon == 69.01611328125 and \ 151 | lat == 34.57895241036947 152 | 153 | def test_tile_to_meters_one(self): 154 | """Test conversion from tile coordinate to meters.""" 155 | merc = Mercator() 156 | z = x = y = 0 157 | mx, my = merc.tile_to_meters(z, x, y) 158 | mx = merc.truncate(mx) 159 | my = merc.truncate(my) 160 | assert float(mx) == -20037508.34 and \ 161 | float(my) == -20037508.34 162 | 163 | def test_tile_to_meters_two(self): 164 | """Test conversion with a different tile coord to meters.""" 165 | merc = Mercator() 166 | z = 0 167 | x = y = 1 168 | mx, my = merc.tile_to_meters(z, x, y) 169 | mx = merc.truncate(mx) 170 | my = merc.truncate(my) 171 | assert float(mx) == 20037508.34 and \ 172 | float(my) == 20037508.34 173 | 174 | def test_tile_to_meters_three(self): 175 | """Test a random tile to meters.""" 176 | merc = Mercator() 177 | z = 14 178 | x = 11332 179 | y = 9870 180 | mx, my = merc.tile_to_meters(z, x, y) 181 | mx = merc.truncate(mx) 182 | my = merc.truncate(my) 183 | assert float(mx) == 7680392.60 and \ 184 | float(my) == 4104362.67 185 | 186 | def test_tile_to_meters_four(self): 187 | """Test another corner of a random tile to meters.""" 188 | merc = Mercator() 189 | z = 14 190 | x = 11333 191 | y = 9871 192 | mx, my = merc.tile_to_meters(z, x, y) 193 | mx = merc.truncate(mx) 194 | my = merc.truncate(my) 195 | assert float(mx) == 7682838.58 and \ 196 | float(my) == 4106808.65 197 | 198 | def test_truncate(self): 199 | """Test mercator accuracy truncation.""" 200 | merc = Mercator() 201 | f = 1234.567890123 202 | result = merc.truncate(f) 203 | assert float(result) == 1234.56 204 | 205 | def test_get_coord_one(self): 206 | """Test get coordinate from tile method.""" 207 | merc = Mercator() 208 | z = x = y = 0 209 | mx, my = merc.get_coord(z, x, y) 210 | mx = merc.truncate(mx) 211 | my = merc.truncate(my) 212 | assert float(mx) == -20037508.34 and \ 213 | float(my) == -20037508.34 214 | 215 | def test_get_coord_two(self): 216 | """Test get coord with a random tile.""" 217 | merc = Mercator() 218 | z = 14 219 | x = 11332 220 | y = 9870 221 | mx, my = merc.get_coord(z, x, y) 222 | mx = merc.truncate(mx) 223 | my = merc.truncate(my) 224 | assert float(mx) == 7680392.60 and \ 225 | float(my) == 4104362.67 226 | 227 | 228 | class TestGeodetic: 229 | 230 | """Test the Geodetic object.""" 231 | 232 | def test_tile_size(self): 233 | geod = Geodetic() 234 | assert geod.tile_size == 256 235 | 236 | def test_res_fact(self): 237 | geod = Geodetic() 238 | assert geod.resolution_factor == 360.0 / geod.tile_size 239 | 240 | def test_pixel_size(self): 241 | geod = Geodetic() 242 | z = randint(0, 21) 243 | result = geod.pixel_size(z) 244 | assert 2**z * result == geod.resolution_factor 245 | 246 | def test_get_coord_one(self): 247 | geod = Geodetic() 248 | z = 1 249 | x = y = 0 250 | lon, lat = geod.get_coord(z, x, y) 251 | assert lon == -180.0 and lat == -90.0 252 | 253 | def test_get_coord_two(self): 254 | geod = Geodetic() 255 | z = x = y = 1 256 | lon, lat = geod.get_coord(z, x, y) 257 | assert lon == 0.0 and lat == 90.0 258 | 259 | def test_get_coord_three(self): 260 | geod = Geodetic() 261 | z = 21 262 | x = 599187 263 | y = 749974 264 | lon, lat = geod.get_coord(z, x, y) 265 | lon = geod.truncate(lon) 266 | lat = geod.truncate(lat) 267 | assert float(lon) == -77.1427345 and \ 268 | float(lat) == 38.7415695 269 | 270 | def test_get_coord_four(self): 271 | geod = Geodetic() 272 | z = 21 273 | x = 599188 274 | y = 749975 275 | lon, lat = geod.get_coord(z, x, y) 276 | lon = geod.truncate(lon) 277 | lat = geod.truncate(lat) 278 | assert float(lon) == -77.1425628 and \ 279 | float(lat) == 38.7417411 280 | 281 | def test_invert_y_one(self): 282 | z = 2 283 | y = 0 284 | assert Geodetic.invert_y(z, y) == 1 285 | 286 | def test_invert_y_two(self): 287 | z = y = 0 288 | assert Geodetic.invert_y(z, y) == 0 289 | 290 | def test_truncate(self): 291 | x = 12.34567890 292 | result = Geodetic.truncate(x) 293 | assert float(result) == 12.3456789 294 | 295 | 296 | class TestEllipsoidalMercator: 297 | 298 | """Test the Ellipsoidal Mercator object.""" 299 | 300 | def test_lat_to_northing(self): 301 | # TODO: need input from Micah 302 | return True 303 | 304 | def test_tile_to_meters_one(self): 305 | ellip = EllipsoidalMercator() 306 | z = x = y = 0 307 | mx, my = ellip.tile_to_meters(z, x, y) 308 | mx = ellip.truncate(mx) 309 | my = ellip.truncate(my) 310 | assert float(mx) == -20037508.34 and \ 311 | float(my) == -20037508.34 312 | 313 | def test_tile_to_meters_two(self): 314 | ellip = EllipsoidalMercator() 315 | z = 0 316 | x = y = 1 317 | mx, my = ellip.tile_to_meters(z, x, y) 318 | mx = ellip.truncate(mx) 319 | my = ellip.truncate(my) 320 | assert float(mx) == 20037508.34 and \ 321 | float(my) == 20037508.34 322 | 323 | def test_tile_to_lat_lon_one(self): 324 | z = x = y = 0 325 | lat, lon = EllipsoidalMercator.tile_to_lat_lon(z, x, y) 326 | assert lon == -180.0 and \ 327 | lat == -85.08405904978349 328 | 329 | def test_tile_to_lat_lon_two(self): 330 | z = 0 331 | x = y = 1 332 | lat, lon = EllipsoidalMercator.tile_to_lat_lon(z, x, y) 333 | assert lon == 180.0 and \ 334 | lat == 85.08405904978349 335 | 336 | def test_tile_to_lat_lon_three(self): 337 | # Visually verified that this tile 338 | # should be near Galva, Illinois 339 | z = 3 340 | x = 2 341 | y = 5 342 | lat, lon = EllipsoidalMercator.tile_to_lat_lon(z, x, y) 343 | assert lat == 41.170427276143315 and \ 344 | lon == -90.0 345 | 346 | def test_tile_to_lat_lon_four(self): 347 | # Visually verified to show an 348 | # Antarctic tile 349 | z = 7 350 | x = 114 351 | y = 31 352 | lat, lon = EllipsoidalMercator.tile_to_lat_lon(z, x, y) 353 | assert lat == -67.7443134783405 and \ 354 | lon == 140.625 355 | 356 | def test_tile_to_lat_lon_five(self): 357 | # AGC 358 | z = 17 359 | x = 37448 360 | y = 80770 361 | lat, lon = EllipsoidalMercator.tile_to_lat_lon(z, x, y) 362 | assert lat == 38.74009055509699 and \ 363 | lon == -77.14599609375 364 | 365 | 366 | class TestScaledWorldMercator: 367 | 368 | """Test the Scaled World Mercator object.""" 369 | 370 | def test_lat_to_northing(self): 371 | # TODO: Input needed from Micah 372 | return True 373 | 374 | def test_pixel_size(self): 375 | z = randint(0, 21) 376 | assert 2**z * ScaledWorldMercator.pixel_size(z) == 125829.12 377 | 378 | def test_tile_to_lat_lon_one(self): 379 | z = x = y = 0 380 | lat, lon = ScaledWorldMercator.tile_to_lat_lon(z, x, y) 381 | lat_result = Geodetic.truncate(lat) 382 | assert float(lat_result) == -85.0840590 and \ 383 | lon == -180.0 384 | 385 | def test_tile_to_lat_lon_two(self): 386 | z = 0 387 | x = y = 1 388 | lat, lon = ScaledWorldMercator.tile_to_lat_lon(z, x, y) 389 | lat_result = Geodetic.truncate(lat) 390 | assert float(lat_result) == 85.0840590 and \ 391 | lon == 180.0 392 | 393 | def test_tile_to_meters_one(self): 394 | # TODO: this should return -16106127.36 395 | scal = ScaledWorldMercator() 396 | z = x = y = 0 397 | expected = -17179869.18 398 | mx, my = scal.tile_to_meters(z, x, y) 399 | mx = ScaledWorldMercator.truncate(mx) 400 | my = round(my, 2) 401 | assert float(mx) == expected and float(my) == expected 402 | 403 | 404 | class TestZoomMetadata: 405 | 406 | """Test the ZoomMetadata object.""" 407 | 408 | def test_zoom_meta_data_zoom(self): 409 | zmd = make_zmd() 410 | assert zmd.zoom is 1 411 | 412 | def test_zoom_meta_data_min_tile_col(self): 413 | zmd = make_zmd() 414 | assert zmd.min_tile_col is 1 415 | 416 | def test_zoom_meta_data_min_tile_row(self): 417 | zmd = make_zmd() 418 | assert zmd.min_tile_row is 1 419 | 420 | def test_zoom_meta_data_min_x(self): 421 | zmd = make_zmd() 422 | assert zmd.min_x is 1 423 | 424 | def test_zoom_meta_data_min_y(self): 425 | zmd = make_zmd() 426 | assert zmd.min_y is 1 427 | 428 | def test_zoom_meta_data_max_tile_col(self): 429 | zmd = make_zmd() 430 | assert zmd.max_tile_col is 2 431 | 432 | def test_zoom_meta_data_max_tile_row(self): 433 | zmd = make_zmd() 434 | assert zmd.max_tile_row is 2 435 | 436 | def test_zoom_meta_data_max_x(self): 437 | zmd = make_zmd() 438 | assert zmd.max_x is 2 439 | 440 | def test_zoom_meta_data_max_y(self): 441 | zmd = make_zmd() 442 | assert zmd.max_y is 2 443 | 444 | 445 | class Testgeopackage: 446 | 447 | """Test the Geopackage object.""" 448 | 449 | def test_file_path(self): 450 | filename = uuid4().hex + '.gpkg' 451 | tmp_dir = gettempdir() 452 | tmp_file = join(tmp_dir, filename) 453 | gpkg = Geopackage(tmp_file, 3857) 454 | assert gpkg.file_path == tmp_file 455 | 456 | def test_assimilate_error(self): 457 | session_folder = make_session_folder() 458 | chdir(session_folder) 459 | with Geopackage(session_folder, 3395) as gpkg: 460 | with raises(IOError): 461 | gpkg.assimilate("None") 462 | remove(join(getcwd(), gpkg.file_path)) 463 | 464 | def test_execute_return(self): 465 | session_folder = make_session_folder() 466 | chdir(session_folder) 467 | gpkg = Geopackage(session_folder, 9804) 468 | result = gpkg.execute("select count(*) from tiles;") 469 | assert (result.fetchone())[0] == 0 470 | 471 | def test_execute_with_inputs(self): 472 | gpkg = make_gpkg() 473 | test_statement = """ 474 | UPDATE gpkg_contents SET 475 | min_x = ?, 476 | min_y = ?, 477 | max_x = ?, 478 | max_y = ? 479 | WHERE table_name = 'tiles'; 480 | """ 481 | result = gpkg.execute(test_statement, (1, 1, 2, 2)) 482 | assert result.fetchone() is None 483 | 484 | def test_update_metadata(self): 485 | zmd_list = [] 486 | for _ in xrange(5): 487 | zmd_list.append(make_zmd()) 488 | gpkg = make_gpkg() 489 | gpkg.update_metadata(zmd_list) 490 | cursor = gpkg.execute("select min_x from gpkg_contents") 491 | assert cursor.fetchone()[0] == 1.0 492 | 493 | def test_matrix_width(self): 494 | test_width_stmt = """ 495 | SELECT matrix_width 496 | FROM gpkg_tile_matrix 497 | WHERE zoom_level is ?; 498 | """ 499 | gpkg = make_gpkg() 500 | gpkg.update_metadata(make_zmd_list_geodetic()) 501 | for zoom in xrange(2, 6): 502 | (result,) = gpkg.execute(test_width_stmt, (zoom,)) 503 | width = (2**zoom) 504 | if result[0] != width: 505 | print(zoom, result[0], width) 506 | assert False 507 | assert True 508 | 509 | def test_matrix_height(self): 510 | test_height_stmt = """ 511 | SELECT matrix_height 512 | FROM gpkg_tile_matrix 513 | WHERE zoom_level is ?; 514 | """ 515 | gpkg = make_gpkg() 516 | gpkg.update_metadata(make_zmd_list_geodetic()) 517 | for zoom in xrange(2, 6): 518 | (result,) = gpkg.execute(test_height_stmt, (zoom,)) 519 | height = (2**(zoom-1)) 520 | if result[0] != height: 521 | print(zoom, result[0], height) 522 | assert False 523 | assert True 524 | 525 | 526 | class TestTempDB: 527 | 528 | """Test the TempDB object.""" 529 | 530 | def __make_tempDB(self): 531 | chdir(gettempdir()) 532 | temp_folder = uuid4().hex 533 | mkdir(temp_folder) 534 | return TempDB(temp_folder) 535 | 536 | def test_insert_image_blob(self): 537 | img = new("RGB", (256, 256), "red") 538 | data = img_to_buf(img, 'jpeg').read() 539 | tempDB = self.__make_tempDB() 540 | tempDB.insert_image_blob(0, 0, 0, Binary(data)) 541 | result = tempDB.execute("select count(*) from tiles;") 542 | assert result.fetchone()[0] == 1 543 | 544 | 545 | class TestImgToBuf: 546 | 547 | """Test the img_to_buf method.""" 548 | 549 | def test_img_to_buf_jpg(self): 550 | img = new("RGB", (256, 256), "red") 551 | data = img_to_buf(img, 'jpeg').read() 552 | # a 'JFIF' chunk in the bitstream indicates a .jpg image 553 | assert b'JFIF' in data 554 | 555 | def test_img_to_buf_png(self): 556 | img = new("RGB", (256, 256), "red") 557 | img.save("test1.png", 'PNG') 558 | data = img_to_buf(img, 'png').read() 559 | # ImageHeaDeR, ImageDATa, and ImageEND are 560 | # all necessary chunks in a .PNG bitstream 561 | assert b'IHDR' in data and b'IDAT' in data and b'IEND' in data 562 | 563 | def test_img_to_buf_source(self): 564 | img = new("RGB", (256, 256), "red") 565 | img.save("test2.jpg") 566 | img.format = "JPEG" 567 | data = img_to_buf(img, 'source').read() 568 | # a 'JFIF' chunk in the bitstream indicates a .jpg image 569 | assert b'JFIF' in data 570 | 571 | 572 | class TestImgHasTransparency: 573 | 574 | """Test the img_has_transparency method.""" 575 | def test_not_transparent(self): 576 | img = new("RGB", (256, 256), "red") 577 | assert img_has_transparency(img) == 0 578 | 579 | def test_partially_transparent(self): 580 | img = new("RGBA", (256, 256)) 581 | draw = ImageDraw.Draw(img) 582 | draw.ellipse((96, 96, 160, 160), fill=(255, 0, 0)) 583 | assert img_has_transparency(img) > 0 584 | 585 | def test_fully_transparent(self): 586 | img = new('RGBA', (256, 256)) 587 | assert img_has_transparency(img) == -1 588 | 589 | def test_paletted_image_transparent(self): 590 | img = new("P", (256, 256), 0) 591 | img.save("test1.png", "PNG", transparency=b'\x00') 592 | img = iopen("test1.png", "r") 593 | assert img_has_transparency(img) 594 | 595 | def test_paletted_image_not_transparent(self): 596 | img = new('P', (256, 256)) 597 | assert img_has_transparency(img) == 0 598 | 599 | 600 | def test_file_count(): 601 | assert len(file_count(MERCATOR_FILE_PATH)) == 4 602 | 603 | 604 | def test_split_all(): 605 | coords = ["1", "2", "3.png"] 606 | file_path = join(getcwd(), "data", coords[0], coords[1], coords[2]) 607 | result = split_all(file_path) 608 | assert result['z'] == int(coords[0]) and \ 609 | result['x'] == int(coords[1]) and \ 610 | result['y'] == int(coords[2].split(".")[0]) and \ 611 | result['path'] == file_path 612 | 613 | 614 | def test_worker_map(): 615 | session_folder = make_session_folder() 616 | tempdb = TempDB(session_folder) 617 | tile_dict = make_mercator_filelist()[0] 618 | tile_info = [make_zmd(), make_zmd()] 619 | imagery = 'mixed' 620 | invert_y = None 621 | extra_args = dict(tile_info=tile_info, imagery=imagery, jpeg_quality=75) 622 | worker_map(tempdb, tile_dict, extra_args, invert_y) 623 | chdir(session_folder) 624 | files = listdir(getcwd()) 625 | # assert the worker_map function put the .gpkg.part file into the db 626 | assert len(files) == 1 and '.gpkg.part' in files[0] 627 | 628 | 629 | class testsqliteworker: 630 | 631 | """Test the sqlite_worker function.""" 632 | 633 | def test_sqlite_worker_4326(self): 634 | session_folder = make_session_folder() 635 | file_list = make_geodetic_filelist() 636 | metadata = build_lut(file_list, True, 4326) 637 | extra_args = dict(root_dir=session_folder, tile_info=metadata, 638 | lower_left=True, srs=4326, imagery='mixed', 639 | jpeg_quality=75) 640 | sqlite_worker(file_list, extra_args) 641 | # assert that worker put the .gpkg.part file into base_dir 642 | files = listdir(session_folder) 643 | assert len(files) == 1 and '.gpkg.part' in files[0] 644 | 645 | def test_sqlite_worker_3857(self): 646 | session_folder = make_session_folder() 647 | file_list = make_geodetic_filelist() 648 | metadata = build_lut(file_list, True, 3857) 649 | extra_args = dict(root_dir=session_folder, tile_info=metadata, 650 | lower_left=True, srs=3857, imagery='mixed', 651 | jpeg_quality=75) 652 | sqlite_worker(file_list, extra_args) 653 | files = listdir(session_folder) 654 | assert len(files) == 1 and '.gpkg.part' in files[0] 655 | 656 | def test_sqlite_worker_3395(self): 657 | session_folder = make_session_folder() 658 | file_list = make_geodetic_filelist() 659 | metadata = build_lut(file_list, True, 3395) 660 | extra_args = dict(root_dir=session_folder, tile_info=metadata, 661 | lower_left=True, srs=3395, imagery='mixed', 662 | jpeg_quality=75) 663 | sqlite_worker(file_list, extra_args) 664 | files = listdir(session_folder) 665 | assert len(files) == 1 and '.gpkg.part' in files[0] 666 | 667 | def test_sqlite_worker_9804(self): 668 | session_folder = make_session_folder() 669 | file_list = make_geodetic_filelist() 670 | metadata = build_lut(file_list, True, 9804) 671 | extra_args = dict(root_dir=session_folder, tile_info=metadata, 672 | lower_left=True, srs=9804, imagery='mixed', 673 | jpeg_quality=75) 674 | sqlite_worker(file_list, extra_args) 675 | files = listdir(session_folder) 676 | assert len(files) == 1 and '.gpkg.part' in files[0] 677 | 678 | 679 | class testallocate: 680 | class MockPool: 681 | def __init__(self): 682 | self.works = True 683 | 684 | def apply_async(self, worker): 685 | raise TypeError("success") 686 | 687 | def test_allocate_one(self): 688 | cores = 4 689 | file_list = [1, 2] 690 | cpu_pool = self.mockpool() 691 | base_dir = gettempdir() 692 | extra_args = dict(root_dir=base_dir, tile_info=make_zmd(), 693 | lower_left=True, srs=4326, imagery='mixed') 694 | e = None 695 | try: 696 | allocate(cores, cpu_pool, file_list, extra_args) 697 | except TypeError as e: 698 | print('success') 699 | else: 700 | assert e is not None and type(e) == TypeError 701 | 702 | 703 | class testbuildlut: 704 | 705 | """Test the build_lut function.""" 706 | 707 | def test_build_lut_scaled_world_mercator(self): 708 | result = build_lut(make_mercator_filelist(), False, 9804) 709 | assert result[0].zoom == 1 710 | 711 | def test_build_lut_ellipsoidal_mercator(self): 712 | result = build_lut(make_mercator_filelist(), False, 3395) 713 | assert result[0].zoom == 1 714 | 715 | def test_build_lut_mercator(self): 716 | result = build_lut(make_mercator_filelist(), False, 3857) 717 | assert result[0].zoom == 1 718 | 719 | def test_build_lut_upper_left(self): 720 | result = build_lut(make_geodetic_filelist(), False, 4326) 721 | assert result[1].zoom == 2 722 | 723 | def test_build_lut_one(self): 724 | result = build_lut(make_geodetic_filelist(), True, 4326) 725 | assert result[1].zoom == 2 726 | 727 | def test_build_lut_two(self): 728 | result = build_lut(make_geodetic_filelist(), True, 4326) 729 | assert result[0].min_tile_col == 0 730 | 731 | def test_build_lut_three(self): 732 | result = build_lut(make_geodetic_filelist(), True, 4326) 733 | assert result[0].min_tile_row == 0 734 | 735 | def test_build_lut_four(self): 736 | result = build_lut(make_geodetic_filelist(), True, 4326) 737 | assert result[0].min_x == -180.0 738 | 739 | def test_build_lut_five(self): 740 | result = build_lut(make_geodetic_filelist(), True, 4326) 741 | assert result[0].min_y == -90.0 742 | 743 | def test_build_lut_six(self): 744 | result = build_lut(make_geodetic_filelist(), True, 4326) 745 | assert result[1].max_tile_row == 1 746 | 747 | def test_build_lut_seven(self): 748 | result = build_lut(make_geodetic_filelist(), True, 4326) 749 | assert result[1].max_tile_col == 1 750 | 751 | def test_build_lut_eight(self): 752 | result = build_lut(make_geodetic_filelist(), True, 4326) 753 | assert result[0].max_x == 0.0 754 | 755 | def test_build_lut_nine(self): 756 | result = build_lut(make_geodetic_filelist(), True, 4326) 757 | assert result[0].max_y == 90.0 758 | 759 | 760 | def test_combine_worker_dbs(): 761 | session_folder = make_session_folder() 762 | # make a random number of tempdbs with dummy data 763 | img = new("RGB", (256, 256), "red") 764 | data = img_to_buf(img, 'jpeg').read() 765 | z = randint(2, 5) 766 | for x in xrange(z): 767 | TempDB(session_folder).insert_image_blob(x, 0, 0, Binary(data)) 768 | # confirm that combine_worker_dbs assimilates all tempdb's into gpkg 769 | chdir(session_folder) # necessary to put gpkg in session_folder 770 | gpkg = Geopackage("test.gpkg", 4326) 771 | combine_worker_dbs(gpkg) 772 | result = gpkg.execute("select count(*) from tiles;") 773 | assert (result.fetchone())[0] == z 774 | 775 | 776 | # todo: test main 777 | def test_main(): 778 | # chdir(gettempdir()) 779 | # parser = argumentparser(description="convert tms folder into geopackage") 780 | # parser.add_argument("source_folder", metavar="source") 781 | # parser.add_argument("output_file", metavar="dest") 782 | # parser.add_argument("-tileorigin", metavar="tile_origin", default="ll") 783 | # parser.add_argument("-srs", metavar="srs", default=3857) 784 | # parser.add_argument("-imagery", metavar="imagery", default="source") 785 | # parser.add_argument("-q", metavar="quality", type=int, default=75) 786 | # parser.add_argument("-t", dest="threading", action="store_false", 787 | # default=true) 788 | # source = geodetic_file_path 789 | # output_file = uuid4().hex + ".gpkg" 790 | # arg_list = parser.parse_args([source, output_file]) 791 | # main(arg_list) 792 | assert True 793 | 794 | 795 | def make_gpkg(): 796 | filename = uuid4().hex + '.gpkg' 797 | tmp_file = join(gettempdir(), filename) 798 | return Geopackage(tmp_file, 4326) 799 | 800 | 801 | def make_zmd(): 802 | zmd = ZoomMetadata() 803 | zmd.zoom = 1 804 | zmd.min_tile_col = 1 805 | zmd.min_tile_row = 1 806 | zmd.min_x = 1 807 | zmd.min_y = 1 808 | zmd.max_tile_col = 2 809 | zmd.max_tile_row = 2 810 | zmd.max_x = 2 811 | zmd.max_y = 2 812 | zmd.matrix_width = 2 813 | zmd.matrix_height = 2 814 | return zmd 815 | 816 | 817 | def make_zmd_list_geodetic(): 818 | """Make a geodetic zoom level metadata mock object.""" 819 | zmd_list = [] 820 | for zoom in xrange(2, 6): 821 | zmd = ZoomMetadata() 822 | zmd.zoom = zoom 823 | zmd.min_tile_row = 0 824 | zmd.min_x = 0 825 | zmd.min_tile_col = 0 826 | zmd.min_y = 0 827 | zmd.max_tile_row = (2**(zoom-1)) - 1 828 | zmd.max_x = zmd.max_tile_row 829 | zmd.max_tile_col = (2**zoom) - 1 830 | zmd.max_y = zmd.max_tile_col 831 | #zmd.matrix_width = (4 if zoom == 2 else zmd_list[zoom-3].matrix_width * 2) 832 | if zoom == 2: 833 | zmd.matrix_width = 4 834 | else: 835 | zmd.matrix_width = zmd_list[zoom-3].matrix_width * 2 836 | #zmd.matrix_height = (2 if zoom == 2 else zmd_list[zoom-3].matrix_height * 2) 837 | if zoom == 2: 838 | zmd.matrix_height = 2 839 | else: 840 | zmd.matrix_height = zmd_list[zoom-3].matrix_height * 2 841 | zmd_list.append(zmd) 842 | return zmd_list 843 | 844 | 845 | def make_mercator_filelist(): 846 | file_path = [] 847 | for root, sub_folders, files in walk(MERCATOR_FILE_PATH): 848 | file_path += [join(root, f) for f in files if f.endswith('.png')] 849 | d1 = dict(x=0, y=0, z=1, path=file_path[0]) 850 | d2 = dict(x=0, y=1, z=1, path=file_path[1]) 851 | d3 = dict(x=1, y=0, z=1, path=file_path[2]) 852 | d4 = dict(x=1, y=1, z=1, path=file_path[3]) 853 | return [d1, d2, d3, d4] 854 | 855 | 856 | def make_geodetic_filelist(): 857 | file_path = [] 858 | for root, sub_folders, files in walk(GEODETIC_FILE_PATH): 859 | file_path += [join(root, f) for f in files if f.endswith('.png')] 860 | d1 = dict(z=1, x=0, y=0, path=file_path[0]) 861 | d2 = dict(z=2, x=0, y=0, path=file_path[1]) 862 | d3 = dict(z=2, x=0, y=1, path=file_path[2]) 863 | d4 = dict(z=2, x=1, y=0, path=file_path[3]) 864 | d5 = dict(z=2, x=1, y=1, path=file_path[4]) 865 | return [d1, d2, d3, d4, d5] 866 | 867 | 868 | def make_session_folder(): 869 | session_folder = uuid4().hex 870 | chdir(gettempdir()) 871 | mkdir(session_folder) 872 | return session_folder 873 | -------------------------------------------------------------------------------- /Tools/generate_wms_aligned.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Author: Steven D. Lander, RGi 4 | # March 18, 2016 5 | 6 | # This script assumes world-referenced tile coordinates 7 | 8 | from collections import namedtuple 9 | 10 | TileRange = namedtuple("TileRange", ["min", "max"]) 11 | 12 | def tile_bounds_geodetic(z, x, y): 13 | """Shamelessly taken from gdal2tiles.py""" 14 | tile_size = 256 15 | res_fact = 360.0 / tile_size 16 | res = res_fact / 2**z 17 | def calc(axis, max): return axis * tile_size * res - max 18 | return (calc(x, 180), calc(y, 90), 19 | calc((x + 1), 180), calc((y + 1), 90)) 20 | 21 | def iterate_tiles(z, range_x, range_y, **kwargs): 22 | try: 23 | base_url = kwargs["base_url"] 24 | except(AttributeError, KeyError): 25 | base_url = "" 26 | if type(range_x) is not TileRange or \ 27 | type(range_y) is not TileRange: 28 | raise KeyError("z/x/y ranges must be TileRange objects") 29 | tmpl = "{}&BBOX={},{},{},{}" 30 | tile_urls = [] 31 | for x in range(range_x.min, range_x.max+1): 32 | for y in range(range_y.min, range_y.max+1): 33 | # convert tile into bbox 34 | bbox = tile_bounds_geodetic(z, x, y) 35 | tile_urls.append(tmpl.format(base_url, 36 | bbox[0], bbox[1], bbox[2], bbox[3])) 37 | return tile_urls 38 | 39 | # Print all these URLs out to the console, so they can 40 | # be piped to output if desired 41 | base_url = "http://localhost/GPEP/Hybrid-Performance-Test/service?" 42 | base_url += "VERSION=1.3.0&REQUEST=GetMap&CRS=CRS:84&WIDTH=256&HEIGHT=256" 43 | base_url += "&LAYERS=2,6,10,11,12&STYLES=,,,,&EXCEPTIONS=xml&FORMAT=image/jpeg" 44 | base_url += "&BGCOLOR=0xFEFFFF&TRANSPARENT=TRUE" 45 | tasks = [ 46 | (6, TileRange(32, 55), TileRange(16,27)), 47 | (7, TileRange(72, 83), TileRange(40, 43)), 48 | (8, TileRange(160, 175), TileRange(64, 67)), 49 | (9, TileRange(336, 355), TileRange(128, 139)), 50 | (10, TileRange(676, 703), TileRange(256, 271)), 51 | (11, TileRange(1364, 1387), TileRange(524, 539)), 52 | (12, TileRange(2744, 2751), TileRange(1056, 1067)), 53 | (13, TileRange(5496, 5519), TileRange(2120, 2127)), 54 | (14, TileRange(11012, 11027), TileRange(4244, 4255)), 55 | (16, TileRange(44088, 44119), TileRange(17012, 17015)), 56 | ] 57 | 58 | for task in tasks: 59 | level = iterate_tiles(task[0], task[1], task[2], base_url=base_url) 60 | for entry in level: 61 | print(entry) 62 | -------------------------------------------------------------------------------- /Tools/generate_wms_aligned_relative.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Author: Steven D. Lander, RGi 4 | # March 18, 2016 5 | 6 | # This script assumes world-referenced tile coordinates 7 | 8 | def gen_bbox(bbox, tiles): 9 | if tiles == 1: 10 | return ["&BBOX={},{},{},{}".format(bbox[0], 11 | bbox[1], bbox[2], bbox[3])] 12 | else: 13 | sub_count = tiles / 4 14 | min_x, min_y, max_x, max_y = bbox[0], bbox[1], bbox[2], bbox[3] 15 | middle_x = ((max_x - min_x) / 2) + min_x 16 | middle_y = ((max_y - min_y) / 2) + min_y 17 | bbox_ul = [min_x, middle_y, middle_x, max_y] 18 | bbox_ur = [middle_x, middle_y, max_x, max_y] 19 | bbox_ll = [min_x, min_y, middle_x, middle_y] 20 | bbox_lr = [middle_x, min_y, max_x, middle_y] 21 | return gen_bbox(bbox_ul, sub_count) + \ 22 | gen_bbox(bbox_ur, sub_count) + \ 23 | gen_bbox(bbox_ll, sub_count) + \ 24 | gen_bbox(bbox_lr, sub_count) 25 | 26 | base_url = "http://localhost/GPEP/Hybrid-Performance-Test/service?" 27 | base_url += "VERSION=1.3.0&REQUEST=GetMap&CRS=CRS:84&WIDTH=256&HEIGHT=256" 28 | base_url += "&LAYERS=2&STYLES=,,,,&EXCEPTIONS=xml&FORMAT=image/png" 29 | base_url += "&BGCOLOR=0xFEFFFF&TRANSPARENT=TRUE" 30 | z_min = 0 31 | z_max = 7 32 | tasks = [(2**x) * (2**x) for x in range(z_min, z_max+1)] 33 | bbox = [50.92, 20.63, 78.12, 41.62] 34 | 35 | bbox_list = [] 36 | for task in tasks: 37 | for entry in gen_bbox(bbox, task): 38 | print(base_url + entry) 39 | -------------------------------------------------------------------------------- /Tools/generate_wmts_urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Author: Steven D. Lander, RGi 4 | # March 17, 2016 5 | 6 | from collections import namedtuple 7 | 8 | TileRange = namedtuple("TileRange", ["min", "max"]) 9 | 10 | def iterate_tiles(z, range_x, range_y, **kwargs): 11 | try: 12 | base_url = kwargs["base_url"] 13 | except(AttributeError, KeyError): 14 | base_url = "" 15 | try: 16 | file_ext = kwargs["file_ext"] 17 | if "." not in file_ext: 18 | raise ValueError("file_ext needs a period in it.") 19 | except(AttributeError, KeyError): 20 | file_ext = "" 21 | except(ValueError): 22 | raise 23 | if type(range_x) is not TileRange or \ 24 | type(range_y) is not TileRange: 25 | raise KeyError("z/x/y ranges must be TileRange objects") 26 | tmpl = "{}{}/{}/{}{}" 27 | tile_urls = [] 28 | for x in range(range_x.min, range_x.max+1): 29 | for y in range(range_y.min, range_y.max+1): 30 | tile_urls.append(tmpl.format(base_url, z, x, y, file_ext)) 31 | return tile_urls 32 | 33 | # Print all these URLs out to the console, so they can 34 | # be piped to output if desired 35 | base_url = "http://example/url/" 36 | file_ext = ".png" 37 | tasks = [ 38 | (1, TileRange(0, 3), TileRange(0, 3)), # All zoom 1 tiles (EPSG:3857) 39 | # (7, TileRange(0, 2**7), TileRange(0, 2**(7-1))), # All zoom 7 tiles (EPSG:4326) 40 | # (16, TileRange(0, 2**16), TileRange(0, 2**16)), # All zoom 16 tiles (EPSG:3857) 41 | ] 42 | 43 | for task in tasks: 44 | level = iterate_tiles(task[0], task[1], task[2], base_url=base_url, 45 | file_ext=file_ext) 46 | for entry in level: 47 | print(entry) 48 | -------------------------------------------------------------------------------- /dependencies.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | pytest 3 | pytest-cov 4 | --------------------------------------------------------------------------------