├── .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 | 
10 |
11 | ## YouTube
12 |
13 | [](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?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 |