├── .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 | [](https://travis-ci.org/GitHubRGI/geopackage-python)
8 | [](https://coveralls.io/r/GitHubRGI/geopackage-python)
9 | [](https://waffle.io/GitHubRGI/geopackage-python)
10 | [](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 |
--------------------------------------------------------------------------------