├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── assets └── example1.png ├── pyproject.toml ├── setup.py └── src └── qtiles ├── __init__.py ├── aboutdialog.py ├── compat.py ├── i18n └── qtiles_ru_RU.ts ├── icons ├── about.png ├── info.png ├── nextgis_logo.svg ├── ngm_index_24x24.png └── qtiles.png ├── mbutils.py ├── metadata.txt ├── qtiles.py ├── qtiles_utils.py ├── qtilesdialog.py ├── resources.qrc ├── resources ├── css │ ├── images │ │ └── layers.png │ ├── jquery-ui.min.css │ └── leaflet.css ├── js │ ├── images │ │ └── ui-bg_flat_75_ffffff_40x100.png │ ├── jquery-ui.min.js │ ├── jquery.min.js │ └── leaflet.js └── viewer.html ├── tile.py ├── tilingthread.py ├── ui ├── __init__.py ├── aboutdialogbase.ui └── qtilesdialogbase.ui └── writers.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | # C++ objects and libs 165 | *.slo 166 | *.lo 167 | *.o 168 | *.a 169 | *.la 170 | *.lai 171 | *.so 172 | *.so.* 173 | *.dll 174 | *.dylib 175 | 176 | # Qt-es 177 | object_script.*.Release 178 | object_script.*.Debug 179 | *_plugin_import.cpp 180 | /.qmake.cache 181 | /.qmake.stash 182 | *.pro.user 183 | *.pro.user.* 184 | *.qbs.user 185 | *.qbs.user.* 186 | *.moc 187 | moc_*.cpp 188 | moc_*.h 189 | qrc_*.cpp 190 | ui_*.h 191 | *.qmlc 192 | *.jsc 193 | Makefile* 194 | *build-* 195 | *.qm 196 | *.prl 197 | 198 | # Qt unit tests 199 | target_wrapper.* 200 | 201 | # QtCreator 202 | *.autosave 203 | 204 | # QtCreator Qml 205 | *.qmlproject.user 206 | *.qmlproject.user.* 207 | 208 | # QtCreator CMake 209 | CMakeLists.txt.user* 210 | 211 | # QtCreator 4.8< compilation database 212 | compile_commands.json 213 | 214 | # QtCreator local machine specific files for imported projects 215 | *creator.user* 216 | 217 | *_qmlcache.qrc 218 | 219 | # PyQt 220 | ui_*.py 221 | *_rc.py 222 | 223 | # QGIS 224 | *.db 225 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-toml 6 | 7 | - repo: https://github.com/astral-sh/ruff-pre-commit 8 | rev: v0.6.9 9 | hooks: 10 | - id: ruff 11 | args: [--fix] 12 | - id: ruff-format 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QTiles 2 | 3 | A QGIS plugin. Generate raster tiles from QGIS project for selected zoom levels and tile naming convention (Slippy Map or TMS). Packages tiles for a variety of formats and applications: NextGIS Mobile, SMASH, simple Leaflet-based viewer and MBTiles. 4 | 5 | QGIS plugins page: https://plugins.qgis.org/plugins/qtiles/ 6 | 7 | ## Create raster tiles from a QGIS project 8 | 9 | ![qtiles](https://github.com/nextgis/qgis_qtiles/assets/101568545/2fe48644-fe8a-405f-a528-f126b7b46e70) 10 | 11 | ## YouTube 12 | 13 | [![vU4bGCh5khM](https://github.com/nextgis/qgis_qtiles/assets/101568545/44b0cf70-740e-42a9-93e2-77544f506884)](https://youtu.be/vU4bGCh5khM) 14 | 15 | ## License 16 | 17 | This program is licensed under GNU GPL v.2 or any later version. 18 | 19 | ## Commercial support 20 | 21 | Need to fix a bug or add a feature to QTiles? 22 | 23 | We provide custom development and support for this software. [Contact us](https://nextgis.com/contact/?utm_source=nextgis-github&utm_medium=plugins&utm_campaign=qtiles) to discuss options! 24 | 25 | 26 | [![https://nextgis.com](https://nextgis.com/img/nextgis_x-logo.png)](https://nextgis.com?utm_source=nextgis-github&utm_medium=plugins&utm_campaign=qtiles) 27 | -------------------------------------------------------------------------------- /assets/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/assets/example1.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "qtiles" 3 | version = "1.8.0" 4 | readme = "README.md" 5 | license = { file = "LICENSE" } 6 | 7 | 8 | [tool.qgspb.package-data] 9 | "qtiles.icons" = ["qtiles.png"] 10 | "qtiles.resources.css.images" = ["layers.png"] 11 | "qtiles.resource.js.images" = ["ui-bg_flat_75_ffffff_40x100.png"] 12 | 13 | [tool.qgspb.forms] 14 | ui-files = ["src/qtiles/ui/*.ui"] 15 | compile = false 16 | 17 | [tool.qgspb.resources] 18 | qrc-files = ["src/qtiles/resources.qrc"] 19 | target-suffix = "_rc" 20 | 21 | [tool.qgspb.translations] 22 | ts-files = ["src/qtiles/i18n/*.ts"] 23 | no-obsolete = true 24 | 25 | 26 | [project.optional-dependencies] 27 | dev = ["ruff", "pre-commit"] 28 | 29 | [tool.pyright] 30 | include = ["src"] 31 | pythonVersion = "3.7" 32 | 33 | reportOptionalCall = false 34 | reportOptionalMemberAccess = false 35 | 36 | [tool.ruff] 37 | line-length = 79 38 | target-version = "py37" 39 | 40 | [tool.ruff.lint] 41 | select = [ 42 | # "A", # flake8-builtins 43 | # "ARG", # flake8-unused-arguments 44 | # "B", # flake8-bugbear 45 | # "C90", # mccabe complexity 46 | # "COM", # flake8-commas 47 | # "E", # pycodestyle errors 48 | # "F", # pyflakes 49 | # "FBT", # flake8-boolean-trap 50 | # "FLY", # flynt 51 | # "I", # isort 52 | # "ISC", # flake8-implicit-str-concat 53 | # "LOG", # flake8-logging 54 | # "N", # pep8-naming 55 | # "PERF", # Perflint 56 | # "PGH", # pygrep-hooks 57 | # "PIE", # flake8-pie 58 | # "PL", # pylint 59 | # "PTH", # flake8-use-pathlib 60 | # "PYI", # flake8-pyi 61 | # "Q", # flake8-quotes 62 | # "RET", # flake8-return 63 | # "RSE", # flake8-raise 64 | # "RUF", 65 | # "SIM", # flake8-simplify 66 | # "SLF", # flake8-self 67 | # "T10", # flake8-debugger 68 | # "T20", # flake8-print 69 | # "TCH", # flake8-type-checking 70 | # "TD", # flake8-todos 71 | # "TID", # flake8-tidy-imports 72 | # "TRY", # tryceratops 73 | # "UP", # pyupgrade 74 | # "W", # pycodesytle warnings 75 | # "ANN", # flake8-annotations 76 | # "CPY", # flake8-copyright 77 | # "D", # pydocstyle 78 | # "FIX", # flake8-fixme 79 | ] 80 | ignore = ["ANN101", "ANN102", "TD003", "FBT003", "ISC001", "COM812", "E501"] 81 | exclude = ["resources_rc.py"] 82 | 83 | [tool.ruff.lint.per-file-ignores] 84 | "__init__.py" = ["F401"] 85 | 86 | [tool.ruff.lint.pep8-naming] 87 | extend-ignore-names = [ 88 | "setLevel", 89 | "classFactory", 90 | "initGui", 91 | "sizeHint", 92 | "createWidget", 93 | "*Event", 94 | ] 95 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | import platform 6 | import shutil 7 | import subprocess 8 | import zipfile 9 | from configparser import ConfigParser 10 | from pathlib import Path 11 | from typing import Dict, List, Optional 12 | 13 | import tomllib 14 | 15 | 16 | def with_name( 17 | path: Path, prefix: str = "", suffix: str = "", extension: str = ".py" 18 | ) -> Path: 19 | return path.with_name(f"{prefix}{path.stem}{suffix}{extension}") 20 | 21 | 22 | class QgisPluginBuilder: 23 | def __init__(self): 24 | current_directory = Path(__file__).parent 25 | pyproject_file = current_directory / "pyproject.toml" 26 | 27 | self.settings = tomllib.loads(pyproject_file.read_text()) 28 | self.project_settings = self.settings.get("project", {}) 29 | self.qgspb_settings = self.settings.get("tool", {}).get("qgspb", {}) 30 | self.data_settings = self.qgspb_settings.get("package-data", {}) 31 | self.ui_settings = self.qgspb_settings.get("forms", {}) 32 | self.qrc_settings = self.qgspb_settings.get("resources", {}) 33 | self.ts_settings = self.qgspb_settings.get("translations", {}) 34 | 35 | def bootstrap( 36 | self, 37 | *, 38 | compile_ui: Optional[bool] = None, 39 | compile_qrc: Optional[bool] = None, 40 | compile_ts: Optional[bool] = None, 41 | ) -> None: 42 | if all( 43 | setting is None 44 | for setting in (compile_ui, compile_qrc, compile_ts) 45 | ): 46 | compile_ui = True 47 | compile_qrc = True 48 | compile_ts = True 49 | 50 | if compile_ui: 51 | self.compile_ui() 52 | if compile_qrc: 53 | self.compile_qrc() 54 | if compile_ts: 55 | self.compile_ts() 56 | 57 | def compile_ui(self) -> None: 58 | if len(self.ui_settings) == 0 or not self.ui_settings.get( 59 | "compile", False 60 | ): 61 | return 62 | 63 | prefix = self.ui_settings.get("target-prefix", "") 64 | suffix = self.ui_settings.get("target-suffix", "") 65 | ui_patterns = self.ui_settings.get("ui-files", []) 66 | ui_paths = [ 67 | ui_path 68 | for ui_pattern in ui_patterns 69 | for ui_path in Path(__file__).parent.rglob(ui_pattern) 70 | ] 71 | for ui_path in ui_paths: 72 | output_path = with_name(ui_path, prefix, suffix, ".py") 73 | subprocess.check_output( 74 | ["pyuic5", "-o", str(output_path), str(ui_path)] 75 | ) 76 | self.__update_generated_file(output_path) 77 | 78 | def compile_qrc(self) -> None: 79 | if len(self.qrc_settings) == 0: 80 | return 81 | 82 | prefix = self.qrc_settings.get("target-prefix", "") 83 | suffix = self.qrc_settings.get("target-suffix", "") 84 | qrc_patterns = self.qrc_settings.get("qrc-files", []) 85 | qrc_paths = [ 86 | qrc_path 87 | for qrc_pattern in qrc_patterns 88 | for qrc_path in Path(__file__).parent.rglob(qrc_pattern) 89 | ] 90 | for qrc_path in qrc_paths: 91 | output_path = with_name(qrc_path, prefix, suffix, ".py") 92 | subprocess.check_output( 93 | ["pyrcc5", "-o", str(output_path), str(qrc_path)] 94 | ) 95 | self.__update_generated_file(output_path) 96 | 97 | def compile_ts(self): 98 | if len(self.ts_settings) == 0: 99 | return 100 | 101 | ts_patterns = self.ts_settings.get("ts-files", []) 102 | command_args = ["lrelease"] 103 | command_args.extend( 104 | str(ts_path) 105 | for ts_pattern in ts_patterns 106 | for ts_path in Path(__file__).parent.rglob(ts_pattern) 107 | ) 108 | 109 | subprocess.check_output(command_args) 110 | 111 | def build(self) -> None: 112 | self.bootstrap() 113 | 114 | build_mapping = self.__create_build_mapping() 115 | 116 | project_name: str = self.project_settings["name"] 117 | project_version: str = self.project_settings["version"] 118 | 119 | zip_file_name = f"{project_name}-{project_version}.zip" 120 | 121 | build_directory = Path(__file__).parent / "build" 122 | build_directory.mkdir(exist_ok=True) 123 | 124 | created_directories = set() 125 | 126 | def create_directories(zip_file: zipfile.ZipFile, path: Path): 127 | directory = "" 128 | for part in path.parts[:-1]: 129 | directory += f"{part}/" 130 | if directory in created_directories: 131 | continue 132 | zip_file.writestr(directory, "") 133 | created_directories.add(directory) 134 | 135 | zip_file_path = build_directory / zip_file_name 136 | with zipfile.ZipFile( 137 | zip_file_path, "w", zipfile.ZIP_DEFLATED 138 | ) as zip_file: 139 | for source_file, build_path in build_mapping.items(): 140 | create_directories(zip_file, build_path) 141 | zip_file.write(source_file, "/".join(build_path.parts)) 142 | 143 | def install( 144 | self, 145 | qgis: str, 146 | profile: Optional[str], 147 | editable: bool = False, 148 | force: bool = False, 149 | ) -> None: 150 | profile_path = self.__profile_path(qgis, profile) 151 | plugins_path = profile_path / "python" / "plugins" 152 | 153 | project_name: str = self.project_settings["name"] 154 | project_version: str = self.project_settings["version"] 155 | 156 | plugin_path = plugins_path / project_name 157 | 158 | installed_version = None 159 | 160 | print(f"Plugin {project_name} {project_version}\n") 161 | 162 | confirmation = ( 163 | input(":: Proceed with installation? [Y/n] ").strip().lower() 164 | ) 165 | 166 | if confirmation == "n": 167 | return 168 | 169 | print() 170 | 171 | if plugin_path.exists(): 172 | metadata_path = plugin_path / "metadata.txt" 173 | if not metadata_path.exists(): 174 | print( 175 | f"Plugin {project_name} is already" 176 | f' installed for "{profile_path.name}" profile' 177 | ) 178 | if not force: 179 | return 180 | 181 | print("\n:: Uninstalling broken plugin version...") 182 | self.__uninstall_plugin(plugin_path) 183 | 184 | else: 185 | metadata = ConfigParser() 186 | with open(metadata_path, encoding="utf-8") as f: 187 | metadata.read_file(f) 188 | installed_version = metadata.get("general", "version") 189 | 190 | print( 191 | f"Plugin {project_name} {installed_version} is already" 192 | f' installed for "{profile_path.name}" profile' 193 | ) 194 | 195 | if not force: 196 | return 197 | 198 | print("\n:: Uninstalling previous plugin version...") 199 | 200 | self.__uninstall_plugin(plugin_path) 201 | 202 | self.bootstrap() 203 | 204 | print(":: Installing plugin...") 205 | 206 | build_mapping = self.__create_build_mapping() 207 | for source_file, build_path in build_mapping.items(): 208 | (plugins_path / build_path).parent.mkdir( 209 | parents=True, exist_ok=True 210 | ) 211 | print(f"- {build_path}") 212 | 213 | if editable: 214 | (plugins_path / build_path).symlink_to(source_file) 215 | else: 216 | shutil.copy(source_file, plugins_path / build_path) 217 | 218 | print(f"\n:: {project_name} {project_version} successfully installed") 219 | 220 | def uninstall(self, qgis: str, profile: Optional[str]) -> None: 221 | profile_path = self.__profile_path(qgis, profile) 222 | plugins_path = profile_path / "python" / "plugins" 223 | 224 | project_name: str = self.project_settings["name"] 225 | 226 | plugin_path = plugins_path / project_name 227 | 228 | if not plugin_path.exists(): 229 | print( 230 | f"Plugin {project_name} is not installed for" 231 | f' "{profile_path.name}" profile' 232 | ) 233 | return 234 | 235 | metadata_path = plugin_path / "metadata.txt" 236 | assert metadata_path.exists() 237 | 238 | metadata = ConfigParser() 239 | with open(metadata_path, encoding="utf-8") as f: 240 | metadata.read_file(f) 241 | installed_version = metadata.get("general", "version") 242 | 243 | print(f"Plugin {project_name} {installed_version}\n") 244 | 245 | confirmation = ( 246 | input(":: Do you want to remove this plugin? [y/N] ") 247 | .strip() 248 | .lower() 249 | ) 250 | 251 | if confirmation != "y": 252 | return 253 | 254 | self.__uninstall_plugin(plugin_path) 255 | 256 | print( 257 | f"\n:: {project_name} {installed_version} successfully uninstalled" 258 | ) 259 | 260 | def clean(self) -> None: 261 | mappings = { 262 | **self.__create_resources_mapping(), 263 | **self.__create_translations_mapping(), 264 | } 265 | 266 | if self.ui_settings.get("compile", False): 267 | mappings.update(self.__create_forms_mapping()) 268 | 269 | src_directory = Path(__file__).parent / "src" 270 | for path in mappings.values(): 271 | (src_directory / path).unlink(missing_ok=True) 272 | 273 | def update_ts(self): 274 | if len(self.ts_settings) == 0: 275 | return 276 | 277 | command_args = ["pylupdate5"] 278 | if self.ts_settings.get("no-obsolete", False): 279 | command_args.append("-noobsolete") 280 | 281 | exclude_patterns = self.ts_settings.get("exclude-files", []) 282 | exclude_paths = set( 283 | exclude_path 284 | for exclude_pattern in exclude_patterns 285 | for exclude_path in Path(__file__).parent.rglob(exclude_pattern) 286 | ) 287 | exclude_paths.update( 288 | path 289 | for path in self.__create_forms_mapping().keys() 290 | if path.suffix == ".py" 291 | ) 292 | exclude_paths.update(self.__create_resources_mapping().keys()) 293 | 294 | ui_patterns = self.ui_settings.get("ui-files", []) 295 | source_paths = list(self.__create_sources_mapping().keys()) 296 | source_paths.extend( 297 | ui_path 298 | for ui_pattern in ui_patterns 299 | for ui_path in Path(__file__).parent.rglob(ui_pattern) 300 | ) 301 | 302 | if len(source_paths) == 0: 303 | raise RuntimeError("Sources list is empty") 304 | 305 | ts_patterns = self.ts_settings.get("ts-files", []) 306 | if len(ts_patterns) == 0: 307 | raise RuntimeError("Empty translations list") 308 | 309 | command_args.extend( 310 | str(source_path) 311 | for source_path in source_paths 312 | if source_path not in exclude_paths 313 | ) 314 | command_args.append("-ts") 315 | command_args.extend( 316 | str(ts_path) 317 | for ts_pattern in ts_patterns 318 | for ts_path in Path(__file__).parent.rglob(ts_pattern) 319 | ) 320 | 321 | subprocess.check_output(command_args) 322 | 323 | # TODO (ivanbarsukov): check unfinished in ts files 324 | 325 | def __create_build_mapping(self) -> Dict[Path, Path]: 326 | result = self.__create_metadata_mapping() 327 | result.update(self.__create_readme_mapping()) 328 | result.update(self.__create_license_mapping()) 329 | result.update(self.__create_sources_mapping()) 330 | result.update(self.__create_data_mapping()) 331 | result.update(self.__create_forms_mapping()) 332 | result.update(self.__create_resources_mapping()) 333 | result.update(self.__create_translations_mapping()) 334 | result = dict(sorted(result.items())) 335 | return result 336 | 337 | def __create_metadata_mapping(self) -> Dict[Path, Path]: 338 | project_version: str = self.project_settings["version"] 339 | 340 | src_directory = Path(__file__).parent / "src" 341 | project_name: str = self.project_settings["name"] 342 | 343 | metadata_path = src_directory / project_name / "metadata.txt" 344 | 345 | metadata = ConfigParser() 346 | with open(metadata_path, encoding="utf-8") as f: 347 | metadata.read_file(f) 348 | assert metadata.get("general", "version") == project_version 349 | 350 | build_path = Path(project_name) / metadata_path.name 351 | 352 | return {metadata_path: build_path} 353 | 354 | def __create_readme_mapping(self) -> Dict[Path, Path]: 355 | if "readme" not in self.project_settings: 356 | return {} 357 | 358 | readme_setting = self.project_settings["readme"] 359 | 360 | if isinstance(readme_setting, str): 361 | readme_path = Path(__file__).parent / readme_setting 362 | elif isinstance(readme_setting, dict): 363 | readme_path = Path(__file__).parent / readme_setting["file"] 364 | else: 365 | raise RuntimeError("Unknown readme setting") 366 | 367 | project_name: str = self.project_settings["name"] 368 | 369 | file_path = readme_path.absolute() 370 | build_path = Path(project_name) / file_path.name 371 | 372 | return {file_path: build_path} 373 | 374 | def __create_license_mapping(self) -> Dict[Path, Path]: 375 | if "license" not in self.project_settings: 376 | return {} 377 | 378 | license_setting = self.project_settings["license"] 379 | license_file = license_setting["file"] 380 | assert isinstance(license_file, str) 381 | 382 | project_name: str = self.project_settings["name"] 383 | 384 | file_path = (Path(__file__).parent / license_file).absolute() 385 | build_path = Path(project_name) / file_path.name 386 | 387 | return {file_path: build_path} 388 | 389 | def __create_sources_mapping(self) -> Dict[Path, Path]: 390 | project_name: str = self.project_settings["name"] 391 | src_directory = Path(__file__).parent / "src" 392 | 393 | exclude_patterns = self.qgspb_settings.get("exclude-files", []) 394 | exclude_paths = set( 395 | exclude_path.absolute() 396 | for exclude_pattern in exclude_patterns 397 | for exclude_path in Path(__file__).parent.rglob(exclude_pattern) 398 | ) 399 | 400 | return { 401 | py_path.absolute(): py_path.relative_to(src_directory) 402 | for py_path in (src_directory / project_name).rglob("*.py") 403 | if py_path.absolute() not in exclude_paths 404 | } 405 | 406 | def __create_data_mapping(self) -> Dict[Path, Path]: 407 | if len(self.data_settings) == 0: 408 | return {} 409 | 410 | src_directory = Path(__file__).parent / "src" 411 | 412 | data_paths = [] 413 | for package, resources in self.data_settings.items(): 414 | package_path = src_directory / package.replace(".", "/") 415 | for data_template in resources: 416 | data_paths.extend(package_path.rglob(data_template)) 417 | 418 | return { 419 | data_path.absolute(): data_path.relative_to(src_directory) 420 | for data_path in data_paths 421 | } 422 | 423 | def __create_forms_mapping(self) -> Dict[Path, Path]: 424 | if len(self.ui_settings) == 0: 425 | return {} 426 | 427 | ui_patterns = self.ui_settings.get("ui-files", []) 428 | ui_paths = [ 429 | ui_path 430 | for ui_pattern in ui_patterns 431 | for ui_path in Path(__file__).parent.rglob(ui_pattern) 432 | ] 433 | 434 | src_directory = Path(__file__).parent / "src" 435 | 436 | if not self.ui_settings.get("compile", False): 437 | return { 438 | ui_file.absolute(): ui_file.relative_to(src_directory) 439 | for ui_file in ui_paths 440 | } 441 | 442 | prefix = self.ui_settings.get("target-prefix", "") 443 | suffix = self.ui_settings.get("target-suffix", "") 444 | 445 | result = {} 446 | for ui_path in ui_paths: 447 | py_path = with_name(ui_path, prefix, suffix, ".py") 448 | result[py_path.absolute()] = py_path.relative_to(src_directory) 449 | 450 | return result 451 | 452 | def __create_resources_mapping(self) -> Dict[Path, Path]: 453 | if len(self.qrc_settings) == 0: 454 | return {} 455 | 456 | prefix = self.qrc_settings.get("target-prefix", "") 457 | suffix = self.qrc_settings.get("target-suffix", "") 458 | qrc_patterns = self.qrc_settings.get("qrc-files", []) 459 | qrc_paths = [ 460 | qrc_path 461 | for qrc_pattern in qrc_patterns 462 | for qrc_path in Path(__file__).parent.rglob(qrc_pattern) 463 | ] 464 | 465 | src_directory = Path(__file__).parent / "src" 466 | 467 | result = {} 468 | for qrc_path in qrc_paths: 469 | py_path = with_name(qrc_path, prefix, suffix, ".py") 470 | result[py_path.absolute()] = py_path.relative_to(src_directory) 471 | 472 | return result 473 | 474 | def __create_translations_mapping(self) -> Dict[Path, Path]: 475 | if len(self.ts_settings) == 0: 476 | return {} 477 | 478 | ts_patterns = self.ts_settings.get("ts-files", []) 479 | ts_paths = [ 480 | ts_path 481 | for ts_pattern in ts_patterns 482 | for ts_path in Path(__file__).parent.rglob(ts_pattern) 483 | ] 484 | 485 | src_directory = Path(__file__).parent / "src" 486 | 487 | result = {} 488 | for ts_path in ts_paths: 489 | qm_file = ts_path.with_suffix(".qm") 490 | result[qm_file.absolute()] = qm_file.relative_to(src_directory) 491 | 492 | return result 493 | 494 | def __update_generated_file(self, file_path: Path) -> None: 495 | assert file_path.suffix == ".py" 496 | content = file_path.read_text(encoding="utf-8") 497 | file_path.write_text(content.replace("from PyQt5", "from qgis.PyQt")) 498 | 499 | def __profile_path(self, qgis: str, profile: Optional[str]) -> Path: 500 | system = platform.system() 501 | 502 | if qgis == "Vanilla": 503 | qgis_profiles = Path("QGIS/QGIS3/profiles") 504 | elif qgis == "NextGIS": 505 | qgis_profiles = Path("NextGIS/ngqgis/profiles") 506 | else: 507 | raise RuntimeError(f"Unknown QGIS: {qgis}") 508 | 509 | if system == "Linux": 510 | profiles_path = ( 511 | Path("~/.local/share/").expanduser() / qgis_profiles 512 | ) 513 | 514 | elif system == "Windows": 515 | appdata = os.getenv("APPDATA") 516 | assert appdata is not None 517 | profiles_path = Path(appdata) / qgis_profiles 518 | 519 | elif system == "Darwin": # macOS 520 | profiles_path = ( 521 | Path("~/Library/Application Support/").expanduser() 522 | / qgis_profiles 523 | ) 524 | 525 | else: 526 | raise OSError(f"Unsupported OS: {system}") 527 | 528 | if not profiles_path.exists(): 529 | raise FileExistsError( 530 | f"Profiles path for {qgis} QGIS is not exists" 531 | ) 532 | 533 | profiles: List[str] = [] 534 | for path in profiles_path.glob("*"): 535 | if path.is_dir(): 536 | profiles.append(path.name) 537 | 538 | if profile is not None and profile not in profiles: 539 | print(f'Warning: profile "{profile}"" is not found\n') 540 | profile = None 541 | 542 | if profile is None: 543 | profiles_ini_path = profiles_path / "profiles.ini" 544 | if not profiles_ini_path.exists(): 545 | raise FileExistsError("profiles.ini is not exists") 546 | 547 | profiles_ini = ConfigParser() 548 | profiles_ini.read(profiles_ini_path) 549 | 550 | default_profile = profiles_ini.get( 551 | "core", "defaultProfile", fallback=None 552 | ) 553 | 554 | if len(profiles) == 0: 555 | raise RuntimeError("There are no QGIS profiles") 556 | 557 | elif len(profiles) == 1: 558 | profile = profiles[0] 559 | 560 | else: 561 | print(f":: {len(profiles)} profiles found") 562 | for i, found_profile in enumerate(profiles, start=1): 563 | print(f"{i:2} {found_profile}") 564 | print() 565 | 566 | default_profile_index = -1 567 | default_text = "" 568 | if default_profile in profiles: 569 | default_profile_index = profiles.index(default_profile) 570 | default_text = f" [default is {default_profile_index + 1}]" 571 | 572 | choosen_index = input( 573 | f":: Choose QGIS profile{default_text}: " 574 | ) 575 | print() 576 | if choosen_index.strip() == "": 577 | choosen_index = default_profile_index + 1 578 | 579 | choosen_index = int(choosen_index) 580 | if choosen_index < 1 or choosen_index > len(profiles): 581 | raise ValueError 582 | 583 | profile = profiles[choosen_index - 1] 584 | 585 | return profiles_path / profile 586 | 587 | def __uninstall_plugin(self, path: Path) -> None: 588 | if path.is_symlink(): 589 | path.unlink() 590 | elif path.is_dir(): 591 | shutil.rmtree(path) 592 | 593 | 594 | def create_parser(): 595 | parser = argparse.ArgumentParser(description="QGIS plugins build tool") 596 | 597 | subparsers = parser.add_subparsers( 598 | dest="command", required=True, help="Available commands" 599 | ) 600 | 601 | # bootstrap command 602 | parser_bootstrap = subparsers.add_parser( 603 | "bootstrap", help="Bootstrap the plugin" 604 | ) 605 | parser_bootstrap.add_argument( 606 | "--ts", 607 | dest="compile_ts", 608 | default=None, 609 | action="store_true", 610 | help="Compile only translations", 611 | ) 612 | parser_bootstrap.add_argument( 613 | "--ui", 614 | dest="compile_ui", 615 | default=None, 616 | action="store_true", 617 | help="Compile only forms", 618 | ) 619 | parser_bootstrap.add_argument( 620 | "--qrc", 621 | dest="compile_qrc", 622 | default=None, 623 | action="store_true", 624 | help="Compile only resources", 625 | ) 626 | 627 | # build command 628 | subparsers.add_parser("build", help="Build the plugin") 629 | 630 | # install command 631 | parser_install = subparsers.add_parser( 632 | "install", help="Install the plugin" 633 | ) 634 | parser_install.add_argument( 635 | "--qgis", 636 | default="Vanilla", 637 | choices=["Vanilla", "NextGIS"], 638 | help="QGIS build", 639 | ) 640 | parser_install.add_argument( 641 | "--profile", default=None, help="QGIS profile name" 642 | ) 643 | parser_install.add_argument( 644 | "--editable", action="store_true", help="Install in editable mode" 645 | ) 646 | parser_install.add_argument( 647 | "--force", action="store_true", help="Reinstall if installed" 648 | ) 649 | 650 | # uninstall command 651 | parser_uninstall = subparsers.add_parser( 652 | "uninstall", help="Uninstall the project" 653 | ) 654 | parser_uninstall.add_argument( 655 | "--qgis", 656 | default="Vanilla", 657 | choices=["Vanilla", "NextGIS"], 658 | help="QGIS build", 659 | ) 660 | parser_uninstall.add_argument( 661 | "--profile", default=None, help="QGIS profile name" 662 | ) 663 | 664 | # clean command 665 | subparsers.add_parser("clean", help="Clean compiled files") 666 | 667 | # update_ts command 668 | subparsers.add_parser("update_ts", help="Update translations") 669 | 670 | return parser 671 | 672 | 673 | def main() -> None: 674 | parser = create_parser() 675 | args = parser.parse_args() 676 | 677 | builder = QgisPluginBuilder() 678 | 679 | try: 680 | if args.command == "bootstrap": 681 | builder.bootstrap( 682 | compile_ui=args.compile_ui, 683 | compile_qrc=args.compile_qrc, 684 | compile_ts=args.compile_ts, 685 | ) 686 | elif args.command == "build": 687 | builder.build() 688 | elif args.command == "install": 689 | builder.install(args.qgis, args.profile, args.editable, args.force) 690 | elif args.command == "uninstall": 691 | builder.uninstall(args.qgis, args.profile) 692 | elif args.command == "clean": 693 | builder.clean() 694 | elif args.command == "update_ts": 695 | builder.update_ts() 696 | 697 | except KeyboardInterrupt: 698 | print("\nInterrupt signal received") 699 | 700 | 701 | if __name__ == "__main__": 702 | main() 703 | -------------------------------------------------------------------------------- /src/qtiles/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ****************************************************************************** 4 | # 5 | # QTiles 6 | # --------------------------------------------------------- 7 | # Generates tiles from QGIS project 8 | # 9 | # Copyright (C) 2012-2014 NextGIS (info@nextgis.org) 10 | # 11 | # This source is free software; you can redistribute it and/or modify it under 12 | # the terms of the GNU General Public License as published by the Free 13 | # Software Foundation, either version 2 of the License, or (at your option) 14 | # any later version. 15 | # 16 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 17 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 19 | # details. 20 | # 21 | # A copy of the GNU General Public License is available on the World Wide Web 22 | # at . You can also obtain it by writing 23 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 24 | # MA 02110-1335 USA. 25 | # 26 | # ****************************************************************************** 27 | 28 | 29 | def classFactory(iface): 30 | from .qtiles import QTilesPlugin 31 | 32 | return QTilesPlugin(iface) 33 | -------------------------------------------------------------------------------- /src/qtiles/aboutdialog.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from pathlib import Path 3 | from typing import Dict, Optional 4 | 5 | from qgis.core import QgsSettings 6 | from qgis.PyQt import uic 7 | from qgis.PyQt.QtCore import QFile, QLocale, QSize, Qt, QUrl 8 | from qgis.PyQt.QtGui import QDesktopServices, QIcon, QPixmap 9 | from qgis.PyQt.QtSvg import QSvgWidget 10 | from qgis.PyQt.QtWidgets import QDialog, QLabel, QWidget 11 | from qgis.utils import pluginMetadata 12 | 13 | CURRENT_PATH = Path(__file__).parent 14 | UI_PATH = Path(__file__).parent / "ui" 15 | RESOURCES_PATH = Path(__file__).parents[1] / "resources" 16 | 17 | if (UI_PATH / "about_dialog_base.ui").exists(): 18 | Ui_AboutDialogBase, _ = uic.loadUiType( 19 | str(UI_PATH / "about_dialog_base.ui") 20 | ) 21 | elif (UI_PATH / "aboutdialogbase.ui").exists(): 22 | Ui_AboutDialogBase, _ = uic.loadUiType(str(UI_PATH / "aboutdialogbase.ui")) 23 | elif (RESOURCES_PATH / "about_dialog_base.ui").exists(): 24 | Ui_AboutDialogBase, _ = uic.loadUiType( 25 | str(RESOURCES_PATH / "about_dialog_base.ui") 26 | ) 27 | elif (CURRENT_PATH / "about_dialog_base.ui").exists(): 28 | Ui_AboutDialogBase, _ = uic.loadUiType( 29 | str(CURRENT_PATH / "about_dialog_base.ui") 30 | ) 31 | elif (UI_PATH / "about_dialog_base.py").exists(): 32 | from .ui.about_dialog_base import ( # type: ignore 33 | Ui_AboutDialogBase, 34 | ) 35 | elif (UI_PATH / "aboutdialogbase.py").exists(): 36 | from .ui.aboutdialogbase import ( # type: ignore 37 | Ui_AboutDialogBase, 38 | ) 39 | elif (UI_PATH / "ui_aboutdialogbase.py").exists(): 40 | from .ui.ui_aboutdialogbase import ( # type: ignore 41 | Ui_AboutDialogBase, 42 | ) 43 | else: 44 | raise ImportError 45 | 46 | 47 | class AboutTab(IntEnum): 48 | Information = 0 49 | License = 1 50 | Components = 2 51 | Contributors = 3 52 | 53 | 54 | class AboutDialog(QDialog, Ui_AboutDialogBase): 55 | def __init__(self, package_name: str, parent: Optional[QWidget] = None): 56 | super().__init__(parent) 57 | self.setupUi(self) 58 | self.__package_name = package_name 59 | 60 | self.tab_widget.setCurrentIndex(0) 61 | 62 | metadata = self.__metadata() 63 | self.__set_icon(metadata) 64 | self.__fill_headers(metadata) 65 | self.__fill_get_involved(metadata) 66 | self.__fill_about(metadata) 67 | self.__fill_license() 68 | self.__fill_components() 69 | self.__fill_contributors() 70 | 71 | def __fill_headers(self, metadata: Dict[str, Optional[str]]) -> None: 72 | plugin_name = metadata["plugin_name"] 73 | assert isinstance(plugin_name, str) 74 | if "NextGIS" not in plugin_name: 75 | plugin_name += self.tr(" by NextGIS") 76 | 77 | self.setWindowTitle(self.windowTitle().format(plugin_name=plugin_name)) 78 | self.plugin_name_label.setText( 79 | self.plugin_name_label.text().format_map(metadata) 80 | ) 81 | self.version_label.setText( 82 | self.version_label.text().format_map(metadata) 83 | ) 84 | 85 | def __set_icon(self, metadata: Dict[str, Optional[str]]) -> None: 86 | if metadata.get("icon_path") is None: 87 | return 88 | 89 | header_size: QSize = self.info_layout.sizeHint() 90 | 91 | icon_path = Path(__file__).parent / str(metadata.get("icon_path")) 92 | svg_icon_path = icon_path.with_suffix(".svg") 93 | 94 | if svg_icon_path.exists(): 95 | icon_widget: QWidget = QSvgWidget(str(svg_icon_path), self) 96 | icon_size: QSize = icon_widget.sizeHint() 97 | else: 98 | pixmap = QPixmap(str(icon_path)) 99 | if pixmap.size().height() > header_size.height(): 100 | pixmap = pixmap.scaled( 101 | header_size.height(), 102 | header_size.height(), 103 | Qt.AspectRatioMode.KeepAspectRatioByExpanding, 104 | ) 105 | 106 | icon_size: QSize = pixmap.size() 107 | 108 | icon_widget = QLabel(self) 109 | icon_widget.setPixmap(pixmap) 110 | icon_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) 111 | 112 | icon_size.scale( 113 | header_size.height(), 114 | header_size.height(), 115 | Qt.AspectRatioMode.KeepAspectRatioByExpanding, 116 | ) 117 | icon_widget.setFixedSize(icon_size) 118 | self.header_layout.insertWidget(0, icon_widget) 119 | 120 | def __fill_get_involved(self, metadata: Dict[str, Optional[str]]) -> None: 121 | plugin_path = Path(__file__).parent 122 | file_path = str(plugin_path / "icons" / "nextgis_logo.svg") 123 | resources_path = ( 124 | f":/plugins/{self.__package_name}/icons/nextgis_logo.svg" 125 | ) 126 | 127 | if QFile(resources_path).exists(): 128 | self.get_involved_button.setIcon(QIcon(resources_path)) 129 | elif QFile(file_path).exists(): 130 | self.get_involved_button.setIcon(QIcon(file_path)) 131 | 132 | self.get_involved_button.clicked.connect( 133 | lambda: QDesktopServices.openUrl( 134 | QUrl(metadata["get_involved_url"]) 135 | ) 136 | ) 137 | 138 | def __fill_about(self, metadata: Dict[str, Optional[str]]) -> None: 139 | self.about_text_browser.setHtml(self.__html(metadata)) 140 | 141 | def __fill_license(self) -> None: 142 | license_path = Path(__file__).parent / "LICENSE" 143 | if not license_path.exists(): 144 | self.tab_widget.setTabVisible(AboutTab.License, False) 145 | return 146 | 147 | self.tab_widget.setTabVisible(AboutTab.License, True) 148 | self.license_text_browser.setPlainText(license_path.read_text()) 149 | 150 | def __fill_components(self) -> None: 151 | self.tab_widget.setTabVisible(AboutTab.Components, False) 152 | 153 | def __fill_contributors(self) -> None: 154 | self.tab_widget.setTabVisible(AboutTab.Contributors, False) 155 | 156 | def __locale(self) -> str: 157 | override_locale = QgsSettings().value( 158 | "locale/overrideFlag", defaultValue=False, type=bool 159 | ) 160 | if not override_locale: 161 | locale_full_name = QLocale.system().name() 162 | else: 163 | locale_full_name = QgsSettings().value("locale/userLocale", "") 164 | 165 | return locale_full_name[0:2] 166 | 167 | def __metadata(self) -> Dict[str, Optional[str]]: 168 | locale = self.__locale() 169 | speaks_russian = locale in ["be", "kk", "ky", "ru", "uk"] 170 | 171 | def metadata_value(key: str) -> Optional[str]: 172 | value = pluginMetadata(self.__package_name, f"{key}[{locale}]") 173 | if value == "__error__": 174 | value = pluginMetadata(self.__package_name, key) 175 | if value == "__error__": 176 | value = None 177 | return value 178 | 179 | about = metadata_value("about") 180 | assert about is not None 181 | for about_stop_phrase in ( 182 | "Разработан", 183 | "Developed by", 184 | "Développé par", 185 | "Desarrollado por", 186 | "Sviluppato da", 187 | "Desenvolvido por", 188 | ): 189 | if about.find(about_stop_phrase) > 0: 190 | about = about[: about.find(about_stop_phrase)] 191 | 192 | package_name = self.__package_name.replace("qgis_", "") 193 | 194 | main_url = f"https://nextgis.{'ru' if speaks_russian else 'com'}" 195 | utm = f"utm_source=qgis_plugin&utm_medium=about&utm_campaign=constant&utm_term={package_name}&utm_content={locale}" 196 | 197 | return { 198 | "plugin_name": metadata_value("name"), 199 | "version": metadata_value("version"), 200 | "icon_path": metadata_value("icon"), 201 | "description": metadata_value("description"), 202 | "about": about, 203 | "authors": metadata_value("author"), 204 | "video_url": metadata_value("video"), 205 | "homepage_url": metadata_value("homepage"), 206 | "tracker_url": metadata_value("tracker"), 207 | "main_url": main_url, 208 | "data_url": main_url.replace("://", "://data."), 209 | "get_involved_url": f"https://nextgis.com/redirect/{locale}/ak45prp5?{utm}", 210 | "utm": f"?{utm}", 211 | "speaks_russian": str(speaks_russian), 212 | } 213 | 214 | def __html(self, metadata: Dict[str, Optional[str]]) -> str: 215 | report_end = self.tr("REPORT_END") 216 | if report_end == "REPORT_END": 217 | report_end = "" 218 | 219 | titles = { 220 | "developers_title": self.tr("Developers"), 221 | "homepage_title": self.tr("Homepage"), 222 | "report_title": self.tr("Please report bugs at"), 223 | "report_end": report_end, 224 | "bugtracker_title": self.tr("bugtracker"), 225 | "video_title": self.tr("Video with an overview of the plugin"), 226 | "services_title": self.tr("Other helpful services by NextGIS"), 227 | "extracts_title": self.tr( 228 | "Convenient up-to-date data extracts for any place in the world" 229 | ), 230 | "webgis_title": self.tr("Fully featured Web GIS service"), 231 | } 232 | 233 | description = """ 234 |

{description}

235 |

{about}

236 |

{developers_title}: {authors}

237 |

{homepage_title}: {homepage_url}

238 |

{report_title} {bugtracker_title} {report_end}

239 | """ 240 | 241 | if metadata.get("video_url") is not None: 242 | description += '

{video_title}: {video_url}

' 243 | 244 | services = """ 245 |

246 | {services_title}: 247 |

251 |

252 | """ 253 | 254 | replacements = dict() 255 | replacements.update(titles) 256 | replacements.update(metadata) 257 | 258 | return (description + services).format_map(replacements) 259 | -------------------------------------------------------------------------------- /src/qtiles/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ****************************************************************************** 3 | # 4 | # OSMInfo 5 | # --------------------------------------------------------- 6 | # This plugin takes coordinates of a mouse click and gets information about all 7 | # objects from this point from OSM using Overpass API. 8 | # 9 | # Author: Denis Ilyin, denis.ilyin@nextgis.com 10 | # ***************************************************************************** 11 | # Copyright (c) 2015-2021. NextGIS, info@nextgis.com 12 | # 13 | # This source is free software; you can redistribute it and/or modify it under 14 | # the terms of the GNU General Public License as published by the Free 15 | # Software Foundation, either version 2 of the License, or (at your option) 16 | # any later version. 17 | # 18 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 19 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 20 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 21 | # details. 22 | # 23 | # A copy of the GNU General Public License is available on the World Wide Web 24 | # at . You can also obtain it by writing 25 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 26 | # MA 02110-1335 USA. 27 | # 28 | # ****************************************************************************** 29 | 30 | import os 31 | import sys 32 | 33 | from qgis import core 34 | from qgis.PyQt.QtWidgets import QFileDialog 35 | 36 | PY2 = sys.version_info[0] == 2 37 | PY3 = sys.version_info[0] == 3 38 | 39 | if PY3: 40 | import configparser 41 | else: 42 | import ConfigParser as configparser 43 | 44 | if hasattr(core, "QGis"): 45 | from qgis.core import QGis 46 | else: 47 | from qgis.core import Qgis as QGis 48 | 49 | if QGis.QGIS_VERSION_INT >= 30000: 50 | QGIS_VERSION_3 = True 51 | 52 | mapLayers = core.QgsProject.instance().mapLayers 53 | 54 | from qgis.core import QgsPointXY 55 | from qgis.core import QgsSettings 56 | 57 | QgsMessageLogInfo = QGis.Info 58 | 59 | qgisUserDatabaseFilePath = core.QgsApplication.qgisUserDatabaseFilePath 60 | else: 61 | QGIS_VERSION_3 = False 62 | 63 | mapLayers = core.QgsMapLayerRegistry.instance().mapLayers 64 | 65 | from qgis.core import QgsPoint as QgsPointXY 66 | from qgis.PyQt.QtCore import QSettings as QgsSettings 67 | 68 | QgsMessageLogInfo = core.QgsMessageLog.INFO 69 | 70 | qgisUserDatabaseFilePath = core.QgsApplication.qgisUserDbFilePath 71 | 72 | 73 | class QgsCoordinateTransform(core.QgsCoordinateTransform): 74 | def __init__(self, src_crs, dst_crs): 75 | super(QgsCoordinateTransform, self).__init__() 76 | 77 | self.setSourceCrs(src_crs) 78 | self.setDestinationCrs(dst_crs) 79 | 80 | def setDestinationCrs(self, dst_crs): 81 | if QGis.QGIS_VERSION_INT >= 30000: 82 | super(QgsCoordinateTransform, self).setDestinationCrs(dst_crs) 83 | else: 84 | self.setDestCRS(dst_crs) 85 | 86 | 87 | class QgsCoordinateReferenceSystem(core.QgsCoordinateReferenceSystem): 88 | def __init__(self, id, type): 89 | if QGis.QGIS_VERSION_INT >= 30000: 90 | super(QgsCoordinateReferenceSystem, self).__init__( 91 | core.QgsCoordinateReferenceSystem.fromEpsgId(id) 92 | ) 93 | else: 94 | super(QgsCoordinateReferenceSystem, self).__init__(id, type) 95 | 96 | @staticmethod 97 | def fromEpsgId(id): 98 | if QGis.QGIS_VERSION_INT >= 30000: 99 | return core.QgsCoordinateReferenceSystem.fromEpsgId(id) 100 | else: 101 | return core.QgsCoordinateReferenceSystem(id) 102 | 103 | 104 | def getSaveFileName(parent, title, directory, filter): 105 | if QGIS_VERSION_3: 106 | outPath, outFilter = QFileDialog.getSaveFileName( 107 | parent, title, directory, filter 108 | ) 109 | else: 110 | outPath = QFileDialog.getSaveFileName(parent, title, directory, filter) 111 | return outPath 112 | -------------------------------------------------------------------------------- /src/qtiles/i18n/qtiles_ru_RU.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AboutDialog 6 | 7 | 8 | Developers 9 | Разработчики 10 | 11 | 12 | 13 | Homepage 14 | Домашняя страница 15 | 16 | 17 | 18 | Please report bugs at 19 | Пожалуйста, сообщайте об ошибках в 20 | 21 | 22 | 23 | Video with an overview of the plugin 24 | Видео с обзором плагина 25 | 26 | 27 | 28 | Other helpful services by NextGIS 29 | Другие полезные сервисы от NextGIS 30 | 31 | 32 | 33 | Convenient up-to-date data extracts for any place in the world 34 | Удобная выборка актуальных данных из любой точки мира 35 | 36 | 37 | 38 | Fully featured Web GIS service 39 | Полнофункциональный Веб ГИС-сервис 40 | 41 | 42 | 43 | REPORT_END 44 | 45 | 46 | 47 | 48 | bugtracker 49 | багтрекер 50 | 51 | 52 | 53 | by NextGIS 54 | от NextGIS 55 | 56 | 57 | 58 | AboutDialogBase 59 | 60 | 61 | About {plugin_name} 62 | О модуле {plugin_name} 63 | 64 | 65 | 66 | Information 67 | Информация 68 | 69 | 70 | 71 | License 72 | Лицензия 73 | 74 | 75 | 76 | Components 77 | Компоненты 78 | 79 | 80 | 81 | Contributors 82 | Участники 83 | 84 | 85 | 86 | {plugin_name} 87 | 88 | 89 | 90 | 91 | Version {version} 92 | Версия {version} 93 | 94 | 95 | 96 | Get involved 97 | Присоединяйтесь 98 | 99 | 100 | 101 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 102 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 103 | p, li { white-space: pre-wrap; } 104 | </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> 105 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> 106 | 107 | 108 | 109 | 110 | Dialog 111 | 112 | 113 | QTiles 114 | 115 | 116 | 117 | 118 | Output 119 | Результат 120 | 121 | 122 | 123 | Directory 124 | Каталог 125 | 126 | 127 | 128 | Extent 129 | Охват 130 | 131 | 132 | 133 | Canvas extent 134 | Текущий охват карты 135 | 136 | 137 | 138 | Full extent 139 | Полный охват 140 | 141 | 142 | 143 | Layer extent 144 | Охват слоя 145 | 146 | 147 | 148 | Zoom 149 | Масштаб 150 | 151 | 152 | 153 | Minimum zoom 154 | Минимальный масштаб 155 | 156 | 157 | 158 | Maximum zoom 159 | Максимальный масштаб 160 | 161 | 162 | 163 | Parameters 164 | Параметры 165 | 166 | 167 | 168 | Tile width 169 | Ширина тайла 170 | 171 | 172 | 173 | Lock 1:1 ratio 174 | Зафиксировать соотношение сторон 1:1 175 | 176 | 177 | 178 | Tile height 179 | Высота тайла 180 | 181 | 182 | 183 | Tileset name 184 | Название набора 185 | 186 | 187 | 188 | Make lines appear less jagged at the expence of some drawing performance 189 | Рисовать сглаженные линии (снижает скорость отрисовки) 190 | 191 | 192 | 193 | Write .mapurl file 194 | Создать файл .mapurl 195 | 196 | 197 | 198 | Write Leaflet-based viewer 199 | Создать просмотрщик на Leaflet 200 | 201 | 202 | 203 | Use TMS tiles convention (Slippy Map by default) 204 | Использовать спецификацию TMS (по умолчанию Slippy Map) 205 | 206 | 207 | 208 | File 209 | Файл 210 | 211 | 212 | 213 | Background transparency 214 | Прозрачность фона 215 | 216 | 217 | 218 | NGM 219 | 220 | 221 | 222 | 223 | Use MBTiles compression 224 | Использовать сжатие MBTiles 225 | 226 | 227 | 228 | Write .json metadata 229 | Сохранить метаданные в файл .json 230 | 231 | 232 | 233 | Write overview image file 234 | Создать файл обзорного изображения карты 235 | 236 | 237 | 238 | Format 239 | Формат 240 | 241 | 242 | 243 | Quality 244 | Качество 245 | 246 | 247 | 248 | (0-100) 249 | 250 | 251 | 252 | 253 | PNG 254 | 255 | 256 | 257 | 258 | JPG 259 | 260 | 261 | 262 | 263 | <a href="infoOutpuZip"><img src=":/plugins/qtiles/icons/info.png"/></a> 264 | 265 | 266 | 267 | 268 | Render tiles outside of layers extents (within combined extent) 269 | Рисовать тайлы вне границ слоёв, но в охвате 270 | 271 | 272 | 273 | ... 274 | 275 | 276 | 277 | 278 | ╮ 279 | 280 | 281 | 282 | 283 | ╯ 284 | 285 | 286 | 287 | 288 | QTiles 289 | 290 | 291 | Error 292 | Ошибка 293 | 294 | 295 | 296 | QTiles 297 | 298 | 299 | 300 | 301 | About QTiles... 302 | О QTiles... 303 | 304 | 305 | 306 | QGIS %s detected. 307 | 308 | Обнаружен QGIS %s. 309 | 310 | 311 | 312 | 313 | QTilesDialog 314 | 315 | 316 | No output 317 | Не указан путь 318 | 319 | 320 | 321 | Output path is not set. Please enter correct path and try again. 322 | Не указан путь назначения. Пожалуйста, введите правильный путь и попробуйте ещё раз. 323 | 324 | 325 | 326 | Layer not selected 327 | Слой не выбран 328 | 329 | 330 | 331 | Please select a layer and try again. 332 | Пожалуйста, выберите слой и попробуйте еще раз. 333 | 334 | 335 | 336 | Directory not empty 337 | Каталог не пуст 338 | 339 | 340 | 341 | Selected directory is not empty. Continue? 342 | Каталог назначения не пуст. Продолжить? 343 | 344 | 345 | 346 | Wrong zoom 347 | Неверный масштаб 348 | 349 | 350 | 351 | Maximum zoom value is lower than minimum. Please correct this and try again. 352 | Значение максимального масштаба меньше минимального. Пожалуйста, исправьте ошибку и попробуйте ещё раз. 353 | 354 | 355 | 356 | Cancel 357 | Отменить 358 | 359 | 360 | 361 | Close 362 | Закрыть 363 | 364 | 365 | 366 | Confirmation 367 | Подтверждение 368 | 369 | 370 | 371 | Estimate number of tiles more then %d! Continue? 372 | Оцениваемое количество тайлов больше %d! Продолжить? 373 | 374 | 375 | 376 | Save to file 377 | Сохранить файл 378 | 379 | 380 | 381 | ZIP archives (*.zip *.ZIP) 382 | ZIP архивы (*.zip *.ZIP) 383 | 384 | 385 | 386 | Save to directory 387 | Выбрать каталог 388 | 389 | 390 | 391 | MBTiles databases (*.mbtiles *.MBTILES) 392 | База MBTiles (*.mbtiles *.MBTILES) 393 | 394 | 395 | 396 | Output type info 397 | Вид результата 398 | 399 | 400 | 401 | Save tiles as Zip or MBTiles 402 | Сохранить тайлы как Zip архив или MBTiles 403 | 404 | 405 | 406 | Save tiles as directory structure 407 | Сохранить тайлы в дереве каталогов 408 | 409 | 410 | 411 | Run 412 | Запустить 413 | 414 | 415 | 416 | Prepare package for <a href='http://nextgis.ru/en/nextgis-mobile/'> NextGIS Mobile </a> 417 | Подготовить пакет для <a href='http://nextgis.ru/nextgis-mobile/'> NextGIS Mobile </a> 418 | 419 | 420 | 421 | TilingThread 422 | 423 | 424 | Searching tiles... 425 | Поиск тайлов... 426 | 427 | 428 | 429 | Rendering: %v from %m (%p%) 430 | Отрисовка: %v из %m (%p%) 431 | 432 | 433 | 434 | -------------------------------------------------------------------------------- /src/qtiles/icons/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/src/qtiles/icons/about.png -------------------------------------------------------------------------------- /src/qtiles/icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/src/qtiles/icons/info.png -------------------------------------------------------------------------------- /src/qtiles/icons/nextgis_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 28 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/qtiles/icons/ngm_index_24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/src/qtiles/icons/ngm_index_24x24.png -------------------------------------------------------------------------------- /src/qtiles/icons/qtiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/src/qtiles/icons/qtiles.png -------------------------------------------------------------------------------- /src/qtiles/mbutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # MBUtil: a tool for MBTiles files 4 | # Supports importing, exporting, and more 5 | # 6 | # (c) Development Seed 2012 7 | # Licensed under BSD 8 | 9 | # for additional reference on schema see: 10 | # https://github.com/mapbox/node-mbtiles/blob/master/lib/schema.sql 11 | 12 | import sqlite3, uuid, sys, logging, time, os, json, zlib, re 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def flip_y(zoom, y): 18 | return (2**zoom - 1) - y 19 | 20 | 21 | def mbtiles_setup(cur): 22 | cur.execute(""" 23 | create table tiles ( 24 | zoom_level integer, 25 | tile_column integer, 26 | tile_row integer, 27 | tile_data blob); 28 | """) 29 | cur.execute("""create table metadata 30 | (name text, value text);""") 31 | cur.execute("""CREATE TABLE grids (zoom_level integer, tile_column integer, 32 | tile_row integer, grid blob);""") 33 | cur.execute("""CREATE TABLE grid_data (zoom_level integer, tile_column 34 | integer, tile_row integer, key_name text, key_json text);""") 35 | cur.execute("""create unique index name on metadata (name);""") 36 | cur.execute("""create unique index tile_index on tiles 37 | (zoom_level, tile_column, tile_row);""") 38 | 39 | 40 | def mbtiles_connect(mbtiles_file): 41 | try: 42 | con = sqlite3.connect(mbtiles_file, check_same_thread=False) 43 | return con 44 | except Exception as e: 45 | logger.error("Could not connect to database") 46 | logger.exception(e) 47 | sys.exit(1) 48 | 49 | 50 | def optimize_connection(cur): 51 | cur.execute("""PRAGMA synchronous=0""") 52 | cur.execute("""PRAGMA locking_mode=EXCLUSIVE""") 53 | cur.execute("""PRAGMA journal_mode=DELETE""") 54 | 55 | 56 | def compression_prepare(cur, con): 57 | cur.execute(""" 58 | CREATE TABLE if not exists images ( 59 | tile_data blob, 60 | tile_id VARCHAR(256)); 61 | """) 62 | cur.execute(""" 63 | CREATE TABLE if not exists map ( 64 | zoom_level integer, 65 | tile_column integer, 66 | tile_row integer, 67 | tile_id VARCHAR(256)); 68 | """) 69 | 70 | 71 | def optimize_database(cur): 72 | logger.debug("analyzing db") 73 | cur.execute("""ANALYZE;""") 74 | logger.debug("cleaning db") 75 | cur.execute("""VACUUM;""") 76 | 77 | 78 | def compression_do(cur, con, chunk): 79 | overlapping = 0 80 | unique = 0 81 | total = 0 82 | cur.execute("select count(zoom_level) from tiles") 83 | res = cur.fetchone() 84 | total_tiles = res[0] 85 | logging.debug("%d total tiles to fetch" % total_tiles) 86 | for i in range(total_tiles / chunk + 1): 87 | logging.debug("%d / %d rounds done" % (i, (total_tiles / chunk))) 88 | ids = [] 89 | files = [] 90 | start = time.time() 91 | cur.execute( 92 | """select zoom_level, tile_column, tile_row, tile_data 93 | from tiles where rowid > ? and rowid <= ?""", 94 | ((i * chunk), ((i + 1) * chunk)), 95 | ) 96 | logger.debug("select: %s" % (time.time() - start)) 97 | rows = cur.fetchall() 98 | for r in rows: 99 | total = total + 1 100 | if r[3] in files: 101 | overlapping = overlapping + 1 102 | start = time.time() 103 | query = """insert into map 104 | (zoom_level, tile_column, tile_row, tile_id) 105 | values (?, ?, ?, ?)""" 106 | logger.debug("insert: %s" % (time.time() - start)) 107 | cur.execute(query, (r[0], r[1], r[2], ids[files.index(r[3])])) 108 | else: 109 | unique = unique + 1 110 | id = str(uuid.uuid4()) 111 | 112 | ids.append(id) 113 | files.append(r[3]) 114 | 115 | start = time.time() 116 | query = """insert into images 117 | (tile_id, tile_data) 118 | values (?, ?)""" 119 | cur.execute(query, (str(id), sqlite3.Binary(r[3]))) 120 | logger.debug("insert into images: %s" % (time.time() - start)) 121 | start = time.time() 122 | query = """insert into map 123 | (zoom_level, tile_column, tile_row, tile_id) 124 | values (?, ?, ?, ?)""" 125 | cur.execute(query, (r[0], r[1], r[2], id)) 126 | logger.debug("insert into map: %s" % (time.time() - start)) 127 | con.commit() 128 | 129 | 130 | def compression_finalize(cur): 131 | cur.execute("""drop table tiles;""") 132 | cur.execute("""create view tiles as 133 | select map.zoom_level as zoom_level, 134 | map.tile_column as tile_column, 135 | map.tile_row as tile_row, 136 | images.tile_data as tile_data FROM 137 | map JOIN images on images.tile_id = map.tile_id;""") 138 | cur.execute(""" 139 | CREATE UNIQUE INDEX map_index on map 140 | (zoom_level, tile_column, tile_row);""") 141 | cur.execute(""" 142 | CREATE UNIQUE INDEX images_id on images 143 | (tile_id);""") 144 | cur.execute("""vacuum;""") 145 | cur.execute("""analyze;""") 146 | 147 | 148 | def getDirs(path): 149 | return [ 150 | name 151 | for name in os.listdir(path) 152 | if os.path.isdir(os.path.join(path, name)) 153 | ] 154 | 155 | 156 | def disk_to_mbtiles(directory_path, mbtiles_file, **kwargs): 157 | logger.info("Importing disk to MBTiles") 158 | logger.debug("%s --> %s" % (directory_path, mbtiles_file)) 159 | con = mbtiles_connect(mbtiles_file) 160 | cur = con.cursor() 161 | optimize_connection(cur) 162 | mbtiles_setup(cur) 163 | # ~ image_format = 'png' 164 | image_format = kwargs.get("format", "png") 165 | try: 166 | metadata = json.load( 167 | open(os.path.join(directory_path, "metadata.json"), "r") 168 | ) 169 | image_format = kwargs.get("format") 170 | for name, value in list(metadata.items()): 171 | cur.execute( 172 | "insert into metadata (name, value) values (?, ?)", 173 | (name, value), 174 | ) 175 | logger.info("metadata from metadata.json restored") 176 | except IOError: 177 | logger.warning("metadata.json not found") 178 | 179 | count = 0 180 | start_time = time.time() 181 | msg = "" 182 | 183 | for zoomDir in getDirs(directory_path): 184 | if kwargs.get("scheme") == "ags": 185 | if not "L" in zoomDir: 186 | logger.warning( 187 | "You appear to be using an ags scheme on an non-arcgis Server cache." 188 | ) 189 | z = int(zoomDir.replace("L", "")) 190 | else: 191 | if "L" in zoomDir: 192 | logger.warning( 193 | "You appear to be using a %s scheme on an arcgis Server cache. Try using --scheme=ags instead" 194 | % kwargs.get("scheme") 195 | ) 196 | z = int(zoomDir) 197 | for rowDir in getDirs(os.path.join(directory_path, zoomDir)): 198 | if kwargs.get("scheme") == "ags": 199 | y = flip_y(z, int(rowDir.replace("R", ""), 16)) 200 | else: 201 | x = int(rowDir) 202 | for current_file in os.listdir( 203 | os.path.join(directory_path, zoomDir, rowDir) 204 | ): 205 | file_name, ext = current_file.split(".", 1) 206 | f = open( 207 | os.path.join( 208 | directory_path, zoomDir, rowDir, current_file 209 | ), 210 | "rb", 211 | ) 212 | file_content = f.read() 213 | f.close() 214 | if kwargs.get("scheme") == "xyz": 215 | y = flip_y(int(z), int(file_name)) 216 | elif kwargs.get("scheme") == "ags": 217 | x = int(file_name.replace("C", ""), 16) 218 | else: 219 | y = int(file_name) 220 | 221 | if ext == image_format: 222 | logger.debug( 223 | " Read tile from Zoom (z): %i\tCol (x): %i\tRow (y): %i" 224 | % (z, x, y) 225 | ) 226 | cur.execute( 227 | """insert into tiles (zoom_level, 228 | tile_column, tile_row, tile_data) values 229 | (?, ?, ?, ?);""", 230 | (z, x, y, sqlite3.Binary(file_content)), 231 | ) 232 | count = count + 1 233 | if (count % 100) == 0: 234 | for c in msg: 235 | sys.stdout.write(chr(8)) 236 | msg = "%s tiles inserted (%d tiles/sec)" % ( 237 | count, 238 | count / (time.time() - start_time), 239 | ) 240 | sys.stdout.write(msg) 241 | elif ext == "grid.json": 242 | logger.debug( 243 | " Read grid from Zoom (z): %i\tCol (x): %i\tRow (y): %i" 244 | % (z, x, y) 245 | ) 246 | # Remove potential callback with regex 247 | has_callback = re.match( 248 | r"[\w\s=+-/]+\(({(.|\n)*})\);?", file_content 249 | ) 250 | if has_callback: 251 | file_content = has_callback.group(1) 252 | utfgrid = json.loads(file_content) 253 | 254 | data = utfgrid.pop("data") 255 | compressed = zlib.compress(json.dumps(utfgrid)) 256 | cur.execute( 257 | """insert into grids (zoom_level, tile_column, tile_row, grid) values (?, ?, ?, ?) """, 258 | (z, x, y, sqlite3.Binary(compressed)), 259 | ) 260 | grid_keys = [k for k in utfgrid["keys"] if k != ""] 261 | for key_name in grid_keys: 262 | key_json = data[key_name] 263 | cur.execute( 264 | """insert into grid_data (zoom_level, tile_column, tile_row, key_name, key_json) values (?, ?, ?, ?, ?);""", 265 | (z, x, y, key_name, json.dumps(key_json)), 266 | ) 267 | 268 | logger.debug("tiles (and grids) inserted.") 269 | optimize_database(con) 270 | 271 | 272 | def mbtiles_to_disk(mbtiles_file, directory_path, **kwargs): 273 | logger.debug("Exporting MBTiles to disk") 274 | logger.debug("%s --> %s" % (mbtiles_file, directory_path)) 275 | con = mbtiles_connect(mbtiles_file) 276 | os.mkdir("%s" % directory_path) 277 | metadata = dict( 278 | con.execute("select name, value from metadata;").fetchall() 279 | ) 280 | json.dump( 281 | metadata, 282 | open(os.path.join(directory_path, "metadata.json"), "w"), 283 | indent=4, 284 | ) 285 | count = con.execute("select count(zoom_level) from tiles;").fetchone()[0] 286 | done = 0 287 | msg = "" 288 | base_path = directory_path 289 | if not os.path.isdir(base_path): 290 | os.makedirs(base_path) 291 | 292 | # if interactivity 293 | formatter = metadata.get("formatter") 294 | if formatter: 295 | layer_json = os.path.join(base_path, "layer.json") 296 | formatter_json = {"formatter": formatter} 297 | open(layer_json, "w").write("grid(" + json.dumps(formatter_json) + ")") 298 | 299 | tiles = con.execute( 300 | "select zoom_level, tile_column, tile_row, tile_data from tiles;" 301 | ) 302 | t = tiles.fetchone() 303 | while t: 304 | z = t[0] 305 | x = t[1] 306 | y = t[2] 307 | if kwargs.get("scheme") == "xyz": 308 | y = flip_y(z, y) 309 | print("flipping") 310 | tile_dir = os.path.join(base_path, str(z), str(x)) 311 | elif kwargs.get("scheme") == "wms": 312 | tile_dir = os.path.join( 313 | base_path, 314 | "%02d" % (z), 315 | "%03d" % (int(x) / 1000000), 316 | "%03d" % ((int(x) / 1000) % 1000), 317 | "%03d" % (int(x) % 1000), 318 | "%03d" % (int(y) / 1000000), 319 | "%03d" % ((int(y) / 1000) % 1000), 320 | ) 321 | else: 322 | tile_dir = os.path.join(base_path, str(z), str(x)) 323 | if not os.path.isdir(tile_dir): 324 | os.makedirs(tile_dir) 325 | if kwargs.get("scheme") == "wms": 326 | tile = os.path.join( 327 | tile_dir, 328 | "%03d.%s" % (int(y) % 1000, kwargs.get("format", "png")), 329 | ) 330 | else: 331 | tile = os.path.join( 332 | tile_dir, "%s.%s" % (y, kwargs.get("format", "png")) 333 | ) 334 | f = open(tile, "wb") 335 | f.write(t[3]) 336 | f.close() 337 | done = done + 1 338 | for c in msg: 339 | sys.stdout.write(chr(8)) 340 | logger.info("%s / %s tiles exported" % (done, count)) 341 | t = tiles.fetchone() 342 | 343 | # grids 344 | callback = kwargs.get("callback") 345 | done = 0 346 | msg = "" 347 | try: 348 | count = con.execute("select count(zoom_level) from grids;").fetchone()[ 349 | 0 350 | ] 351 | grids = con.execute( 352 | "select zoom_level, tile_column, tile_row, grid from grids;" 353 | ) 354 | g = grids.fetchone() 355 | except sqlite3.OperationalError: 356 | g = None # no grids table 357 | while g: 358 | zoom_level = g[0] # z 359 | tile_column = g[1] # x 360 | y = g[2] # y 361 | grid_data_cursor = con.execute( 362 | """select key_name, key_json FROM 363 | grid_data WHERE 364 | zoom_level = %(zoom_level)d and 365 | tile_column = %(tile_column)d and 366 | tile_row = %(y)d;""" 367 | % locals() 368 | ) 369 | if kwargs.get("scheme") == "xyz": 370 | y = flip_y(zoom_level, y) 371 | grid_dir = os.path.join(base_path, str(zoom_level), str(tile_column)) 372 | if not os.path.isdir(grid_dir): 373 | os.makedirs(grid_dir) 374 | grid = os.path.join(grid_dir, "%s.grid.json" % (y)) 375 | f = open(grid, "w") 376 | grid_json = json.loads(zlib.decompress(g[3])) 377 | # join up with the grid 'data' which is in pieces when stored in mbtiles file 378 | grid_data = grid_data_cursor.fetchone() 379 | data = {} 380 | while grid_data: 381 | data[grid_data[0]] = json.loads(grid_data[1]) 382 | grid_data = grid_data_cursor.fetchone() 383 | grid_json["data"] = data 384 | if callback in (None, "", "false", "null"): 385 | f.write(json.dumps(grid_json)) 386 | else: 387 | f.write("%s(%s);" % (callback, json.dumps(grid_json))) 388 | f.close() 389 | done = done + 1 390 | for c in msg: 391 | sys.stdout.write(chr(8)) 392 | logger.info("%s / %s grids exported" % (done, count)) 393 | g = grids.fetchone() 394 | -------------------------------------------------------------------------------- /src/qtiles/metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=QTiles 3 | description=Generate map tiles from a QGIS project 4 | description[ru]=Создавайте растровые тайлы из проекта QGIS 5 | about=Generates raster tiles from QGIS project for selected zoom levels and tile naming conventions (Slippy Map or TMS). Package tiles for NextGIS Mobile, GeoPaparazzi, simple Leaflet-based viewer or MBTiles. Developed by NextGIS. Any feedback is welcome at https://nextgis.com/contact 6 | about[ru]=Создает растровые тайлы из проекта QGIS для выбранных уровней масштабирования в соответствии с соглашениями о наименовании тайлов (Slippy Map или TMS). Набор тайлов для NextGIS Mobile, GeoPaparazzi, простого просмотра карт на основе Leaflet или MBTiles. Разработан NextGIS. Любые отзывы приветствуются на https://nextgis.com/contact 7 | category=Plugins 8 | version=1.8.0 9 | qgisMinimumVersion=3.22 10 | qgisMaximumVersion=3.99 11 | 12 | author=NextGIS 13 | email=info@nextgis.com 14 | 15 | changelog= 16 | 1.8.0 17 | * Updated the "About plugin" dialog 18 | * Added plugin item to help menu 19 | 1.7.2 20 | * Fix rounding error on Python 3.10 21 | 1.7.1 22 | * Fixed file selection dialog 23 | 1.7.0 24 | * Fixed bugs 25 | 1.6.0 26 | * QGIS 3 support added 27 | 1.5.5 28 | * Fix rendering of tiles outside of layer extent 29 | * Fix qgis warnings 30 | 1.5.4 31 | * Allow JPG as format for NGRC 32 | 1.5.3 33 | * Fix problem with 65356 tiles limit 34 | 1.5.2 35 | * Removed the limitation of the maximum zoom 36 | * Host css+js in local repository for LeafLet preview 37 | 1.5.1: 38 | * create tiles for NextGIS Mobile 39 | * add MBTiles compression 40 | * add export MBTiles metadata to .json file 41 | * add image overview for MBTiles 42 | * add option for skiping tiles outside of layers extents (within combined extent) 43 | 1.5.0: 44 | * change MBTiles parameters vаlues: format in lower case, description is 'Created with QTiles' 45 | * tiles are now produced correctly when transparency is set 46 | * geojson is now rendered correctly 47 | * CRS shift when using 3857 is fixed 48 | 1.4.6: 49 | * works fine now with non-english characters in folder names 50 | * add MBTiles initialize arguments for Geopaparazzi4 51 | * take into account the actual zoom level when generating tiles 52 | 53 | icon=icons/qtiles.png 54 | 55 | tags=raster,tiles 56 | 57 | homepage=https://github.com/nextgis/QTiles 58 | tracker=https://github.com/nextgis/QTiles/issues 59 | repository=https://github.com/nextgis/QTiles 60 | video=https://www.youtube.com/watch?v=vU4bGCh5khM 61 | video[ru]=https://www.youtube.com/watch?v=Lk-i4Az0SEo 62 | 63 | experimental=False 64 | deprecated=False 65 | -------------------------------------------------------------------------------- /src/qtiles/qtiles.py: -------------------------------------------------------------------------------- 1 | # ****************************************************************************** 2 | # 3 | # QTiles 4 | # --------------------------------------------------------- 5 | # Generates tiles from QGIS project 6 | # 7 | # Copyright (C) 2012-2022 NextGIS (info@nextgis.com) 8 | # 9 | # This source is free software; you can redistribute it and/or modify it under 10 | # the terms of the GNU General Public License as published by the Free 11 | # Software Foundation, either version 2 of the License, or (at your option) 12 | # any later version. 13 | # 14 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 15 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 | # details. 18 | # 19 | # A copy of the GNU General Public License is available on the World Wide Web 20 | # at . You can also obtain it by writing 21 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 22 | # MA 02110-1335 USA. 23 | # 24 | # ****************************************************************************** 25 | 26 | 27 | from qgis.core import * 28 | from qgis.PyQt.QtCore import ( 29 | QCoreApplication, 30 | QFileInfo, 31 | QLocale, 32 | QSettings, 33 | QTranslator, 34 | ) 35 | from qgis.PyQt.QtGui import QIcon 36 | from qgis.PyQt.QtWidgets import QAction, QMessageBox 37 | from pathlib import Path 38 | 39 | from . import aboutdialog, qtilesdialog, resources_rc # noqa: F401 40 | from .compat import QGis, qgisUserDatabaseFilePath 41 | 42 | 43 | class QTilesPlugin: 44 | def __init__(self, iface): 45 | self.iface = iface 46 | 47 | self.qgsVersion = str(QGis.QGIS_VERSION_INT) 48 | 49 | userPluginPath = ( 50 | QFileInfo(qgisUserDatabaseFilePath()).path() 51 | + "/python/plugins/qtiles" 52 | ) 53 | systemPluginPath = ( 54 | QgsApplication.prefixPath() + "/python/plugins/qtiles" 55 | ) 56 | 57 | overrideLocale = QSettings().value( 58 | "locale/overrideFlag", False, type=bool 59 | ) 60 | if not overrideLocale: 61 | localeFullName = QLocale.system().name() 62 | else: 63 | localeFullName = QSettings().value("locale/userLocale", "") 64 | 65 | if QFileInfo(userPluginPath).exists(): 66 | translationPath = ( 67 | userPluginPath + "/i18n/qtiles_" + localeFullName + ".qm" 68 | ) 69 | else: 70 | translationPath = ( 71 | systemPluginPath + "/i18n/qtiles_" + localeFullName + ".qm" 72 | ) 73 | 74 | self.localePath = translationPath 75 | if QFileInfo(self.localePath).exists(): 76 | self.translator = QTranslator() 77 | self.translator.load(self.localePath) 78 | QCoreApplication.installTranslator(self.translator) 79 | 80 | def initGui(self): 81 | if int(self.qgsVersion) < 20000: 82 | qgisVersion = ( 83 | self.qgsVersion[0] 84 | + "." 85 | + self.qgsVersion[2] 86 | + "." 87 | + self.qgsVersion[3] 88 | ) 89 | QMessageBox.warning( 90 | self.iface.mainWindow(), 91 | QCoreApplication.translate("QTiles", "Error"), 92 | QCoreApplication.translate("QTiles", "QGIS %s detected.\n") 93 | % qgisVersion 94 | + QCoreApplication.translate( 95 | "QTiles", 96 | "This version of QTiles requires at least QGIS 2.0. Plugin will not be enabled.", 97 | ), 98 | ) 99 | return None 100 | 101 | self.actionRun = QAction( 102 | QCoreApplication.translate("QTiles", "QTiles"), 103 | self.iface.mainWindow(), 104 | ) 105 | self.iface.registerMainWindowAction(self.actionRun, "Shift+T") 106 | self.actionRun.setIcon(QIcon(":/plugins/qtiles/icons/qtiles.png")) 107 | self.actionRun.setWhatsThis("Generate tiles from current project") 108 | self.actionAbout = QAction( 109 | QCoreApplication.translate("QTiles", "About QTiles..."), 110 | self.iface.mainWindow(), 111 | ) 112 | self.actionAbout.setIcon(QIcon(":/plugins/qtiles/icons/about.png")) 113 | self.actionAbout.setWhatsThis("About QTiles") 114 | 115 | self.iface.addPluginToMenu( 116 | QCoreApplication.translate("QTiles", "QTiles"), self.actionRun 117 | ) 118 | self.iface.addPluginToMenu( 119 | QCoreApplication.translate("QTiles", "QTiles"), self.actionAbout 120 | ) 121 | self.iface.addToolBarIcon(self.actionRun) 122 | 123 | self.actionRun.triggered.connect(self.run) 124 | self.actionAbout.triggered.connect(self.about) 125 | 126 | self.__show_help_action = QAction( 127 | QIcon(":/plugins/qtiles/icons/qtiles.png"), 128 | "QTiles", 129 | ) 130 | self.__show_help_action.triggered.connect(self.about) 131 | plugin_help_menu = self.iface.pluginHelpMenu() 132 | assert plugin_help_menu is not None 133 | plugin_help_menu.addAction(self.__show_help_action) 134 | 135 | def unload(self): 136 | self.iface.unregisterMainWindowAction(self.actionRun) 137 | 138 | self.iface.removeToolBarIcon(self.actionRun) 139 | self.iface.removePluginMenu( 140 | QCoreApplication.translate("QTiles", "QTiles"), self.actionRun 141 | ) 142 | self.iface.removePluginMenu( 143 | QCoreApplication.translate("QTiles", "QTiles"), self.actionAbout 144 | ) 145 | 146 | def run(self): 147 | d = qtilesdialog.QTilesDialog(self.iface) 148 | d.show() 149 | d.exec() 150 | 151 | def about(self): 152 | package_name = str(Path(__file__).parent.name) 153 | d = aboutdialog.AboutDialog(package_name) 154 | d.exec() 155 | -------------------------------------------------------------------------------- /src/qtiles/qtiles_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ****************************************************************************** 4 | # 5 | # QTiles 6 | # --------------------------------------------------------- 7 | # Generates tiles from QGIS project 8 | # 9 | # Copyright (C) 2012-2022 NextGIS (info@nextgis.com) 10 | # 11 | # This source is free software; you can redistribute it and/or modify it under 12 | # the terms of the GNU General Public License as published by the Free 13 | # Software Foundation, either version 2 of the License, or (at your option) 14 | # any later version. 15 | # 16 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 17 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 19 | # details. 20 | # 21 | # A copy of the GNU General Public License is available on the World Wide Web 22 | # at . You can also obtain it by writing 23 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 24 | # MA 02110-1335 USA. 25 | # 26 | # ****************************************************************************** 27 | 28 | 29 | from qgis.PyQt.QtCore import * 30 | from qgis.core import * 31 | 32 | from .compat import mapLayers 33 | 34 | 35 | def getMapLayers(): 36 | layers = dict() 37 | for name, layer in list(mapLayers().items()): 38 | if layer.type() == QgsMapLayer.VectorLayer: 39 | if layer.id() not in list(layers.keys()): 40 | layers[layer.id()] = str(layer.name()) 41 | if ( 42 | layer.type() == QgsMapLayer.RasterLayer 43 | and layer.providerType() == "gdal" 44 | ): 45 | if layer.id() not in list(layers.keys()): 46 | layers[layer.id()] = str(layer.name()) 47 | return layers 48 | 49 | 50 | def getLayerById(layerId): 51 | for name, layer in list(mapLayers().items()): 52 | if layer.id() == layerId: 53 | if layer.isValid(): 54 | return layer 55 | else: 56 | return None 57 | 58 | 59 | def getLayerGroup(layerId): 60 | return ( 61 | QgsProject.instance() 62 | .layerTreeRoot() 63 | .findLayer(layerId) 64 | .parent() 65 | .name() 66 | ) 67 | -------------------------------------------------------------------------------- /src/qtiles/qtilesdialog.py: -------------------------------------------------------------------------------- 1 | # ****************************************************************************** 2 | # 3 | # QTiles 4 | # --------------------------------------------------------- 5 | # Generates tiles from QGIS project 6 | # 7 | # Copyright (C) 2012-2022 NextGIS (info@nextgis.com) 8 | # 9 | # This source is free software; you can redistribute it and/or modify it under 10 | # the terms of the GNU General Public License as published by the Free 11 | # Software Foundation, either version 2 of the License, or (at your option) 12 | # any later version. 13 | # 14 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 15 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 | # details. 18 | # 19 | # A copy of the GNU General Public License is available on the World Wide Web 20 | # at . You can also obtain it by writing 21 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 22 | # MA 02110-1335 USA. 23 | # 24 | # ****************************************************************************** 25 | import math 26 | import operator 27 | import os 28 | 29 | from qgis.core import QgsRectangle 30 | from qgis.PyQt import uic 31 | from qgis.PyQt.QtCore import QDir, QFileInfo, Qt, pyqtSlot 32 | from qgis.PyQt.QtGui import QIcon 33 | from qgis.PyQt.QtWidgets import ( 34 | QDialog, 35 | QDialogButtonBox, 36 | QFileDialog, 37 | QMessageBox, 38 | ) 39 | 40 | from . import qtiles_utils as utils 41 | from . import tilingthread 42 | from .compat import ( 43 | QgsCoordinateReferenceSystem, 44 | QgsCoordinateTransform, 45 | QgsSettings, 46 | getSaveFileName, 47 | ) 48 | 49 | FORM_CLASS, _ = uic.loadUiType( 50 | os.path.join(os.path.dirname(__file__), "ui/qtilesdialogbase.ui") 51 | ) 52 | 53 | 54 | class QTilesDialog(QDialog, FORM_CLASS): 55 | # MAX_ZOOM_LEVEL = 18 56 | MIN_ZOOM_LEVEL = 0 57 | 58 | def __init__(self, iface): 59 | QDialog.__init__(self) 60 | self.setupUi(self) 61 | 62 | self.btnOk = self.buttonBox.addButton( 63 | self.tr("Run"), QDialogButtonBox.AcceptRole 64 | ) 65 | 66 | # self.spnZoomMax.setMaximum(self.MAX_ZOOM_LEVEL) 67 | self.spnZoomMax.setMinimum(self.MIN_ZOOM_LEVEL) 68 | # self.spnZoomMin.setMaximum(self.MAX_ZOOM_LEVEL) 69 | self.spnZoomMin.setMinimum(self.MIN_ZOOM_LEVEL) 70 | 71 | self.spnZoomMin.valueChanged.connect(self.spnZoomMax.setMinimum) 72 | self.spnZoomMax.valueChanged.connect(self.spnZoomMin.setMaximum) 73 | 74 | self.iface = iface 75 | 76 | self.verticalLayout_2.setAlignment(Qt.AlignTop) 77 | 78 | self.workThread = None 79 | 80 | self.FORMATS = { 81 | self.tr("ZIP archives (*.zip *.ZIP)"): ".zip", 82 | self.tr("MBTiles databases (*.mbtiles *.MBTILES)"): ".mbtiles", 83 | } 84 | 85 | self.settings = QgsSettings("NextGIS", "QTiles") 86 | self.grpParameters.setSettings(self.settings) 87 | self.btnClose = self.buttonBox.button(QDialogButtonBox.Close) 88 | self.rbExtentLayer.toggled.connect(self.__toggleLayerSelector) 89 | self.chkLockRatio.stateChanged.connect(self.__toggleHeightEdit) 90 | self.spnTileWidth.valueChanged.connect(self.__updateTileSize) 91 | self.btnBrowse.clicked.connect(self.__select_output) 92 | self.cmbFormat.activated.connect(self.formatChanged) 93 | 94 | self.rbOutputZip.toggled.connect(self.__toggleTarget) 95 | self.rbOutputDir.toggled.connect(self.__toggleTarget) 96 | self.rbOutputNGM.toggled.connect(self.__toggleTarget) 97 | self.rbOutputNGM.setIcon( 98 | QIcon(":/plugins/qtiles/icons/ngm_index_24x24.png") 99 | ) 100 | 101 | self.lInfoIconOutputZip.linkActivated.connect(self.show_output_info) 102 | self.lInfoIconOutputDir.linkActivated.connect(self.show_output_info) 103 | self.lInfoIconOutputNGM.linkActivated.connect(self.show_output_info) 104 | 105 | self.manageGui() 106 | 107 | def show_output_info(self, href): 108 | title = self.tr("Output type info") 109 | message = "" 110 | if self.sender() is self.lInfoIconOutputZip: 111 | message = self.tr("Save tiles as Zip or MBTiles") 112 | elif self.sender() is self.lInfoIconOutputDir: 113 | message = self.tr("Save tiles as directory structure") 114 | elif self.sender() is self.lInfoIconOutputNGM: 115 | message = ( 116 | " \ 117 | \ 120 | \ 123 |
\ 118 | \ 119 | \ 121 | %s \ 122 |
" 124 | % self.tr( 125 | "Prepare package for NextGIS Mobile " 126 | ) 127 | ) 128 | 129 | # QMessageBox.information( 130 | # self, 131 | # title, 132 | # message 133 | # ) 134 | msgBox = QMessageBox() 135 | msgBox.setWindowTitle(title) 136 | msgBox.setText(message) 137 | msgBox.exec() 138 | 139 | def formatChanged(self): 140 | if self.cmbFormat.currentText() == "JPG": 141 | self.spnTransparency.setEnabled(False) 142 | self.spnQuality.setEnabled(True) 143 | else: 144 | self.spnTransparency.setEnabled(True) 145 | self.spnQuality.setEnabled(False) 146 | 147 | def manageGui(self): 148 | layers = utils.getMapLayers() 149 | for layer in sorted( 150 | iter(list(layers.items())), key=operator.itemgetter(1) 151 | ): 152 | groupName = utils.getLayerGroup(layer[0]) 153 | if groupName == "": 154 | self.cmbLayers.addItem(layer[1], layer[0]) 155 | else: 156 | self.cmbLayers.addItem( 157 | "%s - %s" % (layer[1], groupName), layer[0] 158 | ) 159 | 160 | self.rbOutputZip.setChecked( 161 | self.settings.value("outputToZip", True, type=bool) 162 | ) 163 | self.rbOutputDir.setChecked( 164 | self.settings.value("outputToDir", False, type=bool) 165 | ) 166 | self.rbOutputNGM.setChecked( 167 | self.settings.value("outputToNGM", False, type=bool) 168 | ) 169 | if self.rbOutputZip.isChecked(): 170 | self.leDirectoryName.setEnabled(False) 171 | self.leTilesFroNGM.setEnabled(False) 172 | elif self.rbOutputDir.isChecked(): 173 | self.leZipFileName.setEnabled(False) 174 | self.leTilesFroNGM.setEnabled(False) 175 | elif self.rbOutputNGM.isChecked(): 176 | self.leZipFileName.setEnabled(False) 177 | self.leDirectoryName.setEnabled(False) 178 | else: 179 | self.leZipFileName.setEnabled(False) 180 | self.leDirectoryName.setEnabled(False) 181 | self.leTilesFroNGM.setEnabled(False) 182 | 183 | self.leZipFileName.setText(self.settings.value("outputToZip_Path", "")) 184 | self.leDirectoryName.setText( 185 | self.settings.value("outputToDir_Path", "") 186 | ) 187 | self.leTilesFroNGM.setText(self.settings.value("outputToNGM_Path", "")) 188 | 189 | self.cmbLayers.setEnabled(False) 190 | self.leRootDir.setText(self.settings.value("rootDir", "Mapnik")) 191 | self.rbExtentCanvas.setChecked( 192 | self.settings.value("extentCanvas", True, type=bool) 193 | ) 194 | self.rbExtentFull.setChecked( 195 | self.settings.value("extentFull", False, type=bool) 196 | ) 197 | self.rbExtentLayer.setChecked( 198 | self.settings.value("extentLayer", False, type=bool) 199 | ) 200 | self.spnZoomMin.setValue(self.settings.value("minZoom", 0, type=int)) 201 | self.spnZoomMax.setValue(self.settings.value("maxZoom", 18, type=int)) 202 | self.chkLockRatio.setChecked( 203 | self.settings.value("keepRatio", True, type=bool) 204 | ) 205 | self.spnTileWidth.setValue( 206 | self.settings.value("tileWidth", 256, type=int) 207 | ) 208 | self.spnTileHeight.setValue( 209 | self.settings.value("tileHeight", 256, type=int) 210 | ) 211 | self.spnTransparency.setValue( 212 | self.settings.value("transparency", 255, type=int) 213 | ) 214 | self.spnQuality.setValue(self.settings.value("quality", 70, type=int)) 215 | self.cmbFormat.setCurrentIndex(int(self.settings.value("format", 0))) 216 | self.chkAntialiasing.setChecked( 217 | self.settings.value("enable_antialiasing", False, type=bool) 218 | ) 219 | self.chkTMSConvention.setChecked( 220 | self.settings.value("use_tms_filenames", False, type=bool) 221 | ) 222 | self.chkMBTilesCompression.setChecked( 223 | self.settings.value("use_mbtiles_compression", False, type=bool) 224 | ) 225 | self.chkWriteJson.setChecked( 226 | self.settings.value("write_json", False, type=bool) 227 | ) 228 | self.chkWriteOverview.setChecked( 229 | self.settings.value("write_overview", False, type=bool) 230 | ) 231 | self.chkWriteMapurl.setChecked( 232 | self.settings.value("write_mapurl", False, type=bool) 233 | ) 234 | self.chkWriteViewer.setChecked( 235 | self.settings.value("write_viewer", False, type=bool) 236 | ) 237 | self.chkRenderOutsideTiles.setChecked( 238 | self.settings.value("renderOutsideTiles", True, type=bool) 239 | ) 240 | 241 | self.formatChanged() 242 | 243 | def reject(self): 244 | QDialog.reject(self) 245 | 246 | def accept(self): 247 | if self.rbOutputZip.isChecked(): 248 | output = self.leZipFileName.text() 249 | elif self.rbOutputDir.isChecked(): 250 | output = self.leDirectoryName.text() 251 | if not QFileInfo(output).exists(): 252 | os.mkdir(QFileInfo(output).absoluteFilePath()) 253 | elif self.rbOutputNGM.isChecked(): 254 | output = self.leTilesFroNGM.text() 255 | 256 | if ( 257 | self.rbExtentLayer.isChecked() 258 | and self.cmbLayers.currentIndex() < 0 259 | ): 260 | QMessageBox.warning( 261 | self, 262 | self.tr("Layer not selected"), 263 | self.tr("Please select a layer and try again."), 264 | ) 265 | return 266 | 267 | if not output: 268 | QMessageBox.warning( 269 | self, 270 | self.tr("No output"), 271 | self.tr( 272 | "Output path is not set. Please enter correct path and try again." 273 | ), 274 | ) 275 | return 276 | fileInfo = QFileInfo(output) 277 | if ( 278 | fileInfo.isDir() 279 | and not len( 280 | QDir(output).entryList( 281 | QDir.Dirs | QDir.Files | QDir.NoDotAndDotDot 282 | ) 283 | ) 284 | == 0 285 | ): 286 | res = QMessageBox.warning( 287 | self, 288 | self.tr("Directory not empty"), 289 | self.tr("Selected directory is not empty. Continue?"), 290 | QMessageBox.Yes | QMessageBox.No, 291 | ) 292 | if res == QMessageBox.No: 293 | return 294 | 295 | if self.spnZoomMin.value() > self.spnZoomMax.value(): 296 | QMessageBox.warning( 297 | self, 298 | self.tr("Wrong zoom"), 299 | self.tr( 300 | "Maximum zoom value is lower than minimum. Please correct this and try again." 301 | ), 302 | ) 303 | return 304 | self.settings.setValue("rootDir", self.leRootDir.text()) 305 | self.settings.setValue("outputToZip", self.rbOutputZip.isChecked()) 306 | self.settings.setValue("outputToDir", self.rbOutputDir.isChecked()) 307 | self.settings.setValue("outputToNGM", self.rbOutputNGM.isChecked()) 308 | self.settings.setValue("extentCanvas", self.rbExtentCanvas.isChecked()) 309 | self.settings.setValue("extentFull", self.rbExtentFull.isChecked()) 310 | self.settings.setValue("extentLayer", self.rbExtentLayer.isChecked()) 311 | self.settings.setValue("minZoom", self.spnZoomMin.value()) 312 | self.settings.setValue("maxZoom", self.spnZoomMax.value()) 313 | self.settings.setValue("keepRatio", self.chkLockRatio.isChecked()) 314 | self.settings.setValue("tileWidth", self.spnTileWidth.value()) 315 | self.settings.setValue("tileHeight", self.spnTileHeight.value()) 316 | self.settings.setValue("format", self.cmbFormat.currentIndex()) 317 | self.settings.setValue("transparency", self.spnTransparency.value()) 318 | self.settings.setValue("quality", self.spnQuality.value()) 319 | self.settings.setValue( 320 | "enable_antialiasing", self.chkAntialiasing.isChecked() 321 | ) 322 | self.settings.setValue( 323 | "use_tms_filenames", self.chkTMSConvention.isChecked() 324 | ) 325 | self.settings.setValue( 326 | "use_mbtiles_compression", self.chkMBTilesCompression.isChecked() 327 | ) 328 | self.settings.setValue("write_json", self.chkWriteJson.isChecked()) 329 | self.settings.setValue( 330 | "write_overview", self.chkWriteOverview.isChecked() 331 | ) 332 | self.settings.setValue("write_mapurl", self.chkWriteMapurl.isChecked()) 333 | self.settings.setValue("write_viewer", self.chkWriteViewer.isChecked()) 334 | self.settings.setValue( 335 | "renderOutsideTiles", self.chkRenderOutsideTiles.isChecked() 336 | ) 337 | canvas = self.iface.mapCanvas() 338 | if self.rbExtentCanvas.isChecked(): 339 | extent = canvas.extent() 340 | elif self.rbExtentFull.isChecked(): 341 | extent = canvas.fullExtent() 342 | else: 343 | layer = utils.getLayerById( 344 | self.cmbLayers.itemData(self.cmbLayers.currentIndex()) 345 | ) 346 | extent = canvas.mapSettings().layerExtentToOutputExtent( 347 | layer, layer.extent() 348 | ) 349 | 350 | extent = QgsCoordinateTransform( 351 | canvas.mapSettings().destinationCrs(), 352 | QgsCoordinateReferenceSystem.fromEpsgId(4326), 353 | ).transform(extent) 354 | 355 | arctanSinhPi = math.degrees(math.atan(math.sinh(math.pi))) 356 | extent = extent.intersect( 357 | QgsRectangle(-180, -arctanSinhPi, 180, arctanSinhPi) 358 | ) 359 | layers = canvas.layers() 360 | writeMapurl = ( 361 | self.chkWriteMapurl.isEnabled() and self.chkWriteMapurl.isChecked() 362 | ) 363 | writeViewer = ( 364 | self.chkWriteViewer.isEnabled() and self.chkWriteViewer.isChecked() 365 | ) 366 | self.workThread = tilingthread.TilingThread( 367 | layers, 368 | extent, 369 | self.spnZoomMin.value(), 370 | self.spnZoomMax.value(), 371 | self.spnTileWidth.value(), 372 | self.spnTileHeight.value(), 373 | self.spnTransparency.value(), 374 | self.spnQuality.value(), 375 | self.cmbFormat.currentText(), 376 | fileInfo, 377 | self.leRootDir.text(), 378 | self.chkAntialiasing.isChecked(), 379 | self.chkTMSConvention.isChecked(), 380 | self.chkMBTilesCompression.isChecked(), 381 | self.chkWriteJson.isChecked(), 382 | self.chkWriteOverview.isChecked(), 383 | self.chkRenderOutsideTiles.isChecked(), 384 | writeMapurl, 385 | writeViewer, 386 | ) 387 | 388 | self.workThread.rangeChanged.connect(self.setProgressRange) 389 | self.workThread.updateProgress.connect(self.updateProgress) 390 | self.workThread.processFinished.connect(self.processFinished) 391 | self.workThread.processInterrupted.connect(self.processInterrupted) 392 | self.workThread.threshold.connect(self.confirmContinueThreshold) 393 | self.btnOk.setEnabled(False) 394 | self.btnClose.setText(self.tr("Cancel")) 395 | self.buttonBox.rejected.disconnect(self.reject) 396 | self.btnClose.clicked.connect(self.stopProcessing) 397 | self.workThread.start() 398 | 399 | def confirmContinueThreshold(self, tilesCountThreshold): 400 | res = QMessageBox.question( 401 | self.parent(), 402 | self.tr("Confirmation"), 403 | self.tr("Estimate number of tiles more then %d! Continue?") 404 | % tilesCountThreshold, 405 | QMessageBox.Yes | QMessageBox.No, 406 | ) 407 | 408 | if res == QMessageBox.Yes: 409 | self.workThread.confirmContinue() 410 | else: 411 | self.workThread.confirmStop() 412 | 413 | def setProgressRange(self, message, value): 414 | self.progressBar.setFormat(message) 415 | self.progressBar.setRange(0, value) 416 | 417 | def updateProgress(self): 418 | self.progressBar.setValue(self.progressBar.value() + 1) 419 | 420 | def processInterrupted(self): 421 | self.restoreGui() 422 | 423 | def processFinished(self): 424 | self.stopProcessing() 425 | self.restoreGui() 426 | 427 | def stopProcessing(self): 428 | if self.workThread is not None: 429 | self.workThread.stop() 430 | self.workThread = None 431 | 432 | def restoreGui(self): 433 | self.progressBar.setFormat("%p%") 434 | self.progressBar.setRange(0, 1) 435 | self.progressBar.setValue(0) 436 | self.buttonBox.rejected.connect(self.reject) 437 | self.btnClose.clicked.disconnect(self.stopProcessing) 438 | self.btnClose.setText(self.tr("Close")) 439 | self.btnOk.setEnabled(True) 440 | 441 | def __toggleTarget(self, checked): 442 | if checked: 443 | if self.sender() is self.rbOutputZip: 444 | self.leZipFileName.setEnabled(True) 445 | self.leDirectoryName.setEnabled(False) 446 | self.leTilesFroNGM.setEnabled(False) 447 | self.chkWriteMapurl.setEnabled(False) 448 | self.chkWriteViewer.setEnabled(False) 449 | self.chkWriteJson.setEnabled(True) 450 | 451 | self.spnTileWidth.setEnabled(True) 452 | self.chkLockRatio.setEnabled(True) 453 | self.cmbFormat.setEnabled(True) 454 | self.chkMBTilesCompression.setEnabled(True) 455 | 456 | self.chkWriteOverview.setEnabled(True) 457 | elif self.sender() is self.rbOutputDir: 458 | self.leZipFileName.setEnabled(False) 459 | self.leDirectoryName.setEnabled(True) 460 | self.leTilesFroNGM.setEnabled(False) 461 | self.chkWriteMapurl.setEnabled(True) 462 | self.chkWriteViewer.setEnabled(True) 463 | self.chkWriteJson.setEnabled(True) 464 | self.chkMBTilesCompression.setEnabled(False) 465 | 466 | self.spnTileWidth.setEnabled(True) 467 | self.chkLockRatio.setEnabled(True) 468 | self.cmbFormat.setEnabled(True) 469 | 470 | self.chkWriteOverview.setEnabled(True) 471 | elif self.sender() is self.rbOutputNGM: 472 | self.leZipFileName.setEnabled(False) 473 | self.leDirectoryName.setEnabled(False) 474 | self.leTilesFroNGM.setEnabled(True) 475 | self.chkWriteMapurl.setEnabled(False) 476 | self.chkWriteViewer.setEnabled(False) 477 | self.chkMBTilesCompression.setEnabled(False) 478 | 479 | self.spnTileWidth.setValue(256) 480 | self.spnTileWidth.setEnabled(False) 481 | self.chkLockRatio.setCheckState(Qt.Checked) 482 | self.chkLockRatio.setEnabled(False) 483 | self.cmbFormat.setCurrentIndex(0) 484 | self.cmbFormat.setEnabled(True) 485 | 486 | self.chkWriteOverview.setChecked(False) 487 | self.chkWriteOverview.setEnabled(False) 488 | 489 | self.chkWriteJson.setChecked(False) 490 | self.chkWriteJson.setEnabled(False) 491 | 492 | def __toggleLayerSelector(self, checked): 493 | self.cmbLayers.setEnabled(checked) 494 | 495 | def __toggleHeightEdit(self, state): 496 | if state == Qt.Checked: 497 | self.lblHeight.setEnabled(False) 498 | self.spnTileHeight.setEnabled(False) 499 | self.spnTileHeight.setValue(self.spnTileWidth.value()) 500 | else: 501 | self.lblHeight.setEnabled(True) 502 | self.spnTileHeight.setEnabled(True) 503 | 504 | @pyqtSlot(int) 505 | def __updateTileSize(self, value): 506 | if self.chkLockRatio.isChecked(): 507 | self.spnTileHeight.setValue(value) 508 | 509 | def __select_output(self): 510 | if self.rbOutputZip.isChecked(): 511 | file_directory = QFileInfo( 512 | self.settings.value("outputToZip_Path", ".") 513 | ).absolutePath() 514 | outPath = getSaveFileName( 515 | self, 516 | self.tr("Save to file"), 517 | file_directory, 518 | ";;".join(iter(list(self.FORMATS.keys()))), 519 | ) 520 | if not outPath: 521 | return 522 | self.leZipFileName.setText(outPath) 523 | self.settings.setValue( 524 | "outputToZip_Path", QFileInfo(outPath).absoluteFilePath() 525 | ) 526 | 527 | elif self.rbOutputDir.isChecked(): 528 | dir_directory = QFileInfo( 529 | self.settings.value("outputToDir_Path", ".") 530 | ).absolutePath() 531 | outPath = QFileDialog.getExistingDirectory( 532 | self, 533 | self.tr("Save to directory"), 534 | dir_directory, 535 | QFileDialog.ShowDirsOnly, 536 | ) 537 | if not outPath: 538 | return 539 | self.leDirectoryName.setText(outPath) 540 | self.settings.setValue( 541 | "outputToDir_Path", QFileInfo(outPath).absoluteFilePath() 542 | ) 543 | 544 | elif self.rbOutputNGM.isChecked(): 545 | zip_directory = QFileInfo( 546 | self.settings.value("outputToNGM_Path", ".") 547 | ).absolutePath() 548 | outPath = getSaveFileName( 549 | self, self.tr("Save to file"), zip_directory, "ngrc" 550 | ) 551 | if not outPath: 552 | return 553 | if not outPath.lower().endswith("ngrc"): 554 | outPath += ".ngrc" 555 | self.leTilesFroNGM.setText(outPath) 556 | self.settings.setValue( 557 | "outputToNGM_Path", QFileInfo(outPath).absoluteFilePath() 558 | ) 559 | -------------------------------------------------------------------------------- /src/qtiles/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/nextgis_logo.svg 4 | icons/qtiles.png 5 | icons/about.png 6 | icons/ngm_index_24x24.png 7 | icons/info.png 8 | resources/viewer.html 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/qtiles/resources/css/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/src/qtiles/resources/css/images/layers.png -------------------------------------------------------------------------------- /src/qtiles/resources/css/jquery-ui.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.10.4 - 2014-01-17 2 | * http://jqueryui.com 3 | * Includes: jquery.ui.core.css, jquery.ui.accordion.css, jquery.ui.autocomplete.css, jquery.ui.button.css, jquery.ui.datepicker.css, jquery.ui.dialog.css, jquery.ui.menu.css, jquery.ui.progressbar.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.slider.css, jquery.ui.spinner.css, jquery.ui.tabs.css, jquery.ui.tooltip.css, jquery.ui.theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px 5 | * Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin-top:2px;padding:.5em .5em .5em .7em;min-height:0}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-noicons{padding-left:.7em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("images/animated-overlay.gif");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} 8 | -------------------------------------------------------------------------------- /src/qtiles/resources/css/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-map-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-pane, 8 | .leaflet-tile-container, 9 | .leaflet-overlay-pane, 10 | .leaflet-shadow-pane, 11 | .leaflet-marker-pane, 12 | .leaflet-popup-pane, 13 | .leaflet-overlay-pane svg, 14 | .leaflet-zoom-box, 15 | .leaflet-image-layer, 16 | .leaflet-layer { 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | } 21 | .leaflet-container { 22 | overflow: hidden; 23 | -ms-touch-action: none; 24 | } 25 | .leaflet-tile, 26 | .leaflet-marker-icon, 27 | .leaflet-marker-shadow { 28 | -webkit-user-select: none; 29 | -moz-user-select: none; 30 | user-select: none; 31 | -webkit-user-drag: none; 32 | } 33 | .leaflet-marker-icon, 34 | .leaflet-marker-shadow { 35 | display: block; 36 | } 37 | /* map is broken in FF if you have max-width: 100% on tiles */ 38 | .leaflet-container img { 39 | max-width: none !important; 40 | } 41 | /* stupid Android 2 doesn't understand "max-width: none" properly */ 42 | .leaflet-container img.leaflet-image-layer { 43 | max-width: 15000px !important; 44 | } 45 | .leaflet-tile { 46 | filter: inherit; 47 | visibility: hidden; 48 | } 49 | .leaflet-tile-loaded { 50 | visibility: inherit; 51 | } 52 | .leaflet-zoom-box { 53 | width: 0; 54 | height: 0; 55 | } 56 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 57 | .leaflet-overlay-pane svg { 58 | -moz-user-select: none; 59 | } 60 | 61 | .leaflet-tile-pane { z-index: 2; } 62 | .leaflet-objects-pane { z-index: 3; } 63 | .leaflet-overlay-pane { z-index: 4; } 64 | .leaflet-shadow-pane { z-index: 5; } 65 | .leaflet-marker-pane { z-index: 6; } 66 | .leaflet-popup-pane { z-index: 7; } 67 | 68 | .leaflet-vml-shape { 69 | width: 1px; 70 | height: 1px; 71 | } 72 | .lvml { 73 | behavior: url(#default#VML); 74 | display: inline-block; 75 | position: absolute; 76 | } 77 | 78 | 79 | /* control positioning */ 80 | 81 | .leaflet-control { 82 | position: relative; 83 | z-index: 7; 84 | pointer-events: auto; 85 | } 86 | .leaflet-top, 87 | .leaflet-bottom { 88 | position: absolute; 89 | z-index: 1000; 90 | pointer-events: none; 91 | } 92 | .leaflet-top { 93 | top: 0; 94 | } 95 | .leaflet-right { 96 | right: 0; 97 | } 98 | .leaflet-bottom { 99 | bottom: 0; 100 | } 101 | .leaflet-left { 102 | left: 0; 103 | } 104 | .leaflet-control { 105 | float: left; 106 | clear: both; 107 | } 108 | .leaflet-right .leaflet-control { 109 | float: right; 110 | } 111 | .leaflet-top .leaflet-control { 112 | margin-top: 10px; 113 | } 114 | .leaflet-bottom .leaflet-control { 115 | margin-bottom: 10px; 116 | } 117 | .leaflet-left .leaflet-control { 118 | margin-left: 10px; 119 | } 120 | .leaflet-right .leaflet-control { 121 | margin-right: 10px; 122 | } 123 | 124 | 125 | /* zoom and fade animations */ 126 | 127 | .leaflet-fade-anim .leaflet-tile, 128 | .leaflet-fade-anim .leaflet-popup { 129 | opacity: 0; 130 | -webkit-transition: opacity 0.2s linear; 131 | -moz-transition: opacity 0.2s linear; 132 | -o-transition: opacity 0.2s linear; 133 | transition: opacity 0.2s linear; 134 | } 135 | .leaflet-fade-anim .leaflet-tile-loaded, 136 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 137 | opacity: 1; 138 | } 139 | 140 | .leaflet-zoom-anim .leaflet-zoom-animated { 141 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 142 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 143 | -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); 144 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 145 | } 146 | .leaflet-zoom-anim .leaflet-tile, 147 | .leaflet-pan-anim .leaflet-tile, 148 | .leaflet-touching .leaflet-zoom-animated { 149 | -webkit-transition: none; 150 | -moz-transition: none; 151 | -o-transition: none; 152 | transition: none; 153 | } 154 | 155 | .leaflet-zoom-anim .leaflet-zoom-hide { 156 | visibility: hidden; 157 | } 158 | 159 | 160 | /* cursors */ 161 | 162 | .leaflet-clickable { 163 | cursor: pointer; 164 | } 165 | .leaflet-container { 166 | cursor: -webkit-grab; 167 | cursor: -moz-grab; 168 | } 169 | .leaflet-popup-pane, 170 | .leaflet-control { 171 | cursor: auto; 172 | } 173 | .leaflet-dragging .leaflet-container, 174 | .leaflet-dragging .leaflet-clickable { 175 | cursor: move; 176 | cursor: -webkit-grabbing; 177 | cursor: -moz-grabbing; 178 | } 179 | 180 | 181 | /* visual tweaks */ 182 | 183 | .leaflet-container { 184 | background: #ddd; 185 | outline: 0; 186 | } 187 | .leaflet-container a { 188 | color: #0078A8; 189 | } 190 | .leaflet-container a.leaflet-active { 191 | outline: 2px solid orange; 192 | } 193 | .leaflet-zoom-box { 194 | border: 2px dotted #38f; 195 | background: rgba(255,255,255,0.5); 196 | } 197 | 198 | 199 | /* general typography */ 200 | .leaflet-container { 201 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 202 | } 203 | 204 | 205 | /* general toolbar styles */ 206 | 207 | .leaflet-bar { 208 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 209 | border-radius: 4px; 210 | } 211 | .leaflet-bar a, 212 | .leaflet-bar a:hover { 213 | background-color: #fff; 214 | border-bottom: 1px solid #ccc; 215 | width: 26px; 216 | height: 26px; 217 | line-height: 26px; 218 | display: block; 219 | text-align: center; 220 | text-decoration: none; 221 | color: black; 222 | } 223 | .leaflet-bar a, 224 | .leaflet-control-layers-toggle { 225 | background-position: 50% 50%; 226 | background-repeat: no-repeat; 227 | display: block; 228 | } 229 | .leaflet-bar a:hover { 230 | background-color: #f4f4f4; 231 | } 232 | .leaflet-bar a:first-child { 233 | border-top-left-radius: 4px; 234 | border-top-right-radius: 4px; 235 | } 236 | .leaflet-bar a:last-child { 237 | border-bottom-left-radius: 4px; 238 | border-bottom-right-radius: 4px; 239 | border-bottom: none; 240 | } 241 | .leaflet-bar a.leaflet-disabled { 242 | cursor: default; 243 | background-color: #f4f4f4; 244 | color: #bbb; 245 | } 246 | 247 | .leaflet-touch .leaflet-bar a { 248 | width: 30px; 249 | height: 30px; 250 | line-height: 30px; 251 | } 252 | 253 | 254 | /* zoom control */ 255 | 256 | .leaflet-control-zoom-in, 257 | .leaflet-control-zoom-out { 258 | font: bold 18px 'Lucida Console', Monaco, monospace; 259 | text-indent: 1px; 260 | } 261 | .leaflet-control-zoom-out { 262 | font-size: 20px; 263 | } 264 | 265 | .leaflet-touch .leaflet-control-zoom-in { 266 | font-size: 22px; 267 | } 268 | .leaflet-touch .leaflet-control-zoom-out { 269 | font-size: 24px; 270 | } 271 | 272 | 273 | /* layers control */ 274 | 275 | .leaflet-control-layers { 276 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 277 | background: #fff; 278 | border-radius: 5px; 279 | } 280 | .leaflet-control-layers-toggle { 281 | background-image: url(images/layers.png); 282 | width: 36px; 283 | height: 36px; 284 | } 285 | .leaflet-retina .leaflet-control-layers-toggle { 286 | background-image: url(images/layers-2x.png); 287 | background-size: 26px 26px; 288 | } 289 | .leaflet-touch .leaflet-control-layers-toggle { 290 | width: 44px; 291 | height: 44px; 292 | } 293 | .leaflet-control-layers .leaflet-control-layers-list, 294 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 295 | display: none; 296 | } 297 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 298 | display: block; 299 | position: relative; 300 | } 301 | .leaflet-control-layers-expanded { 302 | padding: 6px 10px 6px 6px; 303 | color: #333; 304 | background: #fff; 305 | } 306 | .leaflet-control-layers-selector { 307 | margin-top: 2px; 308 | position: relative; 309 | top: 1px; 310 | } 311 | .leaflet-control-layers label { 312 | display: block; 313 | } 314 | .leaflet-control-layers-separator { 315 | height: 0; 316 | border-top: 1px solid #ddd; 317 | margin: 5px -10px 5px -6px; 318 | } 319 | 320 | 321 | /* attribution and scale controls */ 322 | 323 | .leaflet-container .leaflet-control-attribution { 324 | background: #fff; 325 | background: rgba(255, 255, 255, 0.7); 326 | margin: 0; 327 | } 328 | .leaflet-control-attribution, 329 | .leaflet-control-scale-line { 330 | padding: 0 5px; 331 | color: #333; 332 | } 333 | .leaflet-control-attribution a { 334 | text-decoration: none; 335 | } 336 | .leaflet-control-attribution a:hover { 337 | text-decoration: underline; 338 | } 339 | .leaflet-container .leaflet-control-attribution, 340 | .leaflet-container .leaflet-control-scale { 341 | font-size: 11px; 342 | } 343 | .leaflet-left .leaflet-control-scale { 344 | margin-left: 5px; 345 | } 346 | .leaflet-bottom .leaflet-control-scale { 347 | margin-bottom: 5px; 348 | } 349 | .leaflet-control-scale-line { 350 | border: 2px solid #777; 351 | border-top: none; 352 | line-height: 1.1; 353 | padding: 2px 5px 1px; 354 | font-size: 11px; 355 | white-space: nowrap; 356 | overflow: hidden; 357 | -moz-box-sizing: content-box; 358 | box-sizing: content-box; 359 | 360 | background: #fff; 361 | background: rgba(255, 255, 255, 0.5); 362 | } 363 | .leaflet-control-scale-line:not(:first-child) { 364 | border-top: 2px solid #777; 365 | border-bottom: none; 366 | margin-top: -2px; 367 | } 368 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 369 | border-bottom: 2px solid #777; 370 | } 371 | 372 | .leaflet-touch .leaflet-control-attribution, 373 | .leaflet-touch .leaflet-control-layers, 374 | .leaflet-touch .leaflet-bar { 375 | box-shadow: none; 376 | } 377 | .leaflet-touch .leaflet-control-layers, 378 | .leaflet-touch .leaflet-bar { 379 | border: 2px solid rgba(0,0,0,0.2); 380 | background-clip: padding-box; 381 | } 382 | 383 | 384 | /* popup */ 385 | 386 | .leaflet-popup { 387 | position: absolute; 388 | text-align: center; 389 | } 390 | .leaflet-popup-content-wrapper { 391 | padding: 1px; 392 | text-align: left; 393 | border-radius: 12px; 394 | } 395 | .leaflet-popup-content { 396 | margin: 13px 19px; 397 | line-height: 1.4; 398 | } 399 | .leaflet-popup-content p { 400 | margin: 18px 0; 401 | } 402 | .leaflet-popup-tip-container { 403 | margin: 0 auto; 404 | width: 40px; 405 | height: 20px; 406 | position: relative; 407 | overflow: hidden; 408 | } 409 | .leaflet-popup-tip { 410 | width: 17px; 411 | height: 17px; 412 | padding: 1px; 413 | 414 | margin: -10px auto 0; 415 | 416 | -webkit-transform: rotate(45deg); 417 | -moz-transform: rotate(45deg); 418 | -ms-transform: rotate(45deg); 419 | -o-transform: rotate(45deg); 420 | transform: rotate(45deg); 421 | } 422 | .leaflet-popup-content-wrapper, 423 | .leaflet-popup-tip { 424 | background: white; 425 | 426 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 427 | } 428 | .leaflet-container a.leaflet-popup-close-button { 429 | position: absolute; 430 | top: 0; 431 | right: 0; 432 | padding: 4px 4px 0 0; 433 | text-align: center; 434 | width: 18px; 435 | height: 14px; 436 | font: 16px/14px Tahoma, Verdana, sans-serif; 437 | color: #c3c3c3; 438 | text-decoration: none; 439 | font-weight: bold; 440 | background: transparent; 441 | } 442 | .leaflet-container a.leaflet-popup-close-button:hover { 443 | color: #999; 444 | } 445 | .leaflet-popup-scrolled { 446 | overflow: auto; 447 | border-bottom: 1px solid #ddd; 448 | border-top: 1px solid #ddd; 449 | } 450 | 451 | .leaflet-oldie .leaflet-popup-content-wrapper { 452 | zoom: 1; 453 | } 454 | .leaflet-oldie .leaflet-popup-tip { 455 | width: 24px; 456 | margin: 0 auto; 457 | 458 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 459 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 460 | } 461 | .leaflet-oldie .leaflet-popup-tip-container { 462 | margin-top: -1px; 463 | } 464 | 465 | .leaflet-oldie .leaflet-control-zoom, 466 | .leaflet-oldie .leaflet-control-layers, 467 | .leaflet-oldie .leaflet-popup-content-wrapper, 468 | .leaflet-oldie .leaflet-popup-tip { 469 | border: 1px solid #999; 470 | } 471 | 472 | 473 | /* div icon */ 474 | 475 | .leaflet-div-icon { 476 | background: #fff; 477 | border: 1px solid #666; 478 | } 479 | -------------------------------------------------------------------------------- /src/qtiles/resources/js/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/src/qtiles/resources/js/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /src/qtiles/resources/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @tilesetname - LeafLet Preview 5 | 6 | 7 | 8 | 9 | 10 | 11 | 33 | 34 | 35 | 36 | 73 | 74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /src/qtiles/tile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ****************************************************************************** 4 | # 5 | # QTiles 6 | # --------------------------------------------------------- 7 | # Generates tiles from QGIS project 8 | # 9 | # Copyright (C) 2012-2014 NextGIS (info@nextgis.org) 10 | # 11 | # This source is free software; you can redistribute it and/or modify it under 12 | # the terms of the GNU General Public License as published by the Free 13 | # Software Foundation, either version 2 of the License, or (at your option) 14 | # any later version. 15 | # 16 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 17 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 19 | # details. 20 | # 21 | # A copy of the GNU General Public License is available on the World Wide Web 22 | # at . You can also obtain it by writing 23 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 24 | # MA 02110-1335 USA. 25 | # 26 | # ****************************************************************************** 27 | 28 | 29 | import math 30 | 31 | from qgis.core import QgsRectangle 32 | from .compat import QgsPointXY 33 | 34 | 35 | class Tile: 36 | def __init__(self, x=0, y=0, z=0, tms=1): 37 | self.x = x 38 | self.y = y 39 | self.z = z 40 | self.tms = tms 41 | 42 | def toPoint(self): 43 | n = math.pow(2, self.z) 44 | longitude = float(self.x) / n * 360.0 - 180.0 45 | latitude = self.tms * math.degrees( 46 | math.atan(math.sinh(math.pi * (1.0 - 2.0 * float(self.y) / n))) 47 | ) 48 | return QgsPointXY(longitude, latitude) 49 | 50 | def toRectangle(self): 51 | return QgsRectangle( 52 | self.toPoint(), 53 | Tile(self.x + 1, self.y + 1, self.z, self.tms).toPoint(), 54 | ) 55 | -------------------------------------------------------------------------------- /src/qtiles/tilingthread.py: -------------------------------------------------------------------------------- 1 | # ****************************************************************************** 2 | # 3 | # QTiles 4 | # --------------------------------------------------------- 5 | # Generates tiles from QGIS project 6 | # 7 | # Copyright (C) 2012-2022 NextGIS (info@nextgis.com) 8 | # 9 | # This source is free software; you can redistribute it and/or modify it under 10 | # the terms of the GNU General Public License as published by the Free 11 | # Software Foundation, either version 2 of the License, or (at your option) 12 | # any later version. 13 | # 14 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 15 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 | # details. 18 | # 19 | # A copy of the GNU General Public License is available on the World Wide Web 20 | # at . You can also obtain it by writing 21 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 22 | # MA 02110-1335 USA. 23 | # 24 | # ****************************************************************************** 25 | import codecs 26 | import json 27 | import time 28 | from string import Template 29 | 30 | from qgis.core import ( 31 | QgsMapRendererCustomPainterJob, 32 | QgsMapSettings, 33 | QgsMessageLog, 34 | QgsProject, 35 | QgsScaleCalculator, 36 | ) 37 | from qgis.PyQt.QtCore import QFile, QIODevice, QMutex, Qt, QThread, pyqtSignal 38 | from qgis.PyQt.QtGui import QColor, QImage, QPainter 39 | from qgis.PyQt.QtWidgets import * 40 | 41 | from . import resources_rc # noqa: F401 42 | from .compat import ( 43 | QGIS_VERSION_3, 44 | QgsCoordinateReferenceSystem, 45 | QgsCoordinateTransform, 46 | QgsMessageLogInfo, 47 | ) 48 | from .tile import Tile 49 | from .writers import * 50 | 51 | 52 | def printQtilesLog(msg, level=QgsMessageLogInfo): 53 | QgsMessageLog.logMessage(msg, "QTiles", level) 54 | 55 | 56 | class TilingThread(QThread): 57 | rangeChanged = pyqtSignal(str, int) 58 | updateProgress = pyqtSignal() 59 | processFinished = pyqtSignal() 60 | processInterrupted = pyqtSignal() 61 | threshold = pyqtSignal(int) 62 | 63 | warring_threshold_tiles_count = 10000 64 | 65 | def __init__( 66 | self, 67 | layers, 68 | extent, 69 | minZoom, 70 | maxZoom, 71 | width, 72 | height, 73 | transp, 74 | quality, 75 | format, 76 | outputPath, 77 | rootDir, 78 | antialiasing, 79 | tmsConvention, 80 | mbtilesCompression, 81 | jsonFile, 82 | overview, 83 | renderOutsideTiles, 84 | mapUrl, 85 | viewer, 86 | ): 87 | QThread.__init__(self, QThread.currentThread()) 88 | self.mutex = QMutex() 89 | self.confirmMutex = QMutex() 90 | self.stopMe = 0 91 | self.interrupted = False 92 | self.layers = layers 93 | self.extent = extent 94 | self.minZoom = minZoom 95 | self.maxZoom = maxZoom 96 | self.output = outputPath 97 | self.width = width 98 | if rootDir: 99 | self.rootDir = rootDir 100 | else: 101 | self.rootDir = "tileset_%s" % str(time.time()).split(".")[0] 102 | self.antialias = antialiasing 103 | self.tmsConvention = tmsConvention 104 | self.mbtilesCompression = mbtilesCompression 105 | self.format = format 106 | self.quality = quality 107 | self.jsonFile = jsonFile 108 | self.overview = overview 109 | self.renderOutsideTiles = renderOutsideTiles 110 | self.mapurl = mapUrl 111 | self.viewer = viewer 112 | if self.output.isDir(): 113 | self.mode = "DIR" 114 | elif self.output.suffix().lower() == "zip": 115 | self.mode = "ZIP" 116 | elif self.output.suffix().lower() == "ngrc": 117 | self.mode = "NGM" 118 | elif self.output.suffix().lower() == "mbtiles": 119 | self.mode = "MBTILES" 120 | self.tmsConvention = True 121 | self.interrupted = False 122 | self.tiles = [] 123 | self.layersId = [] 124 | for layer in self.layers: 125 | self.layersId.append(layer.id()) 126 | myRed = QgsProject.instance().readNumEntry( 127 | "Gui", "/CanvasColorRedPart", 255 128 | )[0] 129 | myGreen = QgsProject.instance().readNumEntry( 130 | "Gui", "/CanvasColorGreenPart", 255 131 | )[0] 132 | myBlue = QgsProject.instance().readNumEntry( 133 | "Gui", "/CanvasColorBluePart", 255 134 | )[0] 135 | self.color = QColor(myRed, myGreen, myBlue, transp) 136 | image = QImage(width, height, QImage.Format_ARGB32_Premultiplied) 137 | self.projector = QgsCoordinateTransform( 138 | QgsCoordinateReferenceSystem.fromEpsgId(4326), 139 | QgsCoordinateReferenceSystem.fromEpsgId(3395), 140 | ) 141 | self.scaleCalc = QgsScaleCalculator() 142 | self.scaleCalc.setDpi(image.logicalDpiX()) 143 | self.scaleCalc.setMapUnits( 144 | QgsCoordinateReferenceSystem.fromEpsgId(3395).mapUnits() 145 | ) 146 | self.settings = QgsMapSettings() 147 | self.settings.setBackgroundColor(self.color) 148 | 149 | if not QGIS_VERSION_3: 150 | self.settings.setCrsTransformEnabled(True) 151 | 152 | self.settings.setOutputDpi(image.logicalDpiX()) 153 | self.settings.setOutputImageFormat(QImage.Format_ARGB32_Premultiplied) 154 | self.settings.setDestinationCrs( 155 | QgsCoordinateReferenceSystem.fromEpsgId(3395) 156 | ) 157 | self.settings.setOutputSize(image.size()) 158 | 159 | if QGIS_VERSION_3: 160 | self.settings.setLayers(self.layers) 161 | else: 162 | self.settings.setLayers(self.layersId) 163 | 164 | if not QGIS_VERSION_3: 165 | self.settings.setMapUnits( 166 | QgsCoordinateReferenceSystem.fromEpsgId(3395).mapUnits() 167 | ) 168 | 169 | if self.antialias: 170 | self.settings.setFlag(QgsMapSettings.Antialiasing, True) 171 | else: 172 | self.settings.setFlag(QgsMapSettings.DrawLabeling, True) 173 | 174 | def run(self): 175 | self.mutex.lock() 176 | self.stopMe = 0 177 | self.mutex.unlock() 178 | if self.mode == "DIR": 179 | self.writer = DirectoryWriter(self.output, self.rootDir) 180 | if self.mapurl: 181 | self.writeMapurlFile() 182 | if self.viewer: 183 | self.writeLeafletViewer() 184 | elif self.mode == "ZIP": 185 | self.writer = ZipWriter(self.output, self.rootDir) 186 | elif self.mode == "NGM": 187 | self.writer = NGMArchiveWriter(self.output, self.rootDir) 188 | elif self.mode == "MBTILES": 189 | self.writer = MBTilesWriter( 190 | self.output, 191 | self.rootDir, 192 | self.format, 193 | self.minZoom, 194 | self.maxZoom, 195 | self.extent, 196 | self.mbtilesCompression, 197 | ) 198 | if self.jsonFile: 199 | self.writeJsonFile() 200 | if self.overview: 201 | self.writeOverviewFile() 202 | self.rangeChanged.emit(self.tr("Searching tiles..."), 0) 203 | useTMS = 1 204 | if self.tmsConvention: 205 | useTMS = -1 206 | self.countTiles(Tile(0, 0, 0, useTMS)) 207 | 208 | if self.interrupted: 209 | del self.tiles[:] 210 | self.tiles = None 211 | self.processInterrupted.emit() 212 | self.rangeChanged.emit( 213 | self.tr("Rendering: %v from %m (%p%)"), len(self.tiles) 214 | ) 215 | 216 | if len(self.tiles) > self.warring_threshold_tiles_count: 217 | self.confirmMutex.lock() 218 | self.threshold.emit(self.warring_threshold_tiles_count) 219 | 220 | self.confirmMutex.lock() 221 | if self.interrupted: 222 | self.processInterrupted.emit() 223 | return 224 | 225 | for t in self.tiles: 226 | self.render(t) 227 | self.updateProgress.emit() 228 | self.mutex.lock() 229 | s = self.stopMe 230 | self.mutex.unlock() 231 | if s == 1: 232 | self.interrupted = True 233 | break 234 | 235 | self.writer.finalize() 236 | if not self.interrupted: 237 | self.processFinished.emit() 238 | else: 239 | self.processInterrupted.emit() 240 | 241 | def stop(self): 242 | self.mutex.lock() 243 | self.stopMe = 1 244 | self.mutex.unlock() 245 | QThread.wait(self) 246 | 247 | def confirmContinue(self): 248 | self.confirmMutex.unlock() 249 | 250 | def confirmStop(self): 251 | self.interrupted = True 252 | self.confirmMutex.unlock() 253 | 254 | def writeJsonFile(self): 255 | filePath = "%s.json" % self.output.absoluteFilePath() 256 | if self.mode == "DIR": 257 | filePath = "%s/%s.json" % ( 258 | self.output.absoluteFilePath(), 259 | self.rootDir, 260 | ) 261 | info = { 262 | "name": self.rootDir, 263 | "format": self.format.lower(), 264 | "minZoom": self.minZoom, 265 | "maxZoom": self.maxZoom, 266 | "bounds": str(self.extent.xMinimum()) 267 | + "," 268 | + str(self.extent.yMinimum()) 269 | + "," 270 | + str(self.extent.xMaximum()) 271 | + "," 272 | + str(self.extent.yMaximum()), 273 | } 274 | with open(filePath, "w") as f: 275 | f.write(json.dumps(info)) 276 | 277 | def writeOverviewFile(self): 278 | self.settings.setExtent(self.projector.transform(self.extent)) 279 | 280 | image = QImage(self.settings.outputSize(), QImage.Format_ARGB32) 281 | image.fill(Qt.transparent) 282 | 283 | dpm = round(self.settings.outputDpi() / 25.4 * 1000) 284 | image.setDotsPerMeterX(dpm) 285 | image.setDotsPerMeterY(dpm) 286 | 287 | # job = QgsMapRendererSequentialJob(self.settings) 288 | # job.start() 289 | # job.waitForFinished() 290 | # image = job.renderedImage() 291 | 292 | painter = QPainter(image) 293 | job = QgsMapRendererCustomPainterJob(self.settings, painter) 294 | job.renderSynchronously() 295 | painter.end() 296 | 297 | filePath = "%s.%s" % ( 298 | self.output.absoluteFilePath(), 299 | self.format.lower(), 300 | ) 301 | if self.mode == "DIR": 302 | filePath = "%s/%s.%s" % ( 303 | self.output.absoluteFilePath(), 304 | self.rootDir, 305 | self.format.lower(), 306 | ) 307 | image.save(filePath, self.format, self.quality) 308 | 309 | def writeMapurlFile(self): 310 | filePath = "%s/%s.mapurl" % ( 311 | self.output.absoluteFilePath(), 312 | self.rootDir, 313 | ) 314 | tileServer = "tms" if self.tmsConvention else "google" 315 | with open(filePath, "w") as mapurl: 316 | mapurl.write( 317 | "%s=%s\n" % ("url", self.rootDir + "/ZZZ/XXX/YYY.png") 318 | ) 319 | mapurl.write("%s=%s\n" % ("minzoom", self.minZoom)) 320 | mapurl.write("%s=%s\n" % ("maxzoom", self.maxZoom)) 321 | mapurl.write( 322 | "%s=%f %f\n" 323 | % ( 324 | "center", 325 | self.extent.center().x(), 326 | self.extent.center().y(), 327 | ) 328 | ) 329 | mapurl.write("%s=%s\n" % ("type", tileServer)) 330 | 331 | def writeLeafletViewer(self): 332 | templateFile = QFile(":/plugins/qtiles/resources/viewer.html") 333 | if templateFile.open(QIODevice.ReadOnly | QIODevice.Text): 334 | viewer = MyTemplate(str(templateFile.readAll())) 335 | 336 | tilesDir = "%s/%s" % (self.output.absoluteFilePath(), self.rootDir) 337 | useTMS = "true" if self.tmsConvention else "false" 338 | substitutions = { 339 | "tilesdir": tilesDir, 340 | "tilesext": self.format.lower(), 341 | "tilesetname": self.rootDir, 342 | "tms": useTMS, 343 | "centerx": self.extent.center().x(), 344 | "centery": self.extent.center().y(), 345 | "avgzoom": (self.maxZoom + self.minZoom) / 2, 346 | "maxzoom": self.maxZoom, 347 | } 348 | 349 | filePath = "%s/%s.html" % ( 350 | self.output.absoluteFilePath(), 351 | self.rootDir, 352 | ) 353 | with codecs.open(filePath, "w", "utf-8") as fOut: 354 | fOut.write(viewer.substitute(substitutions)) 355 | templateFile.close() 356 | 357 | def countTiles(self, tile): 358 | if self.interrupted or not self.extent.intersects(tile.toRectangle()): 359 | return 360 | if self.minZoom <= tile.z and tile.z <= self.maxZoom: 361 | if not self.renderOutsideTiles: 362 | for layer in self.layers: 363 | t = QgsCoordinateTransform( 364 | layer.crs(), 365 | QgsCoordinateReferenceSystem.fromEpsgId(4326), 366 | ) 367 | if t.transform(layer.extent()).intersects( 368 | tile.toRectangle() 369 | ): 370 | self.tiles.append(tile) 371 | break 372 | else: 373 | self.tiles.append(tile) 374 | if tile.z < self.maxZoom: 375 | for x in range(2 * tile.x, 2 * tile.x + 2, 1): 376 | for y in range(2 * tile.y, 2 * tile.y + 2, 1): 377 | self.mutex.lock() 378 | s = self.stopMe 379 | self.mutex.unlock() 380 | if s == 1: 381 | self.interrupted = True 382 | return 383 | subTile = Tile(x, y, tile.z + 1, tile.tms) 384 | self.countTiles(subTile) 385 | 386 | def render(self, tile): 387 | # scale = self.scaleCalc.calculate( 388 | # self.projector.transform(tile.toRectangle()), self.width) 389 | 390 | self.settings.setExtent(self.projector.transform(tile.toRectangle())) 391 | 392 | image = QImage(self.settings.outputSize(), QImage.Format_ARGB32) 393 | image.fill(Qt.transparent) 394 | 395 | dpm = round(self.settings.outputDpi() / 25.4 * 1000) 396 | image.setDotsPerMeterX(dpm) 397 | image.setDotsPerMeterY(dpm) 398 | 399 | # job = QgsMapRendererSequentialJob(self.settings) 400 | # job.start() 401 | # job.waitForFinished() 402 | # image = job.renderedImage() 403 | 404 | painter = QPainter(image) 405 | job = QgsMapRendererCustomPainterJob(self.settings, painter) 406 | job.renderSynchronously() 407 | painter.end() 408 | self.writer.writeTile(tile, image, self.format, self.quality) 409 | 410 | 411 | class MyTemplate(Template): 412 | delimiter = "@" 413 | 414 | def __init__(self, templateString): 415 | Template.__init__(self, templateString) 416 | -------------------------------------------------------------------------------- /src/qtiles/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgis/qgis_qtiles/36fed26376935eb100ccad72d414842ea8532c59/src/qtiles/ui/__init__.py -------------------------------------------------------------------------------- /src/qtiles/ui/aboutdialogbase.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AboutDialogBase 4 | 5 | 6 | 7 | 0 8 | 0 9 | 652 10 | 512 11 | 12 | 13 | 14 | About {plugin_name} 15 | 16 | 17 | 18 | 12 19 | 20 | 21 | 22 | 23 | QTabWidget::North 24 | 25 | 26 | 0 27 | 28 | 29 | false 30 | 31 | 32 | 33 | Information 34 | 35 | 36 | 37 | 0 38 | 39 | 40 | 0 41 | 42 | 43 | 0 44 | 45 | 46 | 0 47 | 48 | 49 | 50 | 51 | true 52 | 53 | 54 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 55 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 56 | p, li { white-space: pre-wrap; } 57 | </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> 58 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> 59 | 60 | 61 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 62 | 63 | 64 | true 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | License 73 | 74 | 75 | 76 | 0 77 | 78 | 79 | 0 80 | 81 | 82 | 0 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Components 95 | 96 | 97 | 98 | 0 99 | 100 | 101 | 0 102 | 103 | 104 | 0 105 | 106 | 107 | 0 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Contributors 117 | 118 | 119 | 120 | 0 121 | 122 | 123 | 0 124 | 125 | 126 | 0 127 | 128 | 129 | 0 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 9 142 | 143 | 144 | 145 | 146 | 3 147 | 148 | 149 | 150 | 151 | 152 | 16 153 | 75 154 | true 155 | 156 | 157 | 158 | {plugin_name} 159 | 160 | 161 | Qt::AlignCenter 162 | 163 | 164 | 165 | 166 | 167 | 168 | Version {version} 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | Qt::Horizontal 178 | 179 | 180 | 181 | 40 182 | 20 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 0 193 | 194 | 195 | 196 | 197 | 198 | 0 199 | 0 200 | 201 | 202 | 203 | Get involved 204 | 205 | 206 | 207 | 208 | 209 | 210 | Qt::Horizontal 211 | 212 | 213 | QDialogButtonBox::Close 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | button_box 225 | rejected() 226 | AboutDialogBase 227 | reject() 228 | 229 | 230 | 316 231 | 260 232 | 233 | 234 | 286 235 | 274 236 | 237 | 238 | 239 | 240 | button_box 241 | accepted() 242 | AboutDialogBase 243 | accept() 244 | 245 | 246 | 248 247 | 254 248 | 249 | 250 | 157 251 | 274 252 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /src/qtiles/writers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ****************************************************************************** 4 | # 5 | # QTiles 6 | # --------------------------------------------------------- 7 | # Generates tiles from QGIS project 8 | # 9 | # Copyright (C) 2012-2022 NextGIS (info@nextgis.com) 10 | # 11 | # This source is free software; you can redistribute it and/or modify it under 12 | # the terms of the GNU General Public License as published by the Free 13 | # Software Foundation, either version 2 of the License, or (at your option) 14 | # any later version. 15 | # 16 | # This code is distributed in the hope that it will be useful, but WITHOUT ANY 17 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 19 | # details. 20 | # 21 | # A copy of the GNU General Public License is available on the World Wide Web 22 | # at . You can also obtain it by writing 23 | # to the Free Software Foundation, 51 Franklin Street, Suite 500 Boston, 24 | # MA 02110-1335 USA. 25 | # 26 | # ****************************************************************************** 27 | 28 | import sqlite3 29 | import zipfile 30 | import json 31 | 32 | from qgis.PyQt.QtCore import ( 33 | QBuffer, 34 | QByteArray, 35 | QIODevice, 36 | QTemporaryFile, 37 | QDir, 38 | ) 39 | 40 | from .mbutils import * 41 | 42 | 43 | class DirectoryWriter: 44 | def __init__(self, outputPath, rootDir): 45 | self.output = outputPath 46 | self.rootDir = rootDir 47 | 48 | def writeTile(self, tile, image, format, quality): 49 | path = "%s/%s/%s" % (self.rootDir, tile.z, tile.x) 50 | dirPath = "%s/%s" % (self.output.absoluteFilePath(), path) 51 | QDir().mkpath(dirPath) 52 | image.save( 53 | "%s/%s.%s" % (dirPath, tile.y, format.lower()), format, quality 54 | ) 55 | 56 | def finalize(self): 57 | pass 58 | 59 | 60 | class ZipWriter: 61 | def __init__(self, outputPath, rootDir): 62 | self.output = outputPath 63 | self.rootDir = rootDir 64 | 65 | self.zipFile = zipfile.ZipFile( 66 | str(self.output.absoluteFilePath()), "w", allowZip64=True 67 | ) 68 | self.tempFile = QTemporaryFile() 69 | self.tempFile.setAutoRemove(False) 70 | self.tempFile.open(QIODevice.WriteOnly) 71 | self.tempFileName = self.tempFile.fileName() 72 | self.tempFile.close() 73 | 74 | def writeTile(self, tile, image, format, quality): 75 | path = "%s/%s/%s" % (self.rootDir, tile.z, tile.x) 76 | 77 | image.save(self.tempFileName, format, quality) 78 | tilePath = "%s/%s.%s" % (path, tile.y, format.lower()) 79 | self.zipFile.write( 80 | bytes(str(self.tempFileName).encode("utf8")), tilePath 81 | ) 82 | 83 | def finalize(self): 84 | self.tempFile.close() 85 | self.tempFile.remove() 86 | self.zipFile.close() 87 | 88 | 89 | class NGMArchiveWriter(ZipWriter): 90 | def __init__(self, outputPath, rootDir): 91 | ZipWriter.__init__(self, outputPath, "Mapnik") 92 | self.levels = {} 93 | self.__layerName = rootDir 94 | 95 | def writeTile(self, tile, image, format, quality): 96 | ZipWriter.writeTile(self, tile, image, format, quality) 97 | level = self.levels.get(tile.z, {"x": [], "y": []}) 98 | level["x"].append(tile.x) 99 | level["y"].append(tile.y) 100 | 101 | self.levels[tile.z] = level 102 | 103 | def finalize(self): 104 | archive_info = { 105 | "cache_size_multiply": 0, 106 | "levels": [], 107 | "max_level": max(self.levels.keys()), 108 | "min_level": min(self.levels.keys()), 109 | "name": self.__layerName, 110 | "renderer_properties": { 111 | "alpha": 255, 112 | "antialias": True, 113 | "brightness": 0, 114 | "contrast": 1, 115 | "dither": True, 116 | "filterbitmap": True, 117 | "greyscale": False, 118 | "type": "tms_renderer", 119 | }, 120 | "tms_type": 2, 121 | "type": 32, 122 | "visible": True, 123 | } 124 | 125 | for level, coords in list(self.levels.items()): 126 | level_json = { 127 | "level": level, 128 | "bbox_maxx": max(coords["x"]), 129 | "bbox_maxy": max(coords["y"]), 130 | "bbox_minx": min(coords["x"]), 131 | "bbox_miny": min(coords["y"]), 132 | } 133 | 134 | archive_info["levels"].append(level_json) 135 | 136 | tempFile = QTemporaryFile() 137 | tempFile.setAutoRemove(False) 138 | tempFile.open(QIODevice.WriteOnly) 139 | tempFile.write(bytes(json.dumps(archive_info).encode("utf8"))) 140 | tempFileName = tempFile.fileName() 141 | tempFile.close() 142 | 143 | self.zipFile.write(tempFileName, "%s.json" % self.rootDir) 144 | 145 | ZipWriter.finalize(self) 146 | 147 | 148 | class MBTilesWriter: 149 | def __init__( 150 | self, 151 | outputPath, 152 | rootDir, 153 | formatext, 154 | minZoom, 155 | maxZoom, 156 | extent, 157 | compression, 158 | ): 159 | self.output = outputPath 160 | self.rootDir = rootDir 161 | self.compression = compression 162 | s = ( 163 | str(extent.xMinimum()) 164 | + "," 165 | + str(extent.yMinimum()) 166 | + "," 167 | + str(extent.xMaximum()) 168 | + "," 169 | + str(extent.yMaximum()) 170 | ) 171 | self.connection = mbtiles_connect(str(self.output.absoluteFilePath())) 172 | self.cursor = self.connection.cursor() 173 | optimize_connection(self.cursor) 174 | mbtiles_setup(self.cursor) 175 | self.cursor.execute( 176 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 177 | ("name", rootDir), 178 | ) 179 | self.cursor.execute( 180 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 181 | ("description", "Created with QTiles"), 182 | ) 183 | self.cursor.execute( 184 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 185 | ("format", formatext.lower()), 186 | ) 187 | self.cursor.execute( 188 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 189 | ("minZoom", str(minZoom)), 190 | ) 191 | self.cursor.execute( 192 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 193 | ("maxZoom", str(maxZoom)), 194 | ) 195 | self.cursor.execute( 196 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 197 | ("type", "baselayer"), 198 | ) 199 | self.cursor.execute( 200 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 201 | ("version", "1.1"), 202 | ) 203 | self.cursor.execute( 204 | """INSERT INTO metadata(name, value) VALUES (?, ?);""", 205 | ("bounds", s), 206 | ) 207 | self.connection.commit() 208 | 209 | def writeTile(self, tile, image, format, quality): 210 | data = QByteArray() 211 | buff = QBuffer(data) 212 | image.save(buff, format, quality) 213 | 214 | self.cursor.execute( 215 | """INSERT INTO tiles(zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?);""", 216 | (tile.z, tile.x, tile.y, sqlite3.Binary(buff.data())), 217 | ) 218 | buff.close() 219 | 220 | def finalize(self): 221 | optimize_database(self.connection) 222 | self.connection.commit() 223 | if self.compression: 224 | # start compression 225 | compression_prepare(self.cursor, self.connection) 226 | self.cursor.execute("select count(zoom_level) from tiles") 227 | res = self.cursor.fetchone() 228 | total_tiles = res[0] 229 | compression_do(self.cursor, self.connection, total_tiles) 230 | compression_finalize(self.cursor) 231 | optimize_database(self.connection) 232 | self.connection.commit() 233 | # end compression 234 | self.connection.close() 235 | self.cursor = None 236 | --------------------------------------------------------------------------------