diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0b1e1e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml new file mode 100644 index 0000000..c9c005b --- /dev/null +++ b/.github/workflows/on-pull-request.yml @@ -0,0 +1,19 @@ +name: flake8 Lint + +on: [push, pull_request] + +jobs: + flake8-lint: + runs-on: ubuntu-latest + name: Lint + steps: + - name: Check out source repository + uses: actions/checkout@v5 + - name: Set up Python environment + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: flake8 Lint + uses: py-actions/flake8@v2 + with: + path: plugin_code/ diff --git a/.gitignore b/.gitignore index 894a44c..538d32d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ __pycache__/ *.py[cod] *$py.class +docker/qgis_plugins/* +plugin_code/shogun_qgis_venv/ # C extensions *.so @@ -102,3 +104,7 @@ venv.bak/ # mypy .mypy_cache/ + +# Intellij IDEA +.idea/ +*.iml diff --git a/.vscode/launch.json b/.vscode/launch.json index 7f1ecf1..aad5a69 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,67 +1,20 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File (Integrated Terminal)", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - }, - { - "name": "Python: Remote Attach", - "type": "python", - "request": "attach", - "port": 5678, - "host": "localhost", - "pathMappings": [ - { - "localRoot": "${workspaceFolder}", - "remoteRoot": "${workspaceFolder}" - } - ] - }, - { - "name": "Python: Module", - "type": "python", - "request": "launch", - "module": "enter-your-module-name-here", - "console": "integratedTerminal" - }, - { - "name": "Python: Django", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/manage.py", - "console": "integratedTerminal", - "args": [ - "runserver", - "--noreload", - "--nothreading" - ], - "django": true - }, - { - "name": "Python: Flask", - "type": "python", - "request": "launch", - "module": "flask", - "env": { - "FLASK_APP": "app.py" - }, - "args": [ - "run", - "--no-debugger", - "--no-reload" - ], - "jinja": true - }, - { - "name": "Python: Current File (External Terminal)", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "externalTerminal" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Remote Attach", + "type": "python", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}/plugin_code", + "remoteRoot": "/root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis-shogun-editor" + } + ] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 615aafb..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "/usr/bin/python3" -} \ No newline at end of file diff --git a/LICENSE b/LICENSE index d159169..a3d1140 100644 --- a/LICENSE +++ b/LICENSE @@ -1,339 +1,16 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. +Qgis-Shogun-Editor +Copyright (C) 2024 terrestris GmbH & Co. KG + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. diff --git a/README.md b/README.md index ebf668a..eed0726 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,17 @@ +# qgis-shogun-editor -# Shogun Editor for QGIS. A plugin +WIP: reworking the old plugin -A QGIS plugin for editing a SHOGun GIS client instance. See [https://github.com/terrestris/shogun](https://github.com/terrestris/shogun) +## Development -After connecting with SHOGun, the user can edit applications and layers on the SHOGun server, in interaction with the QGIS interface. +### Docker -# Features +**Start** -* Add layers from SHOGun to QGIS (as WFS, WMS or raw data) +Run -* Upload new layers (vector and raster format) from QGIS to the SHOGun server - -* Upload layer style (vector layers only) from QGIS and apply to the layers in SHOGun - -> most basic styling is implemented, also the upload of custom symbols for point layers - -* Edit static data like names, descriptions, ... - -* Create new SHOGun applications - -* Set applications homeview directly from the QGIS interface - -# Installation - -You can install the plugin via the QGIS plugin repository or manually. -Manually installing: Copy the "shoguneditor directory to your -qgis plugins directory which you should find at: - -## QGIS 2.x - -usually in your home directory you find: .qgis2/python/plugins/ --> if you need more details see this link (chapter "The Copy Method") - -## QGIS 3.x - -To find the plugins folder, open up QGIS, in the menu go to Settings->User Profiles -> Open active profile folder -In the file explorer, go to python/plugins/ and paste the folder there - -After copying just open up QGIS, activate the plugin in the plugin manager and you are done - -# Development - -Create a symlink: - -```bash -# move to your your plugins directory: -cd ~/.local/share/QGIS/QGIS3/profiles/default/python/plugins -# link your workspace with the plugins directory: -ln -s ~/workspace/qgis-shogun-editor/src/shoguneditor shoguneditor +```shell +./setup.sh ``` -## Debugging in VSCode - -### Install QGIS plugins - -* install `Plugin Reloader` (experimental) -* install `debugvs` - -### Install python dependencies - -* install ptvsd `(sudo) pip3 install ptvsd (--user)` (needed by `debugvs`) -* add qgis to python path: `export PYTHONPATH=/usr/share/qgis/python` (depends on location of `qgis/python`) - -### Install VSCode extensions - -* install `python` extension from marketplace - -### Debug setup - -* in QGIS open `Plugins > Plugin Reloader > Configure` -* select plugin that should be reloaded -* start `debugvs`: In QGIS open `Plugins > Enable debug for visual studio > Enable debug for visual studio` -* in VSCode go to `Debug` tab and start debugging in `attach` mode -* make sure that property `pathMappings.remoteRoot` is set in launch.json as absolute path or VSCode variable - -# Important notes & missing features - -* The plugin works with QGIS 2.x and 3.x, but currently there is a problem with adding wfs layers -to QGIS ins 3.x, which has to be resolved - -* As already mentioned, the plugin works with basic authentication requests and therefore -can only be used with SHOGun installations which support basic authentication - -* Layer styles based on custom icons in SHOGun currently cannot be imported to QGIS, but styles with custom icons created in QGIS can be uploaded to SHOGun - -* Layer styles based on font symbols are currently not supported by the plugin +to start QGIS. diff --git a/docker/docker-compose-dev.yml b/docker/docker-compose-dev.yml new file mode 100644 index 0000000..795a9ce --- /dev/null +++ b/docker/docker-compose-dev.yml @@ -0,0 +1,13 @@ +services: + qgis: + image: qgis/qgis:3.44 + command: ["/bin/bash", "-c", "qgis"] + environment: + DISPLAY: "unix${DISPLAY}" + RUN_IN_DOCKER: "1" + ports: + - "5678:5678" + volumes: + - /tmp/.X11-unix:/tmp/.X11-unix + - ./qgis_plugins:/root/.local/share/QGIS/QGIS3/profiles/default/python/plugins + - ../plugin_code:/root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis-shogun-editor diff --git a/plugin_code/Makefile b/plugin_code/Makefile new file mode 100644 index 0000000..637de3a --- /dev/null +++ b/plugin_code/Makefile @@ -0,0 +1,243 @@ +#/*************************************************************************** +# QgisShogunEditor +# +# ------------------- +# begin : 2025-09 +# git sha : $Format:%H$ +# copyright : (C) 2025 by terrestris +# email : info@terrestris.de +# ***************************************************************************/ +# +#/*************************************************************************** +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation; either version 2 of the License, or * +# * (at your option) any later version. * +# * * +# ***************************************************************************/ + +################################################# +# Edit the following to match your sources lists +################################################# + + +#Add iso code for any locales you want to support here (space separated) +# default is no locales +# LOCALES = af +LOCALES = + +# If locales are enabled, set the name of the lrelease binary on your system. If +# you have trouble compiling the translations, you may have to specify the full path to +# lrelease +#LRELEASE = lrelease +#LRELEASE = lrelease-qt4 + + +# translation +SOURCES = \ + __init__.py \ + qgis_shogun_editor.py qgis_shogun_editor_dialog.py + +PLUGINNAME = qgis_shogun_editor + +PY_FILES = \ + __init__.py \ + qgis_shogun_editor.py qgis_shogun_editor_dialog.py + +UI_FILES = qgis_shogun_editor_dialog_base.ui + +EXTRAS = metadata.txt icon.png + +EXTRA_DIRS = + +COMPILED_RESOURCE_FILES = resources.py + +PEP8EXCLUDE=pydev,resources.py,conf.py,third_party,ui + +# QGISDIR points to the location where your plugin should be installed. +# This varies by platform, relative to your HOME directory: +# * Linux: +# .local/share/QGIS/QGIS3/profiles/default/python/plugins/ +# * Mac OS X: +# Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins +# * Windows: +# AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins' + +QGISDIR=/home/user/.local/share/QGIS/QGIS3/profiles/default/python/plugins/ + +################################################# +# Normally you would not need to edit below here +################################################# + +HELP = help/build/html + +PLUGIN_UPLOAD = $(c)/plugin_upload.py + +RESOURCE_SRC=$(shell grep '^ *@@g;s/.*>//g' | tr '\n' ' ') + +.PHONY: default +default: + @echo While you can use make to build and deploy your plugin, pb_tool + @echo is a much better solution. + @echo A Python script, pb_tool provides platform independent management of + @echo your plugins and runs anywhere. + @echo You can install pb_tool using: pip install pb_tool + @echo See https://g-sherman.github.io/plugin_build_tool/ for info. + +compile: $(COMPILED_RESOURCE_FILES) + +%.py : %.qrc $(RESOURCES_SRC) + pyrcc5 -o $*.py $< + +%.qm : %.ts + $(LRELEASE) $< + +test: compile transcompile + @echo + @echo "----------------------" + @echo "Regression Test Suite" + @echo "----------------------" + + @# Preceding dash means that make will continue in case of errors + @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ + export QGIS_DEBUG=0; \ + export QGIS_LOG_FILE=/dev/null; \ + nosetests -v --with-id --with-coverage --cover-package=. \ + 3>&1 1>&2 2>&3 3>&- || true + @echo "----------------------" + @echo "If you get a 'no module named qgis.core error, try sourcing" + @echo "the helper script we have provided first then run make test." + @echo "e.g. source run-env-linux.sh ; make test" + @echo "----------------------" + +deploy: compile doc transcompile + @echo + @echo "------------------------------------------" + @echo "Deploying plugin to your .qgis2 directory." + @echo "------------------------------------------" + # The deploy target only works on unix like operating system where + # the Python plugin directory is located at: + # $HOME/$(QGISDIR)/python/plugins + mkdir -p $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(PY_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(UI_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(COMPILED_RESOURCE_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(EXTRAS) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vfr i18n $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vfr $(HELP) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)/help + # Copy extra directories if any + (foreach EXTRA_DIR,(EXTRA_DIRS), cp -R (EXTRA_DIR) (HOME)/(QGISDIR)/python/plugins/(PLUGINNAME)/;) + + +# The dclean target removes compiled python files from plugin directory +# also deletes any .git entry +dclean: + @echo + @echo "-----------------------------------" + @echo "Removing any compiled python files." + @echo "-----------------------------------" + find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname "*.pyc" -delete + find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname ".git" -prune -exec rm -Rf {} \; + + +derase: + @echo + @echo "-------------------------" + @echo "Removing deployed plugin." + @echo "-------------------------" + rm -Rf $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + +zip: deploy dclean + @echo + @echo "---------------------------" + @echo "Creating plugin zip bundle." + @echo "---------------------------" + # The zip target deploys the plugin and creates a zip file with the deployed + # content. You can then upload the zip file on http://plugins.qgis.org + rm -f $(PLUGINNAME).zip + cd $(HOME)/$(QGISDIR)/python/plugins; zip -9r $(CURDIR)/$(PLUGINNAME).zip $(PLUGINNAME) + +package: compile + # Create a zip package of the plugin named $(PLUGINNAME).zip. + # This requires use of git (your plugin development directory must be a + # git repository). + # To use, pass a valid commit or tag as follows: + # make package VERSION=Version_0.3.2 + @echo + @echo "------------------------------------" + @echo "Exporting plugin to zip package. " + @echo "------------------------------------" + rm -f $(PLUGINNAME).zip + git archive --prefix=$(PLUGINNAME)/ -o $(PLUGINNAME).zip $(VERSION) + echo "Created package: $(PLUGINNAME).zip" + +upload: zip + @echo + @echo "-------------------------------------" + @echo "Uploading plugin to QGIS Plugin repo." + @echo "-------------------------------------" + $(PLUGIN_UPLOAD) $(PLUGINNAME).zip + +transup: + @echo + @echo "------------------------------------------------" + @echo "Updating translation files with any new strings." + @echo "------------------------------------------------" + @chmod +x scripts/update-strings.sh + @scripts/update-strings.sh $(LOCALES) + +transcompile: + @echo + @echo "----------------------------------------" + @echo "Compiled translation files to .qm files." + @echo "----------------------------------------" + @chmod +x scripts/compile-strings.sh + @scripts/compile-strings.sh $(LRELEASE) $(LOCALES) + +transclean: + @echo + @echo "------------------------------------" + @echo "Removing compiled translation files." + @echo "------------------------------------" + rm -f i18n/*.qm + +clean: + @echo + @echo "------------------------------------" + @echo "Removing uic and rcc generated files" + @echo "------------------------------------" + rm $(COMPILED_UI_FILES) $(COMPILED_RESOURCE_FILES) + +doc: + @echo + @echo "------------------------------------" + @echo "Building documentation using sphinx." + @echo "------------------------------------" + cd help; make html + +pylint: + @echo + @echo "-----------------" + @echo "Pylint violations" + @echo "-----------------" + @pylint --reports=n --rcfile=pylintrc . || true + @echo + @echo "----------------------" + @echo "If you get a 'no module named qgis.core' error, try sourcing" + @echo "the helper script we have provided first then run make pylint." + @echo "e.g. source run-env-linux.sh ; make pylint" + @echo "----------------------" + + +# Run pep8 style checking +#http://pypi.python.org/pypi/pep8 +pep8: + @echo + @echo "-----------" + @echo "PEP8 issues" + @echo "-----------" + @pep8 --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128 --exclude $(PEP8EXCLUDE) . || true + @echo "-----------" + @echo "Ignored in PEP8 check:" + @echo $(PEP8EXCLUDE) diff --git a/src/shoguneditor/__init__.py b/plugin_code/__init__.py similarity index 54% rename from src/shoguneditor/__init__.py rename to plugin_code/__init__.py index 4e65c6f..1ba43f1 100644 --- a/src/shoguneditor/__init__.py +++ b/plugin_code/__init__.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- -''' +""" /*************************************************************************** - ShogunEditor - A QGIS plugin to connect with a Shogun - GIS client instance on a remote or local server - and edit it's content from QGIS - + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ ------------------- - begin : 2018-05-11 - copyright : (C) 2018 by terrestris GmbH & Co. KG - email : jgrieb (at) terrestris.de, info (at) terrestris.de + begin : 2025-09 + copyright : (C) 202 by terrestris + email : info@terrestris.de git sha : $Format:%H$ ***************************************************************************/ @@ -21,15 +19,24 @@ * (at your option) any later version. * * * ***************************************************************************/ -''' + This script initializes the plugin, making it known to QGIS. +""" + +from qgis.core import QgsMessageLog -def classFactory(iface): - """Load ShogunEditorclass from file shogun_editor. +# noinspection PyPep8Naming +def classFactory(iface): # pylint: disable=invalid-name + """Load ShogunQgisConfigurator class from file ShogunQgisConfigurator. :param iface: A QGIS interface instance. - :type iface: QgisInterface + :type iface: QgsInterface """ # - from .shogun_editor import ShogunEditor - return ShogunEditor(iface) + import os + + project_path = os.path.dirname(__file__) + QgsMessageLog.logMessage(f"Project path: {project_path}", "QgisShogunEditor") + + from .qgis_shogun_editor import QgisShogunEditor + return QgisShogunEditor(iface) diff --git a/plugin_code/exception/GraphQLException.py b/plugin_code/exception/GraphQLException.py new file mode 100644 index 0000000..0b562a3 --- /dev/null +++ b/plugin_code/exception/GraphQLException.py @@ -0,0 +1,2 @@ +class GraphQLException(Exception): + pass diff --git a/plugin_code/exception/__init__.py b/plugin_code/exception/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin_code/metadata.txt b/plugin_code/metadata.txt new file mode 100644 index 0000000..05c0098 --- /dev/null +++ b/plugin_code/metadata.txt @@ -0,0 +1,47 @@ +# This file contains metadata for your plugin. + +# This file should be included when you package your plugin.# Mandatory items: + +[general] +name=Qgis Shogun Editor +qgisMinimumVersion=3.0 +description=Shogun Qgis configuration module +version=0.3.1 +author=terrestris GmbH & Co KG +email=info@terrestris.de + +about=This plugin helps to configure a shogun application (NT) + +tracker=http://bugs +repository=http://repo +# End of mandatory metadata + +# Recommended items: + +hasProcessingProvider=no +# Uncomment the following line and add your changelog: +# changelog= + +# Tags are comma separated with spaces allowed +tags=python + +homepage=http://homepage +category=Plugins +icon=shogun_logo.png +# experimental flag +experimental=True + +# deprecated flag (applies to the whole plugin, not just a single version) +deprecated=False + +# Since QGIS 3.8, a comma separated list of plugins to be installed +# (or upgraded) can be specified. +# Check the documentation for more information. +plugin_dependencies=qpip + +Category of the plugin: Raster, Vector, Database or Web +# category= + +# If the plugin can run on QGIS Server. +server=False + diff --git a/plugin_code/models/Application.py b/plugin_code/models/Application.py new file mode 100644 index 0000000..2df647c --- /dev/null +++ b/plugin_code/models/Application.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +from dataclasses import dataclass +from typing import Any, Dict, Optional + +from PyQt5.QtWidgets import QListWidgetItem + +from ..models.BaseEntity import BaseEntity + + +@dataclass +class Application(BaseEntity): + name: Optional[str] = None + state_only: Optional[bool] = None + client_config: Optional[Dict[str, Any]] = None + layer_tree: Optional[Dict[str, Any]] = None + layer_config: Optional[Dict[str, Any]] = None + tool_config: Optional[Dict[str, Any]] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Application': + base = super().from_dict(data) + return cls( + _id=base._id, + created=base.created, + modified=base.modified, + name=data.get('name'), + state_only=data.get('stateOnly'), + client_config=data.get('clientConfig'), + layer_tree=data.get('layerTree'), + layer_config=data.get('layerConfig'), + tool_config=data.get('toolConfig') + ) + + def get_qt_list_item(self): + item = QListWidgetItem(self.name or "Unnamed Application") + item.application_id = self._id + return item diff --git a/plugin_code/models/BaseEntity.py b/plugin_code/models/BaseEntity.py new file mode 100644 index 0000000..cd9a57f --- /dev/null +++ b/plugin_code/models/BaseEntity.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from typing import Any, Dict, Optional + + +class LayerType(Enum): + TILE_WMS = "TileWMS" + VECTOR_TILE = "VectorTile" + WFS = "WFS" + WMS = "WMS" + WMTS = "WMTS" + XYZ = "XYZ" + + +class RevisionType(Enum): + INSERT = "INSERT" + UPDATE = "UPDATE" + DELETE = "DELETE" + + +@dataclass +class BaseEntity: + _id: Optional[int] = None + created: Optional[datetime] = None + modified: Optional[datetime] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'BaseEntity': + return cls( + _id=data.get('id'), + created=cls._parse_datetime(data.get('created')), + modified=cls._parse_datetime(data.get('modified')) + ) + + @staticmethod + def _parse_datetime(date_str: Optional[str]) -> Optional[datetime]: + if not date_str: + return None + try: + return datetime.fromisoformat(date_str.replace('Z', '+00:00')) + except (ValueError, AttributeError): + return None diff --git a/plugin_code/models/Layer.py b/plugin_code/models/Layer.py new file mode 100644 index 0000000..6688e5c --- /dev/null +++ b/plugin_code/models/Layer.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +from dataclasses import dataclass +from typing import Any, Dict, Optional + +from .BaseEntity import BaseEntity + + +@dataclass +class Layer(BaseEntity): + name: Optional[str] = None + client_config: Optional[Dict[str, Any]] = None + source_config: Optional[Dict[str, Any]] = None + features: Optional[Dict[str, Any]] = None + layerType: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Layer': + base = super().from_dict(data) + return cls( + _id=base._id, + created=base.created, + modified=base.modified, + name=data.get('name'), + client_config=data.get('clientConfig'), + source_config=data.get('sourceConfig'), + features=data.get('features'), + layerType=data.get('type') + ) + + def get_id(self): + return self._id diff --git a/plugin_code/models/MutateApplication.py b/plugin_code/models/MutateApplication.py new file mode 100644 index 0000000..361f7ae --- /dev/null +++ b/plugin_code/models/MutateApplication.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +from dataclasses import dataclass +from typing import Any, Dict, Optional + + +@dataclass +class MutateApplication: + name: str + state_only: Optional[bool] = None + client_config: Optional[Dict[str, Any]] = None + layer_tree: Optional[Dict[str, Any]] = None + layer_config: Optional[Dict[str, Any]] = None + tool_config: Optional[Dict[str, Any]] = None + + def to_dict(self) -> Dict[str, Any]: + data = {"name": self.name} + if self.state_only is not None: + data["stateOnly"] = self.state_only + if self.client_config is not None: + data["clientConfig"] = self.client_config + if self.layer_tree is not None: + data["layerTree"] = self.layer_tree + if self.layer_config is not None: + data["layerConfig"] = self.layer_config + if self.tool_config is not None: + data["toolConfig"] = self.tool_config + return data diff --git a/plugin_code/models/MutateLayer.py b/plugin_code/models/MutateLayer.py new file mode 100644 index 0000000..2654bc4 --- /dev/null +++ b/plugin_code/models/MutateLayer.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +from dataclasses import dataclass +from typing import Any, Dict, Optional + + +@dataclass +class MutateLayer: + name: str + _type: str + client_config: Optional[Dict[str, Any]] = None + source_config: Optional[Dict[str, Any]] = None + features: Optional[Dict[str, Any]] = None + + def to_dict(self) -> Dict[str, Any]: + data = {"name": self.name, "type": self._type} + if self.client_config is not None: + data["clientConfig"] = self.client_config + if self.source_config is not None: + data["sourceConfig"] = self.source_config + if self.features is not None: + data["features"] = self.features + return data diff --git a/plugin_code/models/__init__.py b/plugin_code/models/__init__.py new file mode 100644 index 0000000..40c2b84 --- /dev/null +++ b/plugin_code/models/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" diff --git a/plugin_code/pb_tool.cfg b/plugin_code/pb_tool.cfg new file mode 100644 index 0000000..28dc607 --- /dev/null +++ b/plugin_code/pb_tool.cfg @@ -0,0 +1,80 @@ +#/*************************************************************************** +# QgisShogunEditor +# +# Configuration file for plugin builder tool (pb_tool) +# Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ +# ------------------- +# begin : 2025-09 +# copyright : (C) 2025 by terrestris +# email : info@terrestris.de +# ***************************************************************************/ +# +#/*************************************************************************** +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation; either version 2 of the License, or * +# * (at your option) any later version. * +# * * +# ***************************************************************************/ +# +# +# You can install pb_tool using: +# pip install http://geoapt.net/files/pb_tool.zip +# +# Consider doing your development (and install of pb_tool) in a virtualenv. +# +# For details on setting up and using pb_tool, see: +# http://g-sherman.github.io/plugin_build_tool/ +# +# Issues and pull requests here: +# https://github.com/g-sherman/plugin_build_tool: +# +# Sane defaults for your plugin generated by the Plugin Builder are +# already set below. +# +# As you add Python source files and UI files to your plugin, add +# them to the appropriate [files] section below. + +[plugin] +# Name of the plugin. This is the name of the directory that will +# be created in .qgis2/python/plugins +name: qgis_shogun_editor + +# Full path to where you want your plugin directory copied. If empty, +# the QGIS default path will be used. Don't include the plugin name in +# the path. +plugin_path: + +[files] +# Python files that should be deployed with the plugin +python_files: __init__.py qgis_shogun_editor.py qgis_shogun_editor_dialog.py + +# The main dialog file that is loaded (not compiled) +main_dialog: qgis_shogun_editor_dialog_base.ui + +# Other ui files for dialogs you create (these will be compiled) +compiled_ui_files: + +# Resource file(s) that will be compiled +resource_files: resources.qrc + +# Other files required for the plugin +extras: metadata.txt icon.png + +# Other directories to be deployed with the plugin. +# These must be subdirectories under the plugin directory +extra_dirs: + +# ISO code(s) for any locales (translations), separated by spaces. +# Corresponding .ts files must exist in the i18n directory +locales: + +[help] +# the built help directory that should be deployed with the plugin +dir: help/build/html +# the name of the directory to target in the deployed plugin +target: help + + + diff --git a/plugin_code/plugin_upload.py b/plugin_code/plugin_upload.py new file mode 100644 index 0000000..446f207 --- /dev/null +++ b/plugin_code/plugin_upload.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# coding=utf-8 +"""This script uploads a plugin package to the plugin repository. + Authors: A. Pasotti, V. Picavet + git sha : $TemplateVCSFormat +""" + +import getpass +import sys +import xmlrpc.client +from optparse import OptionParser + +standard_library.install_aliases() # type: ignore + +# Configuration +PROTOCOL = 'https' +SERVER = 'plugins.qgis.org' +PORT = '443' +ENDPOINT = '/plugins/RPC2/' +VERBOSE = False + + +def main(parameters, arguments): + """Main entry point. + + :param parameters: Command line parameters. + :param arguments: Command line arguments. + """ + address = "{protocol}://{username}:{password}@{server}:{port}{endpoint}".format( + protocol=PROTOCOL, + username=parameters.username, + password=parameters.password, + server=parameters.server, + port=parameters.port, + endpoint=ENDPOINT) + print("Connecting to: %s" % hide_password(address)) + + server = xmlrpc.client.ServerProxy(address, verbose=VERBOSE) + + try: + with open(arguments[0], 'rb') as handle: + plugin_id, version_id = server.plugin.upload( + xmlrpc.client.Binary(handle.read())) + print("Plugin ID: %s" % plugin_id) + print("Version ID: %s" % version_id) + except xmlrpc.client.ProtocolError as err: + print("A protocol error occurred") + print("URL: %s" % hide_password(err.url, 0)) + print("HTTP/HTTPS headers: %s" % err.headers) + print("Error code: %d" % err.errcode) + print("Error message: %s" % err.errmsg) + except xmlrpc.client.Fault as err: + print("A fault occurred") + print("Fault code: %d" % err.faultCode) + print("Fault string: %s" % err.faultString) + + +def hide_password(url, start=6): + """Returns the http url with password part replaced with '*'. + + :param url: URL to upload the plugin to. + :type url: str + + :param start: Position of start of password. + :type start: int + """ + start_position = url.find(':', start) + 1 + end_position = url.find('@') + return "%s%s%s" % ( + url[:start_position], + '*' * (end_position - start_position), + url[end_position:]) + + +if __name__ == "__main__": + parser = OptionParser(usage="%prog [options] plugin.zip") + parser.add_option( + "-w", "--password", dest="password", + help="Password for plugin site", metavar="******") + parser.add_option( + "-u", "--username", dest="username", + help="Username of plugin site", metavar="user") + parser.add_option( + "-p", "--port", dest="port", + help="Server port to connect to", metavar="80") + parser.add_option( + "-s", "--server", dest="server", + help="Specify server name", metavar="plugins.qgis.org") + options, args = parser.parse_args() + if len(args) != 1: + print("Please specify zip file.\n") + parser.print_help() + sys.exit(1) + if not options.server: + options.server = SERVER + if not options.port: + options.port = PORT + if not options.username: + # interactive mode + username = getpass.getuser() + print("Please enter user name [%s] :" % username, end=' ') + + res = input() + if res != "": + options.username = res + else: + options.username = username + if not options.password: + # interactive mode + options.password = getpass.getpass() + main(options, args) diff --git a/plugin_code/pylintrc b/plugin_code/pylintrc new file mode 100644 index 0000000..ca29cca --- /dev/null +++ b/plugin_code/pylintrc @@ -0,0 +1,281 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them +disable=locally-disabled,C0103 + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct attribute names in class +# bodies +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/plugin_code/qgis_shogun_editor.py b/plugin_code/qgis_shogun_editor.py new file mode 100644 index 0000000..bb126dd --- /dev/null +++ b/plugin_code/qgis_shogun_editor.py @@ -0,0 +1,543 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +import json +import os.path +import urllib + +from qgis.core import ( + Qgis, + QgsBrowserModel, + QgsCoordinateReferenceSystem, + QgsLayerTreeGroup, + QgsMessageLog, + QgsNetworkAccessManager, + QgsPointXY, + QgsProject, + QgsRasterLayer, + QgsSettings, + QgsVectorLayer, + QgsLayerTreeLayer +) + +from qgis.PyQt.QtCore import QCoreApplication, QSettings, Qt, QTranslator, QUrl, QTimer +from qgis.PyQt.QtGui import QDesktopServices, QIcon, QPixmap +from qgis.PyQt.QtNetwork import QNetworkRequest +from qgis.PyQt.QtWidgets import QAction + +from .models.Application import Application +from .qgis_shogun_editor_dialog import QgisShogunEditorDialog +from .service.Application import ApplicationService +from .service.GraphQLClient import GraphQLClient +from .service.LayerService import LayerService + + +class QgisShogunEditor: + """QGIS Plugin Implementation.""" + + def __init__(self, iface): + """Constructor. + + :param iface: An interface instance that will be passed to this class + which provides the hook by which you can manipulate the QGIS + application at run time. + :type iface: QgsInterface + """ + # Save reference to the QGIS interface + self.layer_service = None + self.app_service = None + self.graphql_client = None + self.iface = iface + # initialize plugin directory + self.plugin_dir = os.path.dirname(__file__) + # TODO initialize locale + locale = QSettings().value('locale/userLocale')[0:2] + locale_path = os.path.join( + self.plugin_dir, + 'i18n', + 'qgis_shogun_editor{}.qm'.format(locale)) + + if os.path.exists(locale_path): + self.translator = QTranslator() + self.translator.load(locale_path) + QCoreApplication.installTranslator(self.translator) + + # Declare instance attributes + self.actions = [] + self.menu = self.tr(u'&Shogun Qgis Configurator') + + # Check if plugin was started the first time in current QGIS session + # Must be set in initGui() to survive plugin reloads + # self.first_start = None + + # read all settings from qgis settings + self.settings = QgsSettings() + + # read actual browser model + self.browser_model = QgsBrowserModel() + + # network access + self.na_manager = QgsNetworkAccessManager.instance() + self.request = QNetworkRequest() + + # Achtung: T/F bei Zertifikaten + self.disable_ssl_verification = self.settings.value( + "/MetaSearch/disableSSL", + True, + bool + ) + + # noinspection PyMethodMayBeStatic + def tr(self, message): + """Get the translation for a string using Qt translation API. + + We implement this ourselves since we do not inherit QObject. + + :param message: String for translation. + :type message: str, QString + + :returns: Translated version of message. + :rtype: QString + """ + # noinspection PyTypeChecker,PyArgumentList,PyCallByClass + return QCoreApplication.translate('ShogunQgisConfigurator', message) + + def add_action( + self, + icon_path, + text, + callback, + enabled_flag=True, + add_to_menu=True, + add_to_toolbar=True, + status_tip=None, + whats_this=None, + parent=None): + """Add a toolbar icon to the toolbar. + + :param icon_path: Path to the icon for this action. Can be a resource + path (e.g. ':/plugins/foo/bar.png') or a normal file system path. + :type icon_path: str + + :param text: Text that should be shown in menu items for this action. + :type text: str + + :param callback: Function to be called when the action is triggered. + :type callback: function + + :param enabled_flag: A flag indicating if the action should be enabled + by default. Defaults to True. + :type enabled_flag: bool + + :param add_to_menu: Flag indicating whether the action should also + be added to the menu. Defaults to True. + :type add_to_menu: bool + + :param add_to_toolbar: Flag indicating whether the action should also + be added to the toolbar. Defaults to True. + :type add_to_toolbar: bool + + :param status_tip: Optional text to show in a popup when mouse pointer + hovers over the action. + :type status_tip: str + + :param parent: Parent widget for the new action. Defaults None. + :type parent: QWidget + + :param whats_this: Optional text to show in the status bar when the + mouse pointer hovers over the action. + + :returns: The action that was created. Note that the action is also + added to self.actions list. + :rtype: QAction + """ + + icon = QIcon(icon_path) + action = QAction(icon, text, parent) + action.triggered.connect(callback) + action.setEnabled(enabled_flag) + + if status_tip is not None: + action.setStatusTip(status_tip) + + if whats_this is not None: + action.setWhatsThis(whats_this) + + if add_to_toolbar: + # Adds plugin icon to Plugins toolbar + self.iface.addToolBarIcon(action) + + if add_to_menu: + self.iface.addPluginToWebMenu( + self.menu, + action) + + self.actions.append(action) + + return action + + def initGui(self): + """Create the menu entries and toolbar icons inside the QGIS GUI.""" + icon_path = os.path.join(os.path.dirname(__file__), "shogun_logo.png") + self.add_action( + icon_path, + text=self.tr(u'Shogun Qgis Configurator'), + callback=self.run, + parent=self.iface.mainWindow()) + + # will be set False in run() + self.first_start = True + + def unload(self): + """Removes the plugin menu item and icon from QGIS GUI.""" + for action in self.actions: + self.iface.removePluginWebMenu( + self.tr(u'Shogun Qgis Configurator'), + action) + self.iface.removeToolBarIcon(action) + + def run(self): + """Run method that performs all the real work""" + + # Create the dialog with elements (after translation) and keep reference + # Only create GUI ONCE in callback, so that it will only load when the plugin is started + if self.first_start: + self.first_start = False + self.dlg = QgisShogunEditorDialog() + + self.dlg.entryUrl.setPlaceholderText('Please enter an URL') + + self.dlg.loadButton.clicked.connect(lambda: self.initialize_graphql_and_load_app_list()) + + self.dlg.applicationsList.itemDoubleClicked.connect(self._handle_double_click) + + # add logo + logo_path = os.path.join(os.path.dirname(__file__), "shogun_logo.png") + if logo_path: + # build + pixmap = QPixmap(logo_path) + # draw preview + self.dlg.labelLogo.setPixmap(pixmap.scaled(self.dlg.labelLogo.size(), + Qt.KeepAspectRatio, Qt.SmoothTransformation)) + self.dlg.labelLogo.mousePressEvent = self.open_project_link + else: + QgsMessageLog.logMessage("An error occured while try to open url: ", 'QgisShogunEditor', + level=Qgis.Critical) + # show the dialog + self.dlg.show() + # Run the dialog event loop + result = self.dlg.exec_() + # See if OK was pressed + if result: + # Do something useful here - delete the line containing pass and + # substitute with your code. + pass + + def find_all_layer_ids(self, layer_tree_json): + layer_ids = [] + + if "layerId" in layer_tree_json: + layer_ids.append(layer_tree_json["layerId"]) + + if "children" in layer_tree_json: + for child in layer_tree_json["children"]: + layer_ids.extend(self.find_all_layer_ids(child)) + + return layer_ids + + def _handle_double_click(self, item): + QgsMessageLog.logMessage( + f"You selected: {item.text()} - application id is: {item.application_id}", 'QgisShogunEditor', + level=Qgis.Info + ) + QgsProject.instance().setTitle(item.text()) + application = self.app_service.get_application_by_id(item.application_id) + self.apply_mapview(application) + if application is not None: + if application.layer_tree is not None: + try: + QgsMessageLog.logMessage( + f"Found layer tree: {application.layer_tree}", + 'QgisShogunEditor', + level=Qgis.Info + ) + QgsMessageLog.logMessage( + "Loading layertree to Qgis now.", + 'QgisShogunEditor', + level=Qgis.Info + ) + + root = QgsProject.instance().layerTreeRoot() + # disconnect old connections and clear root + try: + root.visibilityChanged.disconnect(self.on_visibility_changed) + except TypeError: + QgsMessageLog.logMessage( + "Could not remove the signal visibilityChanged", + 'QgisShogunEditor', + level=Qgis.Info + ) + root.clear() + + layer_ids = self.find_all_layer_ids(application.layer_tree) + layers_content = self.layer_service.get_layers_by_ids(layer_ids) + self.buildLayerTree(application.layer_tree, layers_content, root) + root.visibilityChanged.connect(self.on_visibility_changed) + except json.JSONDecodeError as e: + QgsMessageLog.logMessage( + f"Could not decode layer tree json: {e}", 'QgisShogunEditor', + level=Qgis.Critical + ) + else: + QgsMessageLog.logMessage("Application has no layer tree", 'QgisShogunEditor', level=Qgis.Warning) + + def apply_mapview(self, application: Application): + qgis_project = QgsProject.instance() + client_config = application.client_config + if client_config is not None and 'mapView' in client_config: + map_view = client_config.get('mapView') + projection = map_view.get('projection') + center = map_view.get('center') + crs = QgsCoordinateReferenceSystem(projection) + qgis_project.setCrs(crs) + + map_canvas = self.iface.mapCanvas() + map_canvas.setCenter(QgsPointXY(center[0], center[1])) + + zoom = map_view.get('zoom') + resolutions = map_view.get('resolutions') + if resolutions is not None and zoom is not None and zoom < len(resolutions): + resolution = resolutions[zoom] + dpi = 25.4 / 0.28 + inches_per_meter = 39.37 + map_canvas.zoomScale(resolution * dpi * inches_per_meter) + + def on_visibility_changed(self, node): + if isinstance(node, QgsLayerTreeLayer): + is_visible = node.isVisible() + is_loaded = node.customProperty('loaded', False) + if is_loaded: + return + if not is_visible: + return + + layer_in_tree = node.customProperty('layer_in_tree') + layer_group = node.customProperty('layer_group') + if not layer_in_tree: + return + + parent_node = node.parent() + if not parent_node: + return + + index = parent_node.children().index(node) + layer = self.addQgsLayer( + layer_in_tree, + layer_group, + True, + node.name(), + index + ) + if not layer: + return + + QTimer.singleShot( + 0, + lambda n=node, p=parent_node: p.removeChildNode(n) + ) + node.setCustomProperty('loaded', True) + return + + elif isinstance(node, QgsLayerTreeGroup): + return + + def open_project_link(self, event): + if event.button() == Qt.LeftButton: + QDesktopServices.openUrl(QUrl("https://www.terrestris.de/de/")) + + def createWmsLayerFromShogun(self, layer_src_conf): + layerNames = layer_src_conf['layerNames'] + layer_url = layer_src_conf['url'] + if str(layer_url).startswith('/'): + layer_url = self.dlg.entryUrl.text() + layer_url + + params = { + 'layers': layerNames, + 'styles': '', + 'format': 'image/png', + 'crs': 'EPSG:' + str(QgsProject.instance().crs().srsid()), + 'url': layer_url + } + + # set transparency if present + try: + params['transparent'] = layer_src_conf['requestParams']['TRANSPARENT'] + print('set transparent') + except KeyError: + print('no transparency') + + uri = '&'.join([f"{k}={v}" for k, v in params.items()]) + layer = QgsRasterLayer(uri, layerNames, 'wms') + + if layer.isValid(): + return layer + else: + return False + + def createTileWmsLayerFromShogun(self, layer_src_conf): + layerNames = layer_src_conf['layerNames'] + layer_url = layer_src_conf['url'] + if str(layer_url).startswith('/'): + layer_url = self.dlg.entryUrl.text() + layer_url + + params = { + 'layers': layerNames, + 'styles': '', + 'format': 'image/png', + 'crs': 'EPSG:' + str(QgsProject.instance().crs().srsid()), + 'url': layer_url, + 'tiled': 'true' + } + + # set transparency if present + try: + params['transparent'] = layer_src_conf['requestParams']['TRANSPARENT'] + print('set transparent') + except KeyError: + print('no transparency') + + uri = '&'.join([f"{k}={v}" for k, v in params.items()]) + layer = QgsRasterLayer(uri, layerNames, 'wms') + + if layer.isValid(): + return layer + else: + return False + + def createWfsLayerFromShogun(self, layer_src_conf): + layerNames = layer_src_conf['layerNames'] + layer_url = layer_src_conf['url'] + if str(layer_url).startswith('/'): + layer_url = self.dlg.entryUrl.text() + layer_url + + params = { + 'service': 'WFS', + 'version': '2.0.0', + 'request': 'GetFeature', + 'typename': layerNames, + 'srsname': 'EPSG:' + str(QgsProject.instance().crs().srsid()) + } + + uri = layer_url + urllib.parse.unquote(urllib.parse.urlencode(params)) + layer = QgsVectorLayer(uri, layerNames, 'WFS') + + if layer.isValid(): + return layer + else: + return False + + def createLayer(self, layer_in_tree): + layer_src_conf = layer_in_tree.source_config + data_type = layer_in_tree.layerType + print('createLayer', ) + + if data_type == 'WMS': + print('create WMS layer') + return self.createWmsLayerFromShogun(layer_src_conf) + + elif data_type == 'TILEWMS': + print('create TILEWMS layer') + return self.createTileWmsLayerFromShogun(layer_src_conf) + + elif data_type == 'WFS': + print('create WFS layer') + return self.createWfsLayerFromShogun(layer_src_conf) + + def addQgsLayer(self, layer_in_tree, layer_group, layer_is_visible, title, index=None): + self.qgisLayers = [] + # layerutils + layer = self.createLayer(layer_in_tree) + if not layer: + QgsMessageLog.logMessage( + f"Could not create layer: {layer_in_tree}", 'QgisShogunEditor', + level=Qgis.Critical + ) + return + + # explicit addition + if index is not None: + layer_group_layer = layer_group.insertLayer(index, layer) + else: + layer_group_layer = layer_group.addLayer(layer) + + if title or title != '': + layer_group_layer.setName(title) + if layer_is_visible: + QgsProject.instance().addMapLayer(layer, False) # implicit addition + return layer + + def buildLayerTree(self, applications_layertree, layers_content, root): + if 'layerId' not in applications_layertree: + new_group = root.addGroup(applications_layertree['title']) # option: take the name of the application + new_group.setItemVisibilityChecked(applications_layertree.get('checked', True)) + + if 'children' in applications_layertree: + for child in applications_layertree['children']: + self.buildLayerTree(child, layers_content, new_group) + return new_group + + if 'layerId' in applications_layertree: + layer_in_tree = next((layer for layer in layers_content if layer.get_id() == applications_layertree['layerId']), None) + layer_is_visible = applications_layertree.get('checked', False) + layer = self.addQgsLayer(layer_in_tree, root, layer_is_visible, applications_layertree['title']) + if layer: + layer_node = root.findLayer(layer.id()) + if layer_node: + layer_node.setItemVisibilityChecked(layer_is_visible) + layer_node.setCustomProperty('layer_in_tree', layer_in_tree) + layer_node.setCustomProperty('layer_group', root) + return layer + + def sanitize_shogun_url(self, shogun_url): + if shogun_url.endswith('/graphql'): + return shogun_url + elif shogun_url.endswith('/'): + shogun_url += 'graphql' + return shogun_url + else: + shogun_url += '/graphql' + return shogun_url + + def initialize_graphql_client(self): + sanitized_url = self.sanitize_shogun_url(self.dlg.entryUrl.text().strip()) + self.graphql_client = GraphQLClient(sanitized_url) + self.app_service = ApplicationService(self.graphql_client) + self.layer_service = LayerService(self.graphql_client) + + # Example usage in your code + def initialize_graphql_and_load_app_list(self): + self.initialize_graphql_client() + applications = self.app_service.get_all_applications_simple() + if applications is not None: + self.dlg.applicationsList.clear() + for app in applications: + self.dlg.applicationsList.addItem(app.get_qt_list_item()) diff --git a/plugin_code/qgis_shogun_editor_dialog.py b/plugin_code/qgis_shogun_editor_dialog.py new file mode 100644 index 0000000..bdcb61d --- /dev/null +++ b/plugin_code/qgis_shogun_editor_dialog.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditorDialog + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import os +from importlib import import_module + +from qgis.PyQt import QtWidgets, uic + +# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer +FORM_CLASS, _ = uic.loadUiType(os.path.join( + os.path.dirname(__file__), 'qgis_shogun_editor_dialog_base.ui')) + + +class QgisShogunEditorDialog(QtWidgets.QDialog, FORM_CLASS): + def __init__(self, parent=None): + """Constructor.""" + super(QgisShogunEditorDialog, self).__init__(parent) + # Set up the user interface from Designer through FORM_CLASS. + # After self.setupUi() you can access any designer object by doing + # self., and you can use autoconnect slots - see + # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html + # #widgets-and-dialogs-with-auto-connect + + self.setupUi(self) diff --git a/plugin_code/qgis_shogun_editor_dialog_base.ui b/plugin_code/qgis_shogun_editor_dialog_base.ui new file mode 100644 index 0000000..878d723 --- /dev/null +++ b/plugin_code/qgis_shogun_editor_dialog_base.ui @@ -0,0 +1,59 @@ + + + QgisShogunEditorDialogBase + + + + 0 + 0 + 512 + 351 + + + + Shogun-Qgis-Configurator + + + + + + Logo + + + + + + + Enter Shogun URL + + + + + + + Load Shogun + + + + + + + Begin search + + + + + + + + 256 + 192 + + + + + + + + + diff --git a/plugin_code/requirements.txt b/plugin_code/requirements.txt new file mode 100644 index 0000000..c86477f --- /dev/null +++ b/plugin_code/requirements.txt @@ -0,0 +1,2 @@ +python-keycloak +requests==2.31.0 diff --git a/plugin_code/resource_column.py b/plugin_code/resource_column.py new file mode 100644 index 0000000..044b0f4 --- /dev/null +++ b/plugin_code/resource_column.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class ResourceColumn(Enum): + TITLE = 0 + IDENTIFIER = 1 + TYPE = 2 diff --git a/plugin_code/resources.py b/plugin_code/resources.py new file mode 100644 index 0000000..cdf51e4 --- /dev/null +++ b/plugin_code/resources.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x04\x0a\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x17\x00\x00\x00\x18\x08\x06\x00\x00\x00\x11\x7c\x66\x75\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\ +\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xd9\x02\x15\ +\x16\x11\x2c\x9d\x48\x83\xbb\x00\x00\x03\x8a\x49\x44\x41\x54\x48\ +\xc7\xad\x95\x4b\x68\x5c\x55\x18\xc7\x7f\xe7\xdc\x7b\x67\xe6\xce\ +\x4c\x66\x26\x49\xd3\x24\x26\xa6\xc6\xf8\x40\x21\xa5\x04\xb3\x28\ +\xda\x98\x20\xa5\x0b\xad\x55\xa8\x2b\xc5\x50\x1f\xa0\x6e\x34\x2b\ +\x45\x30\x14\x02\xba\x52\x69\x15\x17\x66\x63\x45\x97\x95\xa0\xad\ +\x0b\xfb\xc0\x06\x25\xb6\x71\x61\x12\x41\x50\xdb\x2a\x21\xd1\xe2\ +\x24\xf3\x9e\xc9\xcc\xbd\xe7\x1c\x17\x35\x43\x1e\x33\x21\xb6\xfd\ +\x56\x87\xf3\x9d\xfb\xfb\x1e\xf7\xff\x9d\x23\x8c\x31\x43\x95\xf4\ +\x85\x1e\x3f\x3b\x35\xac\xfd\xcc\x43\xdc\xa4\x49\x3b\xfe\x9d\x1d\ +\xdb\x7b\x22\x90\x78\xf8\xb2\x28\xa7\xbe\x7d\xc1\x4b\x9d\x79\xdf\ +\x18\x15\xe5\x16\x99\x10\x56\xde\x69\xdc\x3f\x22\xfd\xec\xd4\xf0\ +\xad\x04\x03\x18\xa3\xa2\x7e\x76\x6a\x58\xde\x68\x2b\xb4\x36\xf8\ +\xbe\xc6\x18\x53\xdb\xef\xe7\xfa\xec\xed\x67\x63\x10\x42\x00\xf0\ +\xfb\xd5\x65\x2a\x15\x45\xc7\x6d\x0d\x00\xc4\xa2\xc1\xaa\x6f\x0d\ +\x3e\x6c\xab\xc2\x1c\x56\xa4\x77\x4b\xb0\xf2\x35\x15\x5f\x21\x85\ +\xe0\xc8\x6b\x5f\x92\x2d\x37\x33\x39\xf9\x03\x27\x8e\x1f\xa2\xf7\ +\xbe\x9d\x04\x1c\x0b\x37\xe4\xac\xff\xa6\x30\x87\xbd\xba\x00\x6a\ +\x06\x79\xe5\xf5\xaf\x89\xd9\x92\xc5\xcc\x0a\xd9\x7c\x19\xcf\xe9\ +\xe2\xe4\xa9\x2f\x78\x7c\xff\x01\x72\x85\x0a\x2b\x65\x1f\xa5\x4c\ +\xb5\xb2\x55\x16\x80\xbd\x31\xda\xda\x20\x1f\x7d\x3e\xcd\xc2\xfd\ +\x59\xa6\x93\x39\x92\xd1\x22\xea\x9b\x16\xce\x9d\x3f\xce\xe0\x83\ +\x03\x24\x82\x59\x3a\xdb\x7b\x88\xc7\x82\x68\x63\x58\xc9\xcc\x62\ +\x8c\x21\x18\xb0\x6a\xc3\x37\x06\x49\x16\xff\x24\x6b\xa5\x49\xbb\ +\x25\xbc\xa2\xa6\x21\xbb\x40\x7f\xdf\x00\x83\xbd\x01\x8e\x3c\xd5\ +\x45\xd7\x8e\x6b\x9c\x9c\x98\x25\x1a\xb6\xe8\xbe\x3d\xc2\xdd\x77\ +\x44\x48\xc4\x1c\x22\xe1\xeb\x58\x59\xaf\xcf\xd3\x33\x29\x2e\x34\ +\x2d\x91\x93\x3e\xbe\x34\x78\x01\xc5\xe2\x61\xc5\xae\x72\x8e\x70\ +\xc8\xc2\x0d\x5a\xbc\xf5\xee\x2f\x9c\xfa\x3e\x86\x69\x7a\x8e\xcf\ +\x26\xe6\xf9\x63\xa1\x44\xa1\xa4\xd0\xda\x6c\x0d\x2f\x15\x7c\xb4\ +\x67\x28\x59\x0a\xcf\xd6\x54\xe2\x06\x13\x87\x2b\x6f\x68\xa6\x27\ +\xaf\x31\x32\x36\xc7\xb2\x7f\x17\xef\x7d\x7c\x8c\x33\x67\xcf\x12\ +\x70\x24\x4a\x69\xd6\x6a\x46\xd6\xd3\x70\x72\xa9\x82\x67\x34\x45\ +\xad\x28\xdb\x1a\x15\x34\x98\xff\x46\xed\xef\x37\x0d\x99\xbf\x4a\ +\x3c\x30\x38\xc0\xc8\x4b\xaf\x92\x5a\x9c\xe2\xe0\x23\x6d\x74\xb4\ +\xba\x84\x5d\x0b\x29\x45\x7d\xb8\x94\x82\x96\xb6\x10\xf3\xc5\x12\ +\x2a\xef\x53\x11\x1a\x63\xad\x3f\x93\x19\x85\xf1\xb1\x77\x58\x5a\ +\xf8\x99\x97\x9f\xe9\xa6\x75\x47\x90\xc6\xb8\x43\xd8\xb5\xb6\xce\ +\xfc\xfa\xfd\x00\xfb\x3e\xf4\xc8\x05\x35\xba\x5e\xeb\x46\x21\xf9\ +\xcf\x0a\xa9\x8c\x87\xe3\x48\xdc\x90\xb5\x6e\x98\x6a\xaa\x65\xf2\ +\x52\x92\x43\x2f\x5e\xc2\x8c\x02\x1a\x10\xf5\x07\xac\xc3\x75\x70\ +\x83\x92\x80\xb3\xf9\xd0\x26\xf8\x8f\xb3\x29\xc6\x3e\xb8\x8c\x19\ +\x35\x75\x6b\x7b\x7e\x3c\xca\x45\x0c\x7e\x49\x31\xf4\x58\x3b\xf7\ +\xf6\x34\x90\x88\x39\x04\x1c\x59\x1f\xfe\xdb\xd5\x3c\x5f\x9d\x4b\ +\x32\xfd\x44\xb2\xba\xd7\xfa\xb6\x60\xcf\xde\x16\xdc\x90\x45\x4c\ +\x4a\x2a\x9e\x62\xfe\x4e\xc5\xc8\xc1\x4e\xda\x76\x86\xe8\xe9\x0a\ +\xe3\xd8\x92\x58\xd4\xc6\xb2\x44\x6d\x78\x2a\x53\xe1\xca\x7c\x99\ +\x63\x5d\xbf\x56\x9d\xbd\x9f\x44\x18\x7a\xba\x95\x27\x0f\xb4\xd3\ +\xdc\x18\xc0\xf3\x0d\x52\x40\xd8\xb5\xb0\xa4\x20\x14\xb2\x70\x6c\ +\x81\x63\xcb\xaa\x42\xd6\xfd\xb7\xf4\xec\xa3\x06\xa0\x50\x52\xd8\ +\x4e\x1b\x7e\x4a\xd3\x31\xf9\x29\xcf\xfe\xd4\x49\x7f\x5f\x13\xfb\ +\xfa\x9b\x71\x43\x92\x58\xd4\x21\x18\x90\xac\xde\xb0\x42\x50\x13\ +\x58\x33\xf3\x88\x6b\xa1\xfd\x65\x96\xf2\x79\xc6\x43\x7b\xd8\x75\ +\x38\xcc\x3d\xdd\xd1\xaa\xcf\x71\xe4\xff\x7f\x91\x56\x33\xaf\xea\ +\x37\xe7\xa1\x94\x21\x16\xb5\xd1\x06\x2c\x29\x36\xf5\x72\x9b\x96\ +\x95\xc0\xc4\xda\x9d\x78\x83\x43\x53\x22\x80\x65\x09\x1c\xfb\x86\ +\xc1\x00\xe7\x25\x70\x14\x48\x6f\x1e\x22\x51\xe3\x75\xd9\xb6\xa5\ +\x81\xa3\x32\xb1\xfb\xf4\x0c\x30\xb8\xb1\x82\x9b\xb0\x09\x60\x30\ +\xb1\xfb\xf4\xcc\xbf\xa0\xe9\x6e\xae\x5a\xdf\x4b\x81\x00\x00\x00\ +\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x07\ +\x07\x3b\xe0\xb3\ +\x00\x70\ +\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ +\x00\x15\ +\x06\xb4\x60\x98\ +\x00\x67\ +\x00\x70\x00\x72\x00\x6c\x00\x70\x00\x5f\x00\x6d\x00\x65\x00\x74\x00\x61\x00\x64\x00\x61\x00\x74\x00\x61\x00\x5f\x00\x73\x00\x65\ +\x00\x61\x00\x72\x00\x63\x00\x68\ +\x00\x08\ +\x0a\x61\x5a\xa7\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ +\x00\x00\x00\x44\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x44\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x7e\xe0\x16\xbc\x03\ +" + +qt_version = [int(v) for v in QtCore.qVersion().split(".")] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + + +def qInitResources(): + QtCore.qRegisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + + +def qCleanupResources(): + QtCore.qUnregisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + + +qInitResources() diff --git a/plugin_code/resources.qrc b/plugin_code/resources.qrc new file mode 100644 index 0000000..c01356f --- /dev/null +++ b/plugin_code/resources.qrc @@ -0,0 +1,5 @@ + + + icon.png + + diff --git a/plugin_code/service/Application.py b/plugin_code/service/Application.py new file mode 100644 index 0000000..1e441f7 --- /dev/null +++ b/plugin_code/service/Application.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +from datetime import datetime +from typing import List, Optional + +from ..models.Application import Application +from ..models.MutateApplication import MutateApplication +from ..service.GraphQLClient import GraphQLClient + + +class ApplicationService: + def __init__(self, client: GraphQLClient): + self.client = client + + def get_all_applications(self) -> List[Application]: + query = """ + query { + allApplications { + id + created + modified + name + stateOnly + clientConfig + layerTree + layerConfig + toolConfig + } + } + """ + data = self.client.execute_query(query) + return [Application.from_dict(app) for app in data.get('allApplications', [])] + + def get_all_applications_simple(self) -> List[Application]: + query = """ + query { + allApplications { + id + name + } + } + """ + data = self.client.execute_query(query) + return [Application.from_dict(app) for app in data.get('allApplications', [])] + + def get_application_by_id(self, app_id: int) -> Optional[Application]: + query = """ + query GetApplication($id: Int) { + applicationById(id: $id) { + id + created + modified + name + stateOnly + clientConfig + layerTree + layerConfig + toolConfig + } + } + """ + data = self.client.execute_query(query, {'id': app_id}) + app_data = data.get('applicationById') + return Application.from_dict(app_data) if app_data else None + + def get_application_by_id_and_time(self, app_id: int, time: datetime) -> Optional[Application]: + query = """ + query GetApplicationByTime($id: Int, $time: DateTime) { + applicationByIdAndTime(id: $id, time: $time) { + id + created + modified + name + stateOnly + clientConfig + layerTree + layerConfig + toolConfig + } + } + """ + data = self.client.execute_query(query, {'id': app_id, 'time': time.isoformat()}) + app_data = data.get('applicationByIdAndTime') + return Application.from_dict(app_data) if app_data else None + + def get_applications_by_ids(self, ids: List[int]) -> List[Application]: + query = """ + query GetApplicationsByIds($ids: [Int]) { + allApplicationsByIds(ids: $ids) { + id + created + modified + name + stateOnly + clientConfig + layerTree + layerConfig + toolConfig + } + } + """ + data = self.client.execute_query(query, {'ids': ids}) + return [Application.from_dict(app) for app in data.get('allApplicationsByIds', [])] + + def create_application(self, application: MutateApplication) -> Application: + query = """ + mutation CreateApplication($entity: MutateApplication) { + createApplication(entity: $entity) { + id + created + modified + name + stateOnly + clientConfig + layerTree + layerConfig + toolConfig + } + } + """ + data = self.client.execute_query(query, {'entity': application.to_dict()}) + return Application.from_dict(data['createApplication']) + + def update_application(self, app_id: int, application: MutateApplication) -> Application: + query = """ + mutation UpdateApplication($id: Int, $entity: MutateApplication) { + updateApplication(id: $id, entity: $entity) { + id + created + modified + name + stateOnly + clientConfig + layerTree + layerConfig + toolConfig + } + } + """ + data = self.client.execute_query(query, {'id': app_id, 'entity': application.to_dict()}) + return Application.from_dict(data['updateApplication']) + + def delete_application(self, app_id: int) -> bool: + query = """ + mutation DeleteApplication($id: Int) { + deleteApplication(id: $id) + } + """ + data = self.client.execute_query(query, {'id': app_id}) + return data.get('deleteApplication', False) diff --git a/plugin_code/service/GraphQLClient.py b/plugin_code/service/GraphQLClient.py new file mode 100644 index 0000000..64b5dc8 --- /dev/null +++ b/plugin_code/service/GraphQLClient.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +import json +from typing import Any, Dict, Optional + +import requests + +from ..exception.GraphQLException import GraphQLException + + +class GraphQLClient: + def __init__(self, endpoint_url: str, headers: Optional[Dict[str, str]] = None): + self.endpoint_url = endpoint_url + self.headers = headers or {} + self.session = requests.Session() + + # TODO: keycloak authentication required + self.session.headers.update({ + 'Content-Type': 'application/json', + **self.headers + }) + + def execute_query(self, query: str, variables: Optional[Dict] = None) -> Dict[str, Any]: + payload = { + 'query': query, + 'variables': variables or {} + } + + try: + response = self.session.post( + self.endpoint_url, + json=payload, + timeout=30 + ) + response.raise_for_status() + + data = response.json() + if 'errors' in data: + raise GraphQLException(f"GraphQL errors: {data['errors']}") + + return data.get('data', {}) + except requests.exceptions.RequestException as e: + raise GraphQLException(f"Network error: {str(e)}") + except json.JSONDecodeError as e: + raise GraphQLException(f"JSON decode error: {str(e)}") diff --git a/plugin_code/service/LayerService.py b/plugin_code/service/LayerService.py new file mode 100644 index 0000000..39a412b --- /dev/null +++ b/plugin_code/service/LayerService.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +from typing import List, Optional + +from ..models.Layer import Layer +from ..models.MutateLayer import MutateLayer +from .GraphQLClient import GraphQLClient + + +class LayerService: + def __init__(self, client: GraphQLClient): + self.client = client + + def get_all_layers(self) -> List[Layer]: + query = """ + query { + allLayers { + id + created + modified + name + clientConfig + sourceConfig + features + type + } + } + """ + data = self.client.execute_query(query) + return [Layer.from_dict(layer) for layer in data.get('allLayers', [])] + + def get_layers_by_ids(self, layer_ids: List[int]) -> List[Layer]: + query = """ + query GetLayers($ids: [Int]) { + allLayersByIds(ids: $ids) { + id + created + modified + name + clientConfig + sourceConfig + features + type + } + } + """ + data = self.client.execute_query(query, {'ids': layer_ids}) + return [Layer.from_dict(layer) for layer in data.get('allLayersByIds', [])] + + def get_layer_by_id(self, layer_id: int) -> Optional[Layer]: + query = """ + query GetLayer($id: Int) { + layerById(id: $id) { + id + created + modified + name + clientConfig + sourceConfig + features + type + } + } + """ + data = self.client.execute_query(query, {'id': layer_id}) + layer_data = data.get('layerById') + return Layer.from_dict(layer_data) if layer_data else None + + def create_layer(self, layer: MutateLayer) -> Layer: + query = """ + mutation CreateLayer($entity: MutateLayer) { + createLayer(entity: $entity) { + id + created + modified + name + clientConfig + sourceConfig + features + type + } + } + """ + data = self.client.execute_query(query, {'entity': layer.to_dict()}) + return Layer.from_dict(data['createLayer']) + + def update_layer(self, layer_id: int, layer: MutateLayer) -> Layer: + query = """ + mutation UpdateLayer($id: Int, $entity: MutateLayer) { + updateLayer(id: $id, entity: $entity) { + id + created + modified + name + clientConfig + sourceConfig + features + type + } + } + """ + data = self.client.execute_query(query, {'id': layer_id, 'entity': layer.to_dict()}) + return Layer.from_dict(data['updateLayer']) + + def delete_layer(self, layer_id: int) -> bool: + query = """ + mutation DeleteLayer($id: Int) { + deleteLayer(id: $id) + } + """ + data = self.client.execute_query(query, {'id': layer_id}) + return data.get('deleteLayer', False) diff --git a/plugin_code/service/__init__.py b/plugin_code/service/__init__.py new file mode 100644 index 0000000..40c2b84 --- /dev/null +++ b/plugin_code/service/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QgisShogunEditor + A QGIS plugin + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2025-09 + git sha : $Format:%H$ + copyright : (C) 2025 by terrestris + email : info@terrestris.de + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" diff --git a/plugin_code/shogun_logo.png b/plugin_code/shogun_logo.png new file mode 100644 index 0000000..b46fed1 Binary files /dev/null and b/plugin_code/shogun_logo.png differ diff --git a/requirements.development.txt b/requirements.development.txt new file mode 100644 index 0000000..15abadd --- /dev/null +++ b/requirements.development.txt @@ -0,0 +1,9 @@ +flake8 +flake8-builtins +flake8-isort +setuptools +pylint +pycodestyle +autopep8 +black +debugpy diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cc2c068 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,31 @@ +[metadata] +description_file = README.md + +# -- Code quality ------------------------------------ +[flake8] +count = True +exclude = venv,env,__pycache__,build,dist,.git,.eggs,*.egg-info,site-packages +ignore = T201, W503, W504, C901 +max-complexity = 20 +max-doc-length = 130 +max-line-length = 120 +per-file-ignores = + __init__.py: E501, W505 + plugin_upload.py: E501, W505, F821 + resources.py: E501, W505 + qgis_shogun_editor.py: F401, E501, F403, W505 + qgis_shogun_editor_dialog.py: F401, E501, F403, W505 +per-file-line_length = 21 + +[isort] +ensure_newline_before_comments = True +force_grid_wrap = 0 +include_trailing_comma = True +line_length = 120 +multi_line_output = 3 +profile = black +use_parentheses = True + +[pycodestyle] +exclude = venv,env,__pycache__,build,dist,.git,.eggs,*.egg-info,site-packages +max-line-length = 120 diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..53e4ea3 --- /dev/null +++ b/setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +xhost + + +function resetxhost { + echo "Resetting xhost" + xhost - +} + +trap resetxhost EXIT +docker compose -f docker/docker-compose-dev.yml up + +#--force-recreate diff --git a/src/shoguneditor/README.html b/src/shoguneditor/README.html deleted file mode 100644 index 2542dd7..0000000 --- a/src/shoguneditor/README.html +++ /dev/null @@ -1,69 +0,0 @@ - - -

Shogun Editor for QGIS. A plugin

-
-
- -
-

Info:

-A QGIS plugin for editing a Shogun GIS client instance. See https://github.com/terrestris/shogun2
---Link will be replaced when Shogun2-webapp will be released--
-After connecting with the Shogun ressource (note: that ressource needs to have basic authentication possibility),
-the user can edit applications and layers on the Shogun server, in interaction with the QGIS interface.
-
-
-Features: -
    -
  • Add layers from Shogun to QGIS (as WFS, WMS or raw data) -
  • -
  • Upload new layers (vector and raster format) from QGIS to the Shogun server -
  • -
  • Upload layer style (vector layers only) from QGIS and apply to the layers in Shogun -> most basic styling is implemented, also the upload of custom symbols for point layers -
  • -
  • Edit static data like names, descriptions, ... -
  • -
  • Create new Shogun applications -
  • -
  • Set applications homeview directly from the QGIS interface -
- - -

Installation

-You can install the plugin via the QGIS plugin repository or manually. -Manually installing: Copy the shogun-editor directory to your qgis plugins -directory which you should find at: -
    -
  • QGIS 2.x: -
    usually in your home directory you find: - .qgis2/python/plugins/ - if you need more details see this - link (chapter "The Copy Method") -
    -
  • -
  • QGIS 3.x: -
    To find the plugins folder, open up QGIS, in the menu go to - Settings->User Profiles -> Open active profile folder -
    In the file explorer, go to python/plugins/ and paste - the folder there -
-
After copying just open up QGIS, activate the plugin in the -plugin manager and you are done -
-
-

Important notes & missing features

-
    -
  • The plugin works with QGIS 2.x and 3.x, but currently there is a problem with adding wfs layers - to QGIS ins 3.x, which has to be resolved -
  • -
  • As already mentioned, the plugin works with basic authentication requests and - therefore can only be used with Shogun2-Webapp installations which support basic authentication -
  • -
  • Layer styles based on custom icons in Shogun currently cannot be imported -to QGIS, but styles with custom icons created in QGIS can be uploaded to Shogun -
  • -
  • Layer styles based on font symbols are currently not supported by the plugin -
- -
- - diff --git a/src/shoguneditor/README.txt b/src/shoguneditor/README.txt deleted file mode 100644 index 428dc3c..0000000 --- a/src/shoguneditor/README.txt +++ /dev/null @@ -1,38 +0,0 @@ -Shogun Editor for QGIS. A plugin - - -Info: -A QGIS plugin for editing a Shogun GIS client instance. See https://github.com/terrestris/shogun2 ---Link will be replaced when Shogun2-webapp will be released-- -After connecting with the Shogun ressource (note: that ressource needs to have basic authentication possibility), -the user can edit applications and layers on the Shogun server, in interaction with the QGIS interface. - - -Features: - - Add layers from Shogun to QGIS (as WFS, WMS or raw data) - Upload new layers (vector and raster format) from QGIS to the Shogun server - Upload layer style (vector layers only) from QGIS and apply to the layers in Shogun -> most basic styling is implemented, also the upload of custom symbols for point layers - Edit static data like names, descriptions, ... - Create new Shogun applications - Set applications homeview directly from the QGIS interface - -Installation -You can install the plugin via the QGIS plugin repository or manually. Manually installing: Copy the shogun-editor directory to your qgis plugins directory which you should find at: - - QGIS 2.x: - usually in your home directory you find: .qgis2/python/plugins/ - if you need more details see this link (chapter "The Copy Method") - QGIS 3.x: - To find the plugins folder, open up QGIS, in the menu go to Settings->User Profiles -> Open active profile folder - In the file explorer, go to python/plugins/ and paste the folder there - - -After copying just open up QGIS, activate the plugin in the plugin manager and you are done - -Important notes & missing features - - The plugin works with QGIS 2.x and 3.x, but currently there is a problem with adding wfs layers - to QGIS ins 3.x, which has to be resolved. - As already mentioned, the plugin works with basic authentication requests and therefore can only be used with Shogun2-Webapp installations which support basic authentication - Layer styles based on custom icons in Shogun currently cannot be imported to QGIS, but styles with custom icons created in QGIS can be uploaded to Shogun - Layer styles based on font symbols are currently not supported by the plugin diff --git a/src/shoguneditor/connection/__init__.py b/src/shoguneditor/connection/__init__.py deleted file mode 100644 index 4e36e95..0000000 --- a/src/shoguneditor/connection/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' diff --git a/src/shoguneditor/connection/networkaccessmanager.py b/src/shoguneditor/connection/networkaccessmanager.py deleted file mode 100644 index e3b64db..0000000 --- a/src/shoguneditor/connection/networkaccessmanager.py +++ /dev/null @@ -1,316 +0,0 @@ -# -*- coding: utf-8 -*- -""" -*************************************************************************** - An httplib2 replacement that uses QgsNetworkAccessManager - - --------------------- - Date : August 2016 - Copyright : (C) 2016 Boundless, http://boundlessgeo.com - Email : apasotti at boundlessgeo dot com -*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -*************************************************************************** - -Minor changes by J. Grieb (jgrieb (at) terrestris.de) in 2018 for the -shogun-editor plugin by terrestris GmbH & Co. KG (https://www.terrestris.de/en/) --> for re-using purposes better use the original code -""" - - -__author__ = 'Alessandro Pasotti' -__date__ = 'August 2016' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtCore import QUrl - from qgis.PyQt.QtCore import pyqtSlot, QEventLoop - from qgis.PyQt.QtNetwork import QNetworkRequest, QNetworkReply - import urllib -else: - from PyQt4.QtCore import QUrl - from PyQt4.QtCore import pyqtSlot, QEventLoop - from PyQt4.QtNetwork import QNetworkRequest, QNetworkReply - import urllib2 - -from qgis.core import QgsNetworkAccessManager, QgsAuthManager, QgsMessageLog - -# FIXME: ignored -DEFAULT_MAX_REDIRECTS = 4 - -class RequestsException(Exception): - pass - -class RequestsExceptionTimeout(RequestsException): - pass - -class RequestsExceptionConnectionError(RequestsException): - pass - -class Map(dict): - """ - Example: - m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) - """ - def __init__(self, *args, **kwargs): - super(Map, self).__init__(*args, **kwargs) - for arg in args: - if isinstance(arg, dict): - for k, v in arg.items(): - self[k] = v - - if kwargs: - for k, v in kwargs.items(): - self[k] = v - - def __getattr__(self, attr): - return self.get(attr) - - def __setattr__(self, key, value): - self.__setitem__(key, value) - - def __setitem__(self, key, value): - super(Map, self).__setitem__(key, value) - self.__dict__.update({key: value}) - - def __delattr__(self, item): - self.__delitem__(item) - - def __delitem__(self, key): - super(Map, self).__delitem__(key) - del self.__dict__[key] - - -class Response(Map): - pass - -PYTHON_VERSION = sys.version_info[0] - -class NetworkAccessManager(): - """ - This class mimicks httplib2 by using QgsNetworkAccessManager for all - network calls. - - The return value is a tuple of (response, content), the first being and - instance of the Response class, the second being a string that contains - the response entity body. - - Parameters - ---------- - debug : bool - verbose logging if True - exception_class : Exception - Custom exception class - - Usage - ----- - :: - nam = NetworkAccessManager(authcgf) - try: - (response, content) = nam.request('http://www.example.com') - except RequestsException, e: - # Handle exception - pass - - - """ - - - def __init__(self, authid=None, disable_ssl_certificate_validation=False, exception_class=None, debug=True): - self.disable_ssl_certificate_validation = disable_ssl_certificate_validation - self.authid = authid - self.reply = None - self.debug = debug - self.exception_class = exception_class - self.cookie = None - self.basicauth = None - - def setBasicauth(self, encodedString): - self.basicauth = encodedString - - def setCookie(self, cookie): - self.cookie = cookie - - def msg_log(self, msg): - if self.debug: - QgsMessageLog.logMessage(msg, "NetworkAccessManager") - - def request(self, url, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None, authenticate = True): - """ - Make a network request by calling QgsNetworkAccessManager. - redirections argument is ignored and is here only for httplib2 compatibility. - """ - self.msg_log(u'http_call request: {0}'.format(url)) - self.http_call_result = Response({ - 'status': 0, - 'status_code': 0, - 'status_message': '', - 'text' : '', - 'ok': False, - 'headers': {}, - 'reason': '', - 'exception': None, - }) - req = QNetworkRequest() - req.setAttribute(QNetworkRequest.CookieSaveControlAttribute, QNetworkRequest.Manual) - req.setAttribute(QNetworkRequest.CookieLoadControlAttribute, QNetworkRequest.Manual) - # Avoid double quoting form QUrl - if PYTHON_VERSION >= 3: - url = urllib.parse.unquote(url) - else: - url = urllib2.unquote(url) - req.setUrl(QUrl(url)) - - if self.cookie is not None: - if headers is not None: - headers['Cookie'] = self.cookie - else: - headers = {'Cookie':self.cookie} - - if self.basicauth is not None and authenticate: - if headers is not None: - headers['Authorization'] = self.basicauth - else: - headers = {'Authorization':self.basicauth} - - if headers is not None: - # This fixes a wierd error with compressed content not being correctly - # inflated. - # If you set the header on the QNetworkRequest you are basically telling - # QNetworkAccessManager "I know what I'm doing, please don't do any content - # encoding processing". - # See: https://bugs.webkit.org/show_bug.cgi?id=63696#c1 - try: - del headers['Accept-Encoding'] - except KeyError: - pass - for k, v in headers.items(): - if PYTHON_VERSION >= 3: - if isinstance(k, str): - k = k.encode('utf-8') - if isinstance(v, str): - v = v.encode('utf-8') - req.setRawHeader(k, v) - - if self.authid: - self.msg_log("Update request w/ authid: {0}".format(self.authid)) - QgsAuthManager.instance().updateNetworkRequest(req, self.authid) - if self.reply is not None and self.reply.isRunning(): - self.reply.close() - if method.lower() == 'delete': - func = getattr(QgsNetworkAccessManager.instance(), 'deleteResource') - else: - func = getattr(QgsNetworkAccessManager.instance(), method.lower()) - # Calling the server ... - # Let's log the whole call for debugging purposes: - self.msg_log("Sending %s request to %s" % (method.upper(), req.url().toString())) - headers = {str(h): str(req.rawHeader(h)) for h in req.rawHeaderList()} - for k, v in headers.items(): - self.msg_log("%s: %s" % (k, v)) - if method.lower() in ['post', 'put']: - if PYTHON_VERSION >= 3: - if isinstance(body, str): - body = body.encode('utf-8') - self.reply = func(req, body) - else: - self.reply = func(req) - if self.authid: - self.msg_log("Update reply w/ authid: {0}".format(self.authid)) - QgsAuthManager.instance().updateNetworkReply(self.reply, self.authid) - - self.reply.sslErrors.connect(self.sslErrors) - self.reply.finished.connect(self.replyFinished) - - # Call and block - self.el = QEventLoop() - self.reply.finished.connect(self.el.quit) - self.reply.downloadProgress.connect(self.downloadProgress) - - # Catch all exceptions (and clean up requests) - try: - self.el.exec_() - # Let's log the whole response for debugging purposes: - self.msg_log("Got response %s %s from %s" % \ - (self.http_call_result.status_code, - self.http_call_result.status_message, - self.reply.url().toString())) - headers = {str(h): str(self.reply.rawHeader(h)) for h in self.reply.rawHeaderList()} - for k, v in headers.items(): - self.msg_log("%s: %s" % (k, v)) - if len(self.http_call_result.text) < 1024: - self.msg_log("Payload :\n%s" % self.http_call_result.text) - else: - self.msg_log("Payload is > 1 KB ...") - except Exception as e: - raise e - finally: - if self.reply is not None: - if self.reply.isRunning(): - self.reply.close() - self.msg_log("Deleting reply ...") - self.reply.deleteLater() - self.reply = None - else: - self.msg_log("Reply was already deleted ...") - if not self.http_call_result.ok: - if self.http_call_result.exception and not self.exception_class: - raise self.http_call_result.exception - else: - raise self.exception_class(self.http_call_result.reason) - return (self.http_call_result, self.http_call_result.text) - - #@pyqtSlot() - def downloadProgress(self, bytesReceived, bytesTotal): - """Keep track of the download progress""" - #self.msg_log("downloadProgress %s of %s ..." % (bytesReceived, bytesTotal)) - pass - - #@pyqtSlot() - def replyFinished(self): - err = self.reply.error() - httpStatus = self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - httpStatusMessage = self.reply.attribute(QNetworkRequest.HttpReasonPhraseAttribute) - self.http_call_result.status_code = httpStatus - self.http_call_result.status = httpStatus - self.http_call_result.status_message = httpStatusMessage - for k, v in self.reply.rawHeaderPairs(): - self.http_call_result.headers[str(k)] = str(v) - self.http_call_result.headers[str(k).lower()] = str(v) - if err != QNetworkReply.NoError: - msg = "Network error #{0}: {1}".format( - self.reply.error(), self.reply.errorString()) - self.http_call_result.reason = msg - self.http_call_result.ok = False - self.msg_log(msg) - if err == QNetworkReply.TimeoutError: - self.http_call_result.exception = RequestsExceptionTimeout(msg) - elif err == QNetworkReply.ConnectionRefusedError: - self.http_call_result.exception = RequestsExceptionConnectionError(msg) - else: - self.http_call_result.exception = RequestsException(msg) - else: - # since Python 3 readAll() returns a PyQt5.QByteArray, we - # want only the data - if PYTHON_VERSION >= 3: - self.http_call_result.text = self.reply.readAll().data().decode('utf-8') - else: - self.http_call_result.text = str(self.reply.readAll()) - self.http_call_result.ok = True - self.reply.deleteLater() - - #@pyqtSlot() - def sslErrors(self, reply, ssl_errors): - """ - Handle SSL errors, logging them if debug is on and ignoring them - if disable_ssl_certificate_validation is set. - """ - if ssl_errors: - for v in ssl_errors: - self.msg_log("SSL Error: %s" % v) - if self.disable_ssl_certificate_validation: - reply.ignoreSslErrors() diff --git a/src/shoguneditor/connection/shogunressource.py b/src/shoguneditor/connection/shogunressource.py deleted file mode 100644 index 76eae2b..0000000 --- a/src/shoguneditor/connection/shogunressource.py +++ /dev/null @@ -1,516 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys -from base64 import b64encode -import urllib -import json -import os -import webbrowser - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtGui import QIcon - from qgis.PyQt.QtXml import QDomDocument - from qgis.PyQt.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart - from qgis.PyQt.QtCore import QFile, QIODevice, QSize -else: - from PyQt4.QtGui import QIcon - from PyQt4.QtXml import QDomDocument - from PyQt4.QtNetwork import QNetworkRequest, QHttpMultiPart, QHttpPart - from PyQt4.QtCore import QFile, QIODevice, QSize - -from qgis.core import QgsApplication -from qgis.gui import QgsMessageBar - -from .networkaccessmanager import NetworkAccessManager, RequestsExceptionConnectionError, RequestsException -from shoguneditor.layerutils import createAndParseSld - - -PYTHON_VERSION = sys.version_info[0] - -class ShogunRessource: - ''' This class controls all interactions between QGIS and the Shogun ressource, - apart from creating wfs/wms layers (see layerutils for this). It makes use of the - class NetworkAccessManager, which calls QgsNetworkAccessManager for all http - requests''' - - def __init__(self, iface, url, name, user = None, pw = None): - self.iface = iface - if url.endswith('webapp'): - url+='/' - elif url.endswith('rest/'): - url = url[:-5] - elif url.endswith('rest'): - url = url[:-4] - - self.baseurl = url #url should have the format: 'https:.../shogun2-webapp/' - self.name = name - self.applications = [] - self.layers = [] - self.mapconfigs = [] - self.extents = [] - self.http = NetworkAccessManager(debug = False) - - self.icondir = os.path.join(os.path.dirname(__file__), '..', 'images', 'custom-symbols') - if not os.path.isdir(self.icondir): - os.makedirs(self.icondir) - - - if user is not None and pw is not None: - if PYTHON_VERSION >= 3: - bytes = (user + ':' + pw).encode('utf-8') - self.basicauth = b64encode(bytes) - self.http.setBasicauth('Basic '.encode('utf-8') + self.basicauth) - else: - self.basicauth = b64encode(user + ':' + pw) - self.http.setBasicauth('Basic ' + self.basicauth) - - - def checkConnection(self): - # returning a tuple of ('true/false', 'message') - - # # TODO: here we check only the cases of http/https faults in the user's - # input. Maybe there should be more error checks for in case the user has - # written an url which is completely different (no http/ https at beginning) - - testurl = self.baseurl + 'rest/applications' - try: - testresponse = self.http.request(testurl, method = 'HEAD') - if testresponse[0]['status'] > 199 and testresponse[0]['status'] < 210: - return (True, '') - except RequestsException as e: - if self.baseurl.startswith('https'): - # if the first try with 'https' was not successfull try http: - testurl = 'http' + testurl[5:] - try: - testresponse = self.http.request(testurl, method = 'HEAD') - if testresponse[0]['status'] > 199 and testresponse[0]['status'] < 210: - self.baseurl = 'http' + self.baseurl[5:] - return (True, '') - except RequestsException as e: - pass - return (False, 'Error : Failed to connect to the server') - - - - def userInfo(self, status, objectName, method): - #method = 'created'/'updated'/'deleted' - if status > 199 and status < 300: - msg = objectName + ' was successfully ' + method - self.iface.messageBar().pushSuccess('Shogun Editor Info:', msg) - else: - msg = 'Error: ' + objectName + ' could not be ' + method - self.iface.messageBar().pushCritical('Shogun Editor Info:', msg) - - - def editApplication(self, id, data): - data = json.dumps(data) - url = self.baseurl + 'rest/applications/' +str(id) - header = {'Content-type':'application/json'} - response = self.http.request(url, method='PUT', body = data, headers = header) - self.userInfo(response[0]['status'], 'Application', 'edited') - return response[0]['status'] - - - def editLayer(self, id, data): - data = json.dumps(data) - url = self.baseurl + 'rest/layers/' +str(id) - header = {'Content-type':'application/json'} - response = self.http.request(url, method='PUT', body = data, headers = header) - self.userInfo(response[0]['status'], 'Layer', 'edited') - return response[0]['status'] - - - def uploadNewApplication(self, data): - #data as a json-like dict - url = self.baseurl + 'projectapps/create.action' - header = {'Content-type' : 'application/json'} - body = json.dumps(data) - response = self.http.request(url, method='POST', body = body, headers = header) - name = 'Application ' + data['name'] - self.userInfo(response[0]['status'], name, 'created') - if response[0]['status'] == 200 or response[0]['status'] == 201: - return True - else: - return False - - - def editMapConfig(self, id, data): - edit = None - for mapconfig in self.mapconfigs: - if mapconfig['id'] == id: - edit = mapconfig - - url = self.baseurl+'mapconfigs/'+str(id) - body = '{"id":'+str(id)+',"center":{"x":'+str(data['center']['x'])+',"y":' - body += str(data['center']['y'])+'},"zoom":'+str(data['zoom'])+'}' - h = {'Content-type':'application/json'} - response = self.http.request(url, method='PUT', body = body, headers = h) - self.userInfo(response[0]['status'], 'Homeview', 'updated') - - def editObjectPermission(self, id, objectType, permissionType, data): - url = self.baseurl + 'rest/entitypermission/' + objectType + '/' + str(id) - if permissionType == 'User': - url += '/ProjectUser' - elif permissionType == 'UserGroup': - url += '/ProjectUserGroup' - else: - return - - header = {'Content-type' : 'application/json'} - body = json.dumps(data) - response = self.http.request(url, method='POST', body = body, headers = header) - if response[0]['status'] == 200 or response[0]['status'] == 201: - return True - else: - return False - - def updateData(self): - try: - self.updateApplications() - self.updateLayers() - self.updateExtentsAndMapConfigs() - return True - except RequestsExceptionConnectionError: - self.iface.messageBar().pushCritical('Connection Error:', - 'Could not connect to given SHOGUN host application - Please review url') - return False - - def updateApplications(self): - url = self.baseurl + 'rest/applications' - response = self.http.request(url) - self.applications = json.loads(response[1]) - - def updateLayers(self): - url = self.baseurl + 'rest/layers' - response = self.http.request(url) - self.layers = json.loads(response[1]) - - def updateSingleApplication(self, id): - url = self.baseurl + 'rest/applications/' + str(id) - response = self.http.request(url) - updatedApplication = json.loads(response[1]) - for app in enumerate(self.applications): - if app[1]['id'] == id: - self.layers[app[0]] = updatedApplication - return updatedApplication - - def updateSingleLayer(self, id): - url = self.baseurl + 'rest/layers/' + str(id) - response = self.http.request(url) - updatedLayer = json.loads(response[1]) - for layer in enumerate(self.layers): - if layer[1]['id'] == id: - self.layers[layer[0]] = updatedLayer - return updatedLayer - - #one method for retrieving user and groups permissions (permissionType) - #for layers or applications (objectType) - def getObjectPermissions(self, id, objectType, permissionType): - url = self.baseurl + 'rest/entitypermission/Project' + objectType - # objectType = 'Application' or 'Layer' - # permissionType = 'User' or 'UserGroup' - url += '/'+ str(id) + '/Project' + permissionType + '?' - response = self.http.request(url) - return json.loads(response[1]) - - - def updateExtentsAndMapConfigs(self): - url = self.baseurl + 'rest/extents' - response = self.http.request(url) - self.extents = json.loads(response[1]) - url = self.baseurl + 'rest/mapconfigs' - response = self.http.request(url) - self.mapconfigs = json.loads(response[1]) - - def getHomeviewByIds(self, mapconfigid, extentid): - homeview = {} - for mapcf in self.mapconfigs: - if mapcf['id'] == mapconfigid: - homeview['mapconfig']=mapcf - for ext in self.extents: - if ext['id'] == extentid: - homeview['extent']=ext - return homeview - - def getApplicationIdsAndNames(self, reload = False): - if reload: - self.updateApplications() - return [(x['id'], x['name']) for x in self.applications] - - def getLayerIdsAndNames(self, reload = False): - if reload: - self.updateLayers() - return [(x['id'], x['name'], x['dataType'], x['source']) for x in self.layers] - - def getApplicationAttrsById(self, id): - for x in self.applications: - if x['id'] == id: - return x - - def getLayerAttrsById(self, id): - for x in self.layers: - if x['id'] == id: - return x - - def getGroupNames(self): - return [x['name'] for x in self.groups] - - def getUserNames(self): - return [(x['lastName']+', '+x['firstName']) for x in self.users] - - - def downloadStyle(self, qgisLayerItem): - #this downloads the layer's style, saves it as an sld and returns two - #things: 1. the path to the .sld, 2. the specific name of the style - #as it is saved in the background geoserver from shogun, obtained from - #the xml-node sld:UserStyle - sld:Name - #the specific name is later needed for re-uploading the edited style - - - ## TODO: - # maybe a better implementation would be to pass the QDomDocument directly - # to the layer and set it's style from sld... - - # unfortunately for a not known reason this does not work and we first - # have to save the sld to a file, then do layer.loadSldStyle(file) - # can someone fix it?# - - shogunlayer = qgisLayerItem.parentShogunLayer - url = shogunlayer.source['url'] - if url.startswith('/shogun2-webapp'): - url = self.baseurl.rstrip('/shogun2-webapp/rest/') + url - url += '?service=WMS&request=GetStyles&version=1.1.1&layers=' - url += shogunlayer.source['layerNames'] - response = self.http.request(url, authenticate = False) - - mydoc = QDomDocument() - mydoc.setContent(response[1]) - root = mydoc.firstChildElement('sld:StyledLayerDescriptor') - namedLayerNode = root.firstChildElement('sld:NamedLayer') - userStyleNode = namedLayerNode.firstChildElement('sld:UserStyle') - sldNameNode = userStyleNode.firstChildElement('sld:Name') - geoServerStyleName = sldNameNode.text() - - # # TODO: implement more than point style - # check if custom icons are used in the style: - - # # NOTE: this is the beginning of a larger implementation of - # exchange of icons - # problem is that shogun2 only serves png icons, and qgis needs - # svg to turn them into a style - if '' in response[1]: - self.iface.messageBar().pushInfo('Info', - 'The downloaded style for the current layer contains custom icons ' - 'from SHOGUN, which only serves them as PNG pictures, but QGIS ' - 'can only read SVG. Until this is fixed, you see a default QGIS ' - 'style for the layer') - - ''' - featureTypeStyleNode = userStyleNode.firstChildElement('sld:FeatureTypeStyle') - rules = featureTypeStyleNode.elementsByTagName('sld:Rule') - for x in range(rules.length()): - rule = rules.at(x).toElement() - listOfGraphics = rule.elementsByTagName('sld:OnlineResource') - for x in range(listOfGraphics.length()): - graphicNode = listOfGraphics(x) - attributes = graphicNode.attributes() - url = attributes.namedItem('xlink:href').nodeValue() - id = url.split('getThumbnail.action?id=')[1] - iconPath = self.downloadIconThumbnail(id) - ''' - - dirpath = os.path.dirname(__file__) - filename = os.path.join(dirpath, 'latest-symbology.sld') - with open(filename, 'w') as file: - file.write(response[1]) - return filename, geoServerStyleName - - def uploadStyle(self, qgisLayerItem): - url = self.baseurl.rstrip('rest/') + '/sld/update.action' - h = {'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'} - - sld = createAndParseSld(qgisLayerItem) - b = 'sld=' + sld + '&sldName=' + qgisLayerItem.stylename + '&layerId=' - b += str(qgisLayerItem.parentShogunLayer.id) - response = self.http.request(url, method = 'POST', body = b, headers = h) - if json.loads(response[1])['success']: - return True - else: - return False - - def prepareIconForUpload(self, svgIconName): - svgPaths = QgsApplication.svgPaths() - svgIconPath = None - for dir in svgPaths: - if os.path.isfile(os.path.join(dir, svgIconName)): - svgIconPath = os.path.join(dir, svgIconName) - if svgIconPath is None or not svgIconPath.endswith('svg'): - return False - name = os.path.splitext(svgIconName)[0] - outputPath = os.path.join(self.icondir, 'qgis-' + name +'.png') - img = QIcon(svgIconPath).pixmap(QSize(100,100)).toImage() - if img.save(outputPath): - newId = self.uploadImage(outputPath) - if newId: - iconUrl = self.baseurl + 'projectimage/getThumbnail.action?id=' - iconUrl += str(newId) - return iconUrl - self.userInfo(400, 'Custom symbology icon', 'uploaded') - return False - - - # # NOTE: the following method is still not used: - def downloadIconThumbnail(self, id): - iconPath = os.path.join(self.icondir, str(icon['id']) + '.png') - if os.path.isfile(iconPath): - return iconPath - else: - url = self.baseurl + 'projectimage/getThumbnail.action?id=' + str(id) - urlretrieve(url, iconPath) - return iconPath - - - def uploadImage(self, pathToImage): - img = QFile(pathToImage) - img.open(QIODevice.ReadOnly) - imgPart = QHttpPart() - txt = 'form-data; name="file"; filename="' + os.path.basename(pathToImage) + '"' - imgPart.setHeader(QNetworkRequest.ContentDispositionHeader, txt) - imgPart.setHeader(QNetworkRequest.ContentTypeHeader, 'image/zip') - imgPart.setBodyDevice(img) - - multiPart = QHttpMultiPart(QHttpMultiPart.FormDataType) - multiPart.append(imgPart) - - url = self.baseurl + 'projectimage/upload.action?' - - response = self.http.request(url, method = 'POST', body = multiPart) - if response[0]['status'] > 199 and response[0]['status'] < 210: - # if icon upload was successfull, server returns id of the new icon - # in it's database - id = json.loads(response[1])['data']['id'] - return id - else: - return False - - - - def publishWmsLayer(self, wmsUri): - url = self.baseurl + '/ogcservicehelper/publishlayer.action?' - url += wmsUri - #header = {'Cookie': self.cookie} - response = self.http.request(url, method = 'GET') - self.userInfo(response[0]['status'], 'New WMS layer', 'published') - - - def uploadLayer(self, pathToZipFile, dataType): - url = self.baseurl + '/import/create-layer.action' - - # the following creates a QHttpMultiPart with 2 parts, one defining the - # dataType (i.e. Vector or Raster) and one with the binary file data itself - - textpart = QHttpPart() - textpart.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="dataType"') - if PYTHON_VERSION >= 3: - textpart.setBody(dataType.encode('utf-8')) - else: - textpart.setBody(dataType) - - file = QFile(pathToZipFile) - file.open(QIODevice.ReadOnly) - lyr = 'form-data; name="file"; filename="' + os.path.basename(pathToZipFile) + '"' - layerpart = QHttpPart() - layerpart.setHeader(QNetworkRequest.ContentTypeHeader, 'application/zip') - layerpart.setHeader(QNetworkRequest.ContentDispositionHeader, lyr) - layerpart.setBodyDevice(file) - - multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) - multipart.append(textpart) - multipart.append(layerpart) - - response = self.http.request(url, method = 'POST', body = multipart) - res = json.loads(response[1]) - if res['success']: - self.userInfo(response[0]['status'], 'New Vector Layer', 'uploaded') - else: - if res['error'] == 'NO_CRS': - self.requestCrsUpdateOnLayer(res['importJobId']) - else: - self.userInfo(response[0]['status'], 'New Vector Layer', 'uploaded') - return response[0]['status'] - - def requestCrsUpdateOnLayer(self, importJobId): - url = self.baseurl + '/import/update-crs-for-import.action' - data = 'importJobId=' + str(importJobId) + '&taskId=0&fileProjection=EPSG%3A3857&layerName=&dataType=Vector' - h = {'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'} - response = self.http.request(url, method = 'POST', body = data, headers = h) - - def getFieldNamesFromWfs(self, layerRessourceName): - url = self.baseurl + 'geoserver-noauth.action?service=WFS&request=DescribeFeatureType&typeName=' - url += layerRessourceName + '&outputFormat=application/json' - response = self.http.request(url) - if response[0]['status'] == 200 or response[0]['status'] == 201: - fieldNames = [] - answer = json.loads(response[1]) - if answer['featureTypes'][0]['properties']: - for prop in answer['featureTypes'][0]['properties']: - fieldNames.append(prop['name']) - if len(fieldNames) > 0: - return fieldNames - return False - - - def deleteLayer(self, id): - url = self.baseurl + 'rest/projectlayers/' + str(id) - response = self.http.request(url, method = 'DELETE') - self.userInfo(response[0]['status'], 'Layer', 'deleted') - - - def deleteApplication(self, id): - url = self.baseurl + 'rest/projectapps/' + str(id) - response = self.http.request(url, method = 'DELETE') - self.userInfo(response[0]['status'], 'Application', 'deleted') - - - def copyApplication(self, id, applicationName): - url = self.baseurl + 'projectapps/copy.action' - data = 'appId=' + str(id) + '&appName=' + applicationName + '-Copy' - h = {'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'} - response = self.http.request(url, method = 'POST', body = data, headers = h) - self.userInfo(response[0]['status'], 'Application', 'copied') - - - def viewApplicationOnline(self, id): - url = self.baseurl + 'client/?id=' + str(id) - webbrowser.open(url) - - - def createLayerTreeItem(self, data): - url = self.baseurl + 'rest/layertree' - h = {'Content-type':'application/json'} - body = json.dumps(data) - response = self.http.request(url, method = 'POST', body = body, headers = h) - return response[0]['status'] - - - def updateLayerTreeItem(self, layerTreeItemId, data): - url = self.baseurl + 'rest/layertree/' + str(layerTreeItemId) - h = {'Content-type':'application/json'} - body = json.dumps(data) - response = self.http.request(url, method = 'PUT', body = body, headers = h) - return response[0]['status'] - - - def deleteLayerTreeItem(self, layerTreeItemIdd): - url = self.baseurl + 'rest/layertree/' + str(layerTreeItemIdd) - h = {'Content-type':'application/json'} - body = json.dumps({'id' : layerTreeItemIdd}) - response = self.http.request(url, method = 'DELETE', body = body, headers = h) - return response[0]['status'] diff --git a/src/shoguneditor/gui/__init__.py b/src/shoguneditor/gui/__init__.py deleted file mode 100644 index 4e36e95..0000000 --- a/src/shoguneditor/gui/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' diff --git a/src/shoguneditor/gui/dialog_bases/__init__.py b/src/shoguneditor/gui/dialog_bases/__init__.py deleted file mode 100644 index 4e36e95..0000000 --- a/src/shoguneditor/gui/dialog_bases/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' diff --git a/src/shoguneditor/gui/dialog_bases/addraster.py b/src/shoguneditor/gui/dialog_bases/addraster.py deleted file mode 100644 index 8188639..0000000 --- a/src/shoguneditor/gui/dialog_bases/addraster.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtWidgets import QDialog, QPushButton, QLabel - from qgis.PyQt import QtCore -else: - from PyQt4.QtGui import QDialog, QPushButton, QLabel - from PyQt4 import QtCore - -class AddRasterDialog(QDialog): - def __init__(self): - super(QDialog, self).__init__() - self.resize(410, 200) - self.setWindowTitle('Add Raster Layer Dialog') - self.label = QLabel(self) - self.label.setGeometry(QtCore.QRect(20,20,300,100)) - self.label.setAlignment(QtCore.Qt.AlignCenter) - self.label.setText('The requested ressource is a raster layer. \n' - 'Depending on it\'s size\', downloading \nand importing it to QGIS' - 'may \ntake while. You can also consider to only import the\n' - 'layer as a WMS if you only need to view it') - self.cancelbutton = QPushButton(self) - self.cancelbutton.setText('Cancel') - self.cancelbutton.setGeometry(QtCore.QRect(20,150,125,30)) - self.wmsbutton = QPushButton(self) - self.wmsbutton.setText('Add as WMS Layer') - self.wmsbutton.setGeometry(QtCore.QRect(150,150,125,30)) - self.rasterbutton = QPushButton(self) - self.rasterbutton.setText('Add as Raster Layer') - self.rasterbutton.setGeometry(QtCore.QRect(280,150,125,30)) - - # we reconfigure the already existing slot self.done() (inherited - # from QDialog to emit different ints - self.done() is called by - # exec_()) - self.cancelbutton.clicked.connect(lambda: self.done(0)) - self.wmsbutton.clicked.connect(lambda: self.done(1)) - self.rasterbutton.clicked.connect(lambda: self.done(2)) diff --git a/src/shoguneditor/gui/dialog_bases/applicationSettings.py b/src/shoguneditor/gui/dialog_bases/applicationSettings.py deleted file mode 100644 index 3e2ce46..0000000 --- a/src/shoguneditor/gui/dialog_bases/applicationSettings.py +++ /dev/null @@ -1,616 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtCore import QRect, Qt - # we are faking the old way of QtGui, not the best style, but makes it easier - # for switching betweeng version 2 and 3 - from qgis.PyQt import QtWidgets as QtGui - from qgis.PyQt.QtGui import QFont -else: - from PyQt4.QtCore import QRect, Qt - from PyQt4 import QtGui - from PyQt4.QtGui import QFont - -from qgis.gui import QgsExtentGroupBox - -class LayerListItem(QtGui.QListWidgetItem): - def __init__(self, text, layerId): - super(LayerListItem, self).__init__(text) - self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsDragEnabled | - Qt.ItemIsSelectable) - self.layerId = layerId - -class LayerListWidget(QtGui.QListWidget): - def __init__(self, parent): - super(LayerListWidget, self).__init__(parent) - self.setDragEnabled(True) - self.setDragDropMode(QtGui.QAbstractItemView.DragDrop) - - def populateList(self, layers): - for layer in layers: - item = LayerListItem(text = layer[1], layerId = layer[0]) - self.addItem(item) - - def dragEnterEvent(self, e): - item = self.itemAt(e.pos()) - if item is not None: - # we need to pass the name of the layer and it's id to the mimeData - # for drag and drop. For reasons of simplicity we just add the - # layerId and name to one string which we pass and decode it later - # we use '&;*&' so this code must not appear in layer names - mimeText = item.text() + '&;*&' + str(item.layerId) - e.mimeData().setText(mimeText) - - -class LayerTreeItem(QtGui.QTreeWidgetItem): - def __init__(self, parent): - super(LayerTreeItem, self).__init__(parent) - self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | - Qt.ItemIsDragEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | - Qt.ItemIsDropEnabled) - self.savedAttributes = {} - self.newAttributes = {} - self.layerId = None - self.id = None - - - def setSavedAttributes(self, savedAttributes): - self.savedAttributes = savedAttributes - self.setText(0, self.savedAttributes['text']) - self.role = self.savedAttributes['@class'] - self.id = self.savedAttributes['id'] - if self.savedAttributes['root']: - return - if self.savedAttributes['checked'] == True : - self.setCheckState(0,Qt.Checked) - else: - self.setCheckState(0,Qt.Unchecked) - - - def updateNewAttributes(self): - self.newAttributes['text'] = self.text(0) - self.newAttributes['root'] = False - self.newAttributes['@class'] = self.role - - if self.checkState(0) == Qt.Checked: - self.newAttributes['checked'] = True - else: - self.newAttributes['checked'] = False - - if self.parent() is None: - self.newAttributes['parentId'] = self.treeWidget().rootId - self.newAttributes['index'] = self.treeWidget().indexOfTopLevelItem(self) - else: - self.newAttributes['parentId'] = self.parent().id - self.newAttributes['index'] = self.parent().indexOfChild(self) - - if self.role == 'de.terrestris.appshogun.model.tree.LayerTreeLeaf': - self.newAttributes['expandable'] = False - self.newAttributes['expanded'] = False - self.newAttributes['leaf'] = True - else: - self.newAttributes['expandable'] = True - self.newAttributes['expanded'] = True - self.newAttributes['leaf'] = False - - if self.layerId is not None: - self.newAttributes['layer'] = self.layerId - #self.newAttributes['expanded'] = self.isExpanded() - - - def getItemChange(self): - if len(self.savedAttributes) == 0: - return self.newAttributes - else: - change = {} - for (key, value) in self.savedAttributes.items(): - if key in self.newAttributes: - if value != self.newAttributes[key]: - change[key] = self.newAttributes[key] - if len(change) == 0: - return None - else: - change['id'] = self.id - return change - - -class LayerTreeWidget(QtGui.QTreeWidget): - SHOGUN_TREE_LEAF = 'de.terrestris.appshogun.model.tree.LayerTreeLeaf' - SHOGUN_TREE_FOLDER = 'de.terrestris.appshogun.model.tree.LayerTreeFolder' - - def __init__(self, parentWindow): - super(LayerTreeWidget, self).__init__(parentWindow) - self.setHeaderHidden(True) - self.setColumnCount(1) - self.setDragEnabled(True) - self.setAcceptDrops(True) - self.setDragDropMode(QtGui.QAbstractItemView.DragDrop) - self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.on_context_menu) - self.deletedItemIds = [] - - def setupNewTree(self): - folder = self.addNewFolder(None) - folder.setText(0, 'Background layer') - - - def populateTree(self, layerTree): - # delete all items, then populate the tree with items representing - # the layer tree structure - self.clear() - self.rootId = layerTree['id'] - self.constructTreeChildrenRecursive(self.invisibleRootItem(), layerTree['children']) - - iter = QtGui.QTreeWidgetItemIterator(self) - val = iter.value() - while val: - val.setExpanded(True) - iter += 1 - val = iter.value() - - def constructTreeChildrenRecursive(self, parent, children): - for child in children: - item = LayerTreeItem(parent) - attrs = {key : value for (key, value) in child.items() if key != 'children'} - item.setSavedAttributes(attrs) - if 'children' in child.keys(): - if not child['children']: - return - # 'children' is a list, to make sure the items are put into the - # tree in the right order according to there index, we sort them - sortedChildren = sorted(child['children'], key = lambda x : x['index']) - self.constructTreeChildrenRecursive(item, sortedChildren) - - - def getLayerTreeChanges(self): - allChanges = { - 'newItems' : [], - 'changeItems' : [], - 'deleteItems' : [] - } - # iterate through all items in the layertree and find new or - # changed items - iter = QtGui.QTreeWidgetItemIterator(self) - treeitem = iter.value() - while treeitem: - treeitem.updateNewAttributes() - change = treeitem.getItemChange() - if change is not None: - if not 'id' in change: - allChanges['newItems'].append(change) - else: - allChanges['changeItems'].append(change) - - iter += 1 - treeitem = iter.value() - - if len(self.deletedItemIds) > 0: - allChanges['deleteItems'] = [x for x in self.deletedItemIds] - self.deletedItemIds = [] - - for x in allChanges: - if len(allChanges[x]) > 0: - return allChanges - return None - - - def dropEvent(self, e): - dropItem = self.itemAt(e.pos()) - mime = e.mimeData() - if dropItem is None: - if mime.hasText(): - newItem = LayerTreeItem(parent = self) - layerName, layerId = mime.text().split('&;*&') - newItem.setText(0, layerName) - newItem.role = self.SHOGUN_TREE_LEAF - newItem.layerId = int(layerId) - newItem.setCheckState(0,Qt.Checked) - else: - self.changePositionInTree(self.invisibleRootItem()) - else: - # if dropItem is a TreeLeaf it represents a layer so it cannot get a - # child, so insert the dragged item in the parent folder - if dropItem.role == self.SHOGUN_TREE_LEAF: - if dropItem.parent() is not None: - dropItem = dropItem.parent() - else: - dropItem = self.invisibleRootItem() - - # if mime has Text its coming from the layerlistwidget - if mime.hasText(): - newItem = LayerTreeItem(parent = dropItem) - layerName, layerId = mime.text().split('&;*&') - newItem.setText(0, layerName) - newItem.role = self.SHOGUN_TREE_LEAF - newItem.layerId = int(layerId) - newItem.setCheckState(0,Qt.Checked) - else: - self.changePositionInTree(dropItem) - iter = QtGui.QTreeWidgetItemIterator(self) - val = iter.value() - while val: - val.setSelected(False) - iter += 1 - val = iter.value() - - - def changePositionInTree(self, newParentItem): - selectedItem = self.selectedItems()[0] - oldParentItem = selectedItem.parent() - # cannot insert a folder to itself - cursor = newParentItem - while cursor is not None: - if selectedItem == cursor: - return - cursor = cursor.parent() - - if oldParentItem is None: - self.invisibleRootItem().removeChild(selectedItem) - else: - oldParentItem.removeChild(selectedItem) - newParentItem.addChild(selectedItem) - newParentItem.setExpanded(True) - selectedItem.setExpanded(True) - - - def on_context_menu(self, point): - item = self.itemAt(point) - acts = [] - if item is None: - a1 = QtGui.QAction('Add Folder (top level)', None) - a1.triggered.connect(lambda: self.addNewFolder(None)) - acts.append(a1) - a2 = QtGui.QAction('Delete Tree Contents completely', None) - a2.triggered.connect(self.deleteAll) - acts.append(a2) - else: - a1 = QtGui.QAction('Rename', None) - a1.triggered.connect(lambda: self.renameItem(item)) - acts.append(a1) - if item.role == self.SHOGUN_TREE_LEAF: - a2 = QtGui.QAction('Delete Leaf', None) - a2.triggered.connect(lambda: self.deleteLeaf(item)) - acts.append(a2) - else: - a2 = QtGui.QAction('New Folder (inside selected)', None) - a2.triggered.connect(lambda: self.addNewFolder(item)) - acts.append(a2) - a3 = QtGui.QAction('Delete Folder', None) - a3.triggered.connect(lambda: self.deleteLeaf(item)) - acts.append(a3) - menu = QtGui.QMenu() - menu.addActions(acts) - point = self.mapToGlobal(point) - menu.exec_(point) - - - def addNewFolder(self, item): - if item is None: - parent = self - else: - parent = item - new = LayerTreeItem(parent) - new.setText(0, 'New folder') - new.role = self.SHOGUN_TREE_FOLDER - new.setCheckState(0, Qt.Checked) - return new - - def deleteAll(self): - topitem = self.invisibleRootItem() - for index in range(topitem.childCount()): - child = topitem.child(index) - topitem.removeChild(child) - - def renameItem(self, item): - text, ok = QtGui.QInputDialog.getText(self, - 'Text Input Dialog', 'Enter the new name:') - if ok: - item.setText(0, text) - - def getSubtreeIds(self, item): - # as there is no option in QT to iterate only a subtree, we had to write - # a recursive iteration by ourselves to get all id's that are about to - # be deleted - idList = [] - if item.id is not None: - idList.append(item.id) - if item.childCount() > 0: - for x in range(item.childCount()): - idList.extend(getSubtreeIds(item.child(x))) - return idList - - - def deleteLeaf(self, item): - self.deletedItemIds.extend(self.getSubtreeIds(item)) - - parent = item.parent() - if parent is None: - parent = self.invisibleRootItem() - parent.removeChild(item) - - - -class ApplicationSettingsDialog(QtGui.QDialog): - def __init__(self): - QtGui.QDialog.__init__(self) - self.tabs = [] #All child-tabWidgets - self.tabedits = [] #All QLineEdits per tabWidget in a list - self.tabboxes = [] #All QCheckBoxes per tabWidget in a list - self.moreObjects = [] - self.setupUi() - - def setupUi(self): - self.resize(550, 550) - self.setWindowTitle('Settings') - - #create tabWidget that holds the tabs - self.tabWidget = QtGui.QTabWidget(self) - self.tabWidget.setGeometry(QRect(10, 20, 500, 480)) - self.tabWidget.setObjectName('tabWidget') - tab0labels=[['Name',(50, 50, 56, 17)], ['Description', (50,100,70,25)], ['Language', (50, 150, 56, 17)]] - tab1labels = [['Which tools/ buttons shall be activated in the application:', (50, 25, 56, 17)]] - tab2labels = [['Center:', (50, 50, 70, 17)], ['X:', (160, 53, 10, 10)], ['Y:', (320, 53, 10, 10)], ['Zoom:', (50, 100, 70, 17)], - ['Extent:', (50, 363, 70, 17)], ['MinX:', (132, 367, 40, 17)], ['MinY:', (222, 327, 40, 17)], ['MaxX:', (306, 367, 40, 17)], - ['MaxY:', (222, 407, 40, 17)]] - tab3labels = [['All Layers', (90, 40, 80, 30)], ['Layer Tree', (325, 40, 80, 30)]] - tab4labels = [['Users', (100, 10, 50, 20)], ['Groups', (320, 10, 50, 20)]] - tabwidgets = [['General', tab0labels], ['Tools', tab1labels], ['Homeview', tab2labels], ['Layer', tab3labels], ['Permissions', tab4labels]] - - #first set the labes for all tabwwidgets in a loop: - for tab in tabwidgets: - t = QtGui.QWidget() - t.setObjectName(tab[0]) - self.tabs.append(t) - self.tabWidget.addTab(t, tab[0]) - - for label in tab[1]: - l = QtGui.QLabel(t) - l.setText(label[0]) - l.setGeometry(QRect(label[1][0],label[1][1],label[1][2],label[1][3])) - if (tab[0] == 'Layer'): - font = QFont('Arial',12) - font.setBold(True) - l.setFont(font) - - - self.tabWidget.setCurrentIndex(0) - - #then populate the specific tabwidgets with other QObjects: - #tab 0 = 'General': - self.nameEdit = QtGui.QLineEdit(self.tabs[0]) - self.nameEdit.setGeometry(QRect(250, 40, 150, 27)) - self.tabedits.append(self.nameEdit) - - self.descriptionEdit = QtGui.QLineEdit(self.tabs[0]) - self.descriptionEdit.setGeometry(QRect(250, 90, 150,27)) - self.tabedits.append(self.descriptionEdit) - - self.languageBox = QtGui.QComboBox(self.tabs[0]) - self.languageBox.setGeometry(QRect(250, 140, 113,27)) - self.languageBox.addItems(['en','de']) - self.tabedits.append(self.languageBox) - - self.boxPublic = QtGui.QCheckBox(self.tabs[0]) - self.boxPublic.setGeometry(QRect(250, 180, 80, 17)) - self.boxPublic.setText('Public') - self.tabboxes.append(self.boxPublic) - - self.boxActive = QtGui.QCheckBox(self.tabs[0]) - self.boxActive.setGeometry(QRect(250, 230, 80, 17)) - self.boxActive.setText('Active') - self.tabboxes.append(self.boxActive) - - - #tab 1 = 'Tools': - toollist = ['Zoom in button', 'Zoom out button', 'Zoom to extent button', 'Step back to previous extent button', - 'Step forward to next extent button', 'Activate hover-select tool', 'Print button', 'Show measure tools button', - 'Show redlining tools button', 'Show workstate tools button', 'Show addwms tools button', 'Show meta toolbar button'] - y = 50 - self.tools = {} - # a dictonary with toolbutton id as key and reference to the QCheckBox - # as value, i.e.: {58: -Reference to QCheckBox Object-} - tcount = 57 - for tool in toollist: - t = QtGui.QCheckBox(self.tabs[1]) - t.setGeometry(QRect(60, y, 180, 17)) - t.setText(tool) - self.tools[tcount] = t - y += 30 - tcount += 1 - - - #tab 2 = 'Homeview': - self.homeviewCenterEditX = QtGui.QLineEdit(self.tabs[2]) - self.homeviewCenterEditX.setGeometry(QRect(170, 50, 125, 25)) - self.tabedits.append(self.homeviewCenterEditX) - self.homeviewCenterEditY = QtGui.QLineEdit(self.tabs[2]) - self.homeviewCenterEditY.setGeometry(QRect(330, 50, 125, 25)) - self.tabedits.append(self.homeviewCenterEditY) - - self.homeviewZoomBox = QtGui.QSpinBox(self.tabs[2]) - self.homeviewZoomBox.setGeometry(QRect(170, 100, 40, 25)) - self.moreObjects.append(self.homeviewZoomBox) - - self.extentEdits = [] - minX = QtGui.QLineEdit(self.tabs[2]) - minX.setGeometry(175, 360, 120, 25) - self.extentEdits.append(minX) - - minY = QtGui.QLineEdit(self.tabs[2]) - minY.setGeometry(265, 320, 120, 25) - self.extentEdits.append(minY) - - maxX = QtGui.QLineEdit(self.tabs[2]) - maxX.setGeometry(350, 360, 120, 25) - self.extentEdits.append(maxX) - - maxY = QtGui.QLineEdit(self.tabs[2]) - maxY.setGeometry(265, 400, 120, 25) - self.extentEdits.append(maxY) - - style = 'QLineEdit { background-color : #a6a6a6; color : white; }' - for edit in self.extentEdits: - edit.setReadOnly(True) - edit.lower() - edit.setStyleSheet(style) - - self.origExtentButton = QtGui.QPushButton(self.tabs[2]) - self.origExtentButton.setGeometry(100, 150, 190, 30) - self.origExtentButton.setText('Set original homview') - self.moreObjects.append(self.origExtentButton) - - self.qgsExtentButton = QtGui.QPushButton(self.tabs[2]) - self.qgsExtentButton.setGeometry(290, 150, 190, 30) - self.qgsExtentButton.setText('Set current QGIS view') - self.moreObjects.append(self.qgsExtentButton) - - self.homeviewEpsgWarning = QtGui.QLabel(self.tabs[2]) - self.homeviewEpsgWarning.setGeometry(QRect(50, 220, 435, 80)) - self.homeviewEpsgWarning.setFont(QFont('Arial', 9)) - - self.jumpButtonOrig = QtGui.QPushButton(self.tabs[2]) - self.jumpButtonOrig.setGeometry(QRect(115, 192, 160, 20)) - self.jumpButtonOrig.setText('Jump to original homeview') - self.jumpButtonOrig.setStyleSheet('QPushButton { background-color : #a6a6a6; color : white; }') - - self.jumpButtonNew = QtGui.QPushButton(self.tabs[2]) - self.jumpButtonNew.setGeometry(QRect(305, 192, 160, 20)) - self.jumpButtonNew.setText('Jump to new homeview') - self.moreObjects.append(self.jumpButtonNew) - - - #tab 3 = 'Layer' (layertree) - - self.layerlistwidget = LayerListWidget(self.tabs[3]) - self.layerlistwidget.setGeometry(QRect(25, 70, 210, 350)) - - self.layertreewidget = LayerTreeWidget(self.tabs[3]) - self.layertreewidget.setGeometry(QRect(260, 70, 210, 350)) - - - #tab 4 = 'Permissions' - self.usertabel = QtGui.QTableWidget(self.tabs[4]) - self.usertabel.setGeometry(QRect(10, 30, 230, 300)) - self.usertabel.setColumnCount(3) - self.usertabel.setHorizontalHeaderLabels(['Read', 'Update', 'Delete']) - self.moreObjects.append(self.usertabel) - - self.groupstabel = QtGui.QTableWidget(self.tabs[4]) - self.groupstabel.setGeometry(QRect(250, 30, 230, 300)) - self.groupstabel.setColumnCount(3) - self.groupstabel.setHorizontalHeaderLabels(['Read', 'Update', 'Delete']) - self.moreObjects.append(self.groupstabel) - - - #create Gui surrounding the tabs - self.editCheckBox = QtGui.QCheckBox(self) - self.editCheckBox.setGeometry(QRect(420, 10, 50, 17)) - self.editCheckBox.setText('Edit') - - self.pushButtonOk = QtGui.QPushButton(self) - self.pushButtonOk.setGeometry(QRect(420, 500, 85, 27)) - self.pushButtonCancel = QtGui.QPushButton(self) - self.pushButtonCancel.setGeometry(QRect(320, 500, 85, 27)) - self.pushButtonCancel.setText('Cancel') - - self.warnLabel = QtGui.QLabel(self) - self.warnLabel.setGeometry(QRect(300, 505, 80, 15)) - self.warnLabel.setText('Please fill out all mandatory fields') - self.warnLabel.setHidden(True) - self.warnLabel.setStyleSheet('QLabel { color : #ff6666; }') - - def setEditState(self, b): #b = true or false - if b: - self.pushButtonOk.setText('Save Changes') - self.pushButtonCancel.setHidden(False) - - for editable in self.getAllEditables(): - editable.setEnabled(b) - - - else: - self.pushButtonCancel.setHidden(True) - self.pushButtonOk.setText('OK') - - for editable in self.getAllEditables(): - editable.setEnabled(b) - - def getAllEditables(self): - list = [] - for edit in self.tabedits: - list.append(edit) - for box in self.tabboxes: - list.append(box) - for object in self.moreObjects: - list.append(object) - for box in self.tools.values(): - list.append(box) - list.append(self.layerlistwidget) - list.append(self.layertreewidget) - return list - - - def populateTable(self, table, usersList): - if table == 'users': - table = self.usertabel - else: - table = self.groupstabel - - tableRowCount = len(usersList) - table.setRowCount(tableRowCount) - - for row in range(tableRowCount): - user = usersList[row] - table.setVerticalHeaderItem(row, QtGui.QTableWidgetItem(user['displayTitle'])) - permissions = user['permissions'] - permList = ['Read', 'Update', 'Delete'] - if not permissions: - for x in permList: - item = QtGui.QTableWidgetItem(x) - item.setCheckState(Qt.Unchecked) - table.setItem(row, permList.index(x), item) - else: - for x in permList: - item = QtGui.QTableWidgetItem(x) - if x.upper() in permissions['permissions']: - item.setCheckState(Qt.Checked) - else: - item.setCheckState(Qt.Unchecked) - table.setItem(row, permList.index(x), item) - - def noPermissionAccess(self): - self.usertabel.setHidden(True) - self.groupstabel.setHidden(True) - self.noPermissionAccessLabel = QtGui.QLabel(self.tabs[4]) - self.noPermissionAccessLabel.setGeometry(QRect(50, 50, 420, 50)) - self.noPermissionAccessLabel.setText('Could not access application ' - 'permissions. User permission is not high enough') - - def newAppCreation(self): - self.usertabel.setHidden(True) - self.groupstabel.setHidden(True) - self.noPermissionAccessLabel = QtGui.QLabel(self.tabs[4]) - self.noPermissionAccessLabel.setGeometry(QRect(50, 50, 420, 100)) - self.noPermissionAccessLabel.setText('You have to save and upload the ' - 'new application once\nfirst, then you can edit it\'s permissions') - - def showEpsgWarning(self, currentCrs, applicationCrs): - self.homeviewEpsgWarning.setHidden(False) - txt = 'Note: The coordinate reference system CRS of the current QGIS \n' - txt += 'project: ' + currentCrs +' is different from this application\'s' - txt += ' CRS: ' + applicationCrs + '\nIt is strongly recommended to ' - txt += 'change the QGIS Project CRS\nto ' + applicationCrs + ' before ' - txt += 'working with the homeview' - self.homeviewEpsgWarning.setText(txt) - - def hideEpsgWarning(self): - self.homeviewEpsgWarning.setHidden(True) diff --git a/src/shoguneditor/gui/dialog_bases/connectdlg.py b/src/shoguneditor/gui/dialog_bases/connectdlg.py deleted file mode 100644 index e5e3b83..0000000 --- a/src/shoguneditor/gui/dialog_bases/connectdlg.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtCore import QRect - from qgis.PyQt.QtWidgets import QDialog, QLabel, QLineEdit, QPushButton -else: - from PyQt4.QtCore import QRect - from PyQt4.QtGui import QDialog, QLabel, QLineEdit, QPushButton - -class ConnectDialog(QDialog): - def __init__(self): - QDialog.__init__(self) - self.resize(400, 300) - self.setWindowTitle('Connection Dialog') - - self.label = QLabel(self) - self.label.setGeometry(QRect(140, 150, 81, 20)) - self.label.setText('username') - self.label2 = QLabel(self) - self.label2.setGeometry(QRect(140, 190, 81, 20)) - self.label2.setText('password') - self.label3 = QLabel(self) - self.label3.setGeometry(QRect(40, 32, 151, 20)) - self.label3.setText('Name of the Shogun Client') - self.label4 = QLabel(self) - self.label4.setGeometry(QRect(40, 82, 151, 20)) - self.label4.setText('URL:') - - self.nameIn = QLineEdit(self) - self.nameIn.setGeometry(QRect(200, 30, 180, 27)) - self.nameIn.setText('Default Shogun Client') - self.urlIn = QLineEdit(self) - self.urlIn.setGeometry(QRect(122, 80, 258, 27)) - self.urlIn.setPlaceholderText('i. e.: http(s)://.../shogun2-webapp') - self.userIn = QLineEdit(self) - self.userIn.setGeometry(QRect(230, 150, 150, 27)) - self.passwordIn = QLineEdit(self) - self.passwordIn.setGeometry(QRect(230, 190, 150, 27)) - self.passwordIn.setEchoMode(QLineEdit.Password) - self.okButton = QPushButton(self) - self.okButton.setGeometry(QRect(270, 240, 85, 27)) - self.okButton.setText('OK') - self.cancelButton = QPushButton(self) - self.cancelButton.setGeometry(QRect(180, 240, 85, 27)) - self.cancelButton.setText('Cancel') diff --git a/src/shoguneditor/gui/dialog_bases/dockwidget.py b/src/shoguneditor/gui/dialog_bases/dockwidget.py deleted file mode 100644 index db986f1..0000000 --- a/src/shoguneditor/gui/dialog_bases/dockwidget.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtCore import QRect, Qt - from qgis.PyQt.QtWidgets import QWidget, QPushButton, QDockWidget, QTreeWidget -else: - from PyQt4.QtCore import QRect, Qt - from PyQt4.QtGui import QWidget, QPushButton, QDockWidget, QTreeWidget - -class DockWidget(QDockWidget): - def __init__(self): - QDockWidget.__init__(self) - self.setWindowTitle('Shogun Editor') - self.setContextMenuPolicy(Qt.DefaultContextMenu) - self.setLayoutDirection(Qt.LeftToRight) - self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) - self.setFloating(False) - - self.dockWidgetContents = QWidget(self) - self.dockWidgetContents.setGeometry(QRect(20, 30, 320, 700)) - self.newConnectionButton = QPushButton(self.dockWidgetContents) - self.newConnectionButton.setGeometry(QRect(10, 0, 141, 27)) - self.newConnectionButton.setText('New Connection') - self.treeWidget = QTreeWidget(self.dockWidgetContents) - self.treeWidget.setGeometry(QRect(10, 40, 300, 650)) - self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu) - self.treeWidget.setHeaderHidden(True) - self.treeWidget.setColumnCount(1) diff --git a/src/shoguneditor/gui/dialog_bases/layerSettings.py b/src/shoguneditor/gui/dialog_bases/layerSettings.py deleted file mode 100644 index 8c52af3..0000000 --- a/src/shoguneditor/gui/dialog_bases/layerSettings.py +++ /dev/null @@ -1,256 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtCore import QRect, Qt - from qgis.PyQt.QtGui import QDoubleValidator - # we are faking the old way of QtGui, not the best style, but makes it easier - # for switching betweeng version 2 and 3 - from qgis.PyQt import QtWidgets as QtGui -else: - from PyQt4.QtCore import QRect, Qt - from PyQt4 import QtGui - -from qgis.gui import QgsMapLayerComboBox - -class LayerSettingsDialog(QtGui.QDialog): - def __init__(self): - QtGui.QDialog.__init__(self) - self.tabs = [] #All child-tabWidgets - self.tabedits = [] #All QLineEdits and QComboBox per tabWidget in a list - self.tabboxes = [] #All QCheckBoxes per tabWidget in a list - self.moreObjects = [] - self.setupUi() - - def setupUi(self): - self.resize(550, 550) - self.setWindowTitle('Settings') - - #create tabWidget that holds the tabs - self.tabWidget = QtGui.QTabWidget(self) - self.tabWidget.setGeometry(QRect(10, 20, 500, 480)) - self.tabWidget.setObjectName('tabWidget') - tab0labels = [['Name', (50, 50, 56, 17)],['Layer Opacity',(50,100,80,25)], ['Hover Template', (50, 150, 120, 17)]] - tab1labels = [['Until now "Metadata" has to be edited in the shogun2-webapp', (50, 50, 300, 17)]] - tab2labels = [['explanation', (50, 50, 400, 200)]] - tab3labels = [['Users', (100, 10, 50, 20)], ['Groups', (320, 10, 50, 20)]] - tabwidgets = [['General', tab0labels], ['Metadata', tab1labels], ['Style', tab2labels], ['Permissions', tab3labels]] - - expl = 'To edit the style of layer in shogun, first add the layer to QGIS.\n' - expl += 'Then style the layer via the QGIS layer properties.\nWhen finished, ' - expl += 'you can upload the current layer style \nto this layer in Shogun by ' - expl += 'right-clicking it in \nthe Shogun Editor menu' - - #first set the labes for all tabwwidgets in a loop: - for tab in tabwidgets: - t = QtGui.QWidget() - t.setObjectName(tab[0]) - self.tabs.append(t) - self.tabWidget.addTab(t, tab[0]) - - for label in tab[1]: - l = QtGui.QLabel(t) - l.setGeometry(QRect(label[1][0],label[1][1],label[1][2],label[1][3])) - if label[0] == 'explanation': - l.setText(expl) - l.setAlignment(Qt.AlignTop) - else: - l.setText(label[0]) - - - self.tabWidget.setCurrentIndex(0) - - - #then populate the specific tabwidgets with other QObjects: - #tab 0 = 'General': - self.nameEdit = QtGui.QLineEdit(self.tabs[0]) - self.nameEdit.setGeometry(QRect(180, 40, 113, 27)) - self.tabedits.append(self.nameEdit) - - self.sliderEdit = QtGui.QLineEdit(self.tabs[0]) - self.sliderEdit.setGeometry(QRect(400, 90, 30, 23)) - self.sliderEdit.setInputMask('9.99') - if sys.version_info[0] >= 3: - validator = QDoubleValidator(-0.01, 1.01, 2) - else: - validator = QtGui.QDoubleValidator(-0.01, 1.01, 2) - self.sliderEdit.setValidator(validator) - self.tabedits.append(self.sliderEdit) - - self.hoverEdit = QtGui.QLineEdit(self.tabs[0]) - self.hoverEdit.setGeometry(QRect(180, 140, 113,27)) - self.tabedits.append(self.hoverEdit) - - self.hoverBox = QtGui.QComboBox(self.tabs[0]) - self.hoverBox.setGeometry(QRect(320, 140, 80, 27)) - self.tabedits.append(self.hoverBox) - - self.hoverAddButton = QtGui.QPushButton(self.tabs[0]) - self.hoverAddButton.setGeometry(QRect(410, 140, 30, 27)) - self.hoverAddButton.setText('Add') - self.tabedits.append(self.hoverAddButton) - - self.slider = QtGui.QSlider(self.tabs[0]) - self.slider.setGeometry(QRect(180, 90, 160, 18)) - self.slider.setOrientation(Qt.Horizontal) - self.slider.setMaximum(100) - self.slider.setMinimum(-1) - self.slider.setEnabled(False) - self.moreObjects.append(self.slider) - self.slider.valueChanged.connect(lambda: self.sliderEdit.setText(str(float(self.slider.value())/100))) - self.sliderEdit.textEdited.connect(lambda: self.slider.setValue(int(float(self.sliderEdit.text())*100))) - - self.hoverAddButton.clicked.connect(self.addHoverAttribute) - - - - #tab 3 = 'Permissions': - self.usertabel = QtGui.QTableWidget(self.tabs[3]) - self.usertabel.setGeometry(QRect(10, 30, 230, 300)) - self.usertabel.setColumnCount(3) - self.usertabel.setHorizontalHeaderLabels(['Read', 'Update', 'Delete']) - self.moreObjects.append(self.usertabel) - - self.groupstabel = QtGui.QTableWidget(self.tabs[3]) - self.groupstabel.setGeometry(QRect(250, 30, 230, 300)) - self.groupstabel.setColumnCount(3) - self.groupstabel.setHorizontalHeaderLabels(['Read', 'Update', 'Delete']) - self.moreObjects.append(self.groupstabel) - - - #create Gui surrounding the tabs - self.editCheckBox = QtGui.QCheckBox(self) - self.editCheckBox.setGeometry(QRect(420, 10, 50, 17)) - self.editCheckBox.setText('Edit') - - self.pushButtonOk = QtGui.QPushButton(self) - self.pushButtonOk.setGeometry(QRect(420, 500, 85, 27)) - self.pushButtonCancel = QtGui.QPushButton(self) - self.pushButtonCancel.setGeometry(QRect(320, 500, 85, 27)) - self.pushButtonCancel.setText('Cancel') - - - def addHoverAttribute(self): - attribute = self.hoverBox.currentText() - if len(attribute) > 0: - attribute = '{' + attribute + '}' - text = self.hoverEdit.text() + attribute - self.hoverEdit.setText(text) - - - def setEditState(self, b): #b = true or false - if b: - self.pushButtonOk.setText('Save Changes') - self.pushButtonCancel.setHidden(False) - for editable in self.getAllEditables(): - editable.setEnabled(b) - else: - self.pushButtonCancel.setHidden(True) - self.pushButtonOk.setText('OK') - for editable in self.getAllEditables(): - editable.setEnabled(b) - - def getAllEditables(self): - list = [] - for edit in self.tabedits: - list.append(edit) - for box in self.tabboxes: - list.append(box) - for object in self.moreObjects: - list.append(object) - return list - - - def deactivateHoverEditing(self): - self.hoverBox.setHidden(True) - self.hoverEdit.setHidden(True) - self.hoverAddButton.setHidden(True) - self.infoEdit = QtGui.QLineEdit(self.tabs[0]) - self.infoEdit.setEnabled(False) - self.infoEdit.setText('only available for vector layers') - self.infoEdit.setGeometry(QRect(180, 143, 200 ,27)) - - - def populateTable(self, table, usersList): - if table == 'users': - table = self.usertabel - else: - table = self.groupstabel - - usersList = sorted(usersList, key = lambda x : x['displayTitle']) - tableRowCount = len(usersList) - table.setRowCount(tableRowCount) - - for row in range(tableRowCount): - user = usersList[row] - table.setVerticalHeaderItem(row, QtGui.QTableWidgetItem(user['displayTitle'])) - permissions = user['permissions'] - permList = ['Read', 'Update', 'Delete'] - if not permissions: - for x in permList: - item = QtGui.QTableWidgetItem(x) - item.setCheckState(Qt.Unchecked) - table.setItem(row, permList.index(x), item) - else: - perms = permissions['permissions'] - for x in permList: - item = QtGui.QTableWidgetItem(x) - if perms[0] == 'ADMIN' or x.upper() in perms: - item.setCheckState(Qt.Checked) - else: - item.setCheckState(Qt.Unchecked) - table.setItem(row, permList.index(x), item) - table.sortItems(0, Qt.AscendingOrder) - - - def noPermissionAccess(self): - self.usertabel.setHidden(True) - self.groupstabel.setHidden(True) - self.noPermissionAccessLabel = QtGui.QLabel(self.tabs[3]) - self.noPermissionAccessLabel.setGeometry(QRect(50, 50, 420, 50)) - self.noPermissionAccessLabel.setText('Could not access application ' - 'permissions. User permission is not high enough') - - - -class UploadLayerDialog(QtGui.QDialog): - def __init__(self): - QtGui.QDialog.__init__(self) - self.setupUi() - - def setupUi(self): - self.resize(400, 400) - self.setWindowTitle('Upload layer to Shogun') - - title = QtGui.QLabel(self) - title.setGeometry(50,30,300,70) - title.setText('Please select the layer you wish to upload to the \n Shogun Server') - - self.layerBox = QgsMapLayerComboBox(self) - self.layerBox.setGeometry(QRect(50, 100, 300, 30)) - - self.uploadButton = QtGui.QPushButton(self) - self.uploadButton.setGeometry(QRect(250, 160, 100, 35)) - self.uploadButton.setText('Upload Layer') - - self.cancelButton = QtGui.QPushButton(self) - self.cancelButton.setGeometry(QRect(140, 160, 100, 35)) - self.cancelButton.setText('Cancel') - self.cancelButton.clicked.connect(self.hide) - - self.logWindow = QtGui.QTextEdit(self) - self.logWindow.setGeometry(QRect(50, 200, 300, 180)) - self.logWindow.setReadOnly(True) - self.logWindow.setText('Upload Log:') - - def log(self, message): - msg = ' - ' + message - self.logWindow.append(msg) diff --git a/src/shoguneditor/gui/editor.py b/src/shoguneditor/gui/editor.py deleted file mode 100644 index 67d54bf..0000000 --- a/src/shoguneditor/gui/editor.py +++ /dev/null @@ -1,258 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtCore import QObject, Qt, QTimer - from qgis.PyQt.QtWidgets import QMenu, QAction, QMessageBox - from qgis.PyQt.QtWidgets import QTreeWidgetItemIterator -else: - from PyQt4.QtCore import QObject, Qt, QTimer - from PyQt4.QtGui import QMenu, QAction, QMessageBox, QTreeWidgetItemIterator - -from qgis.gui import QgsMessageBar -from qgis.core import QgsNetworkAccessManager - -from .dialog_bases.connectdlg import ConnectDialog -from .dialog_bases.dockwidget import DockWidget -from .editoritems import EditorItem, EditorTopItem, QgisLayerItem, ApplicationItem, LayerItem -from shoguneditor.connection.shogunressource import ShogunRessource - - -class Editor(QObject): - '''This class controls all plugin-related GUI elements.''' - - def __init__ (self, iface): - '''initialize the GUI control''' - QObject.__init__(self) - self.iface = iface - - self.dock = DockWidget() - self.connectdlg = ConnectDialog() - self.iface.addDockWidget( Qt.RightDockWidgetArea, self.dock) - self.dock.newConnectionButton.clicked.connect(lambda: self.showDialog(self.connectdlg)) - self.connectdlg.okButton.clicked.connect(self.setupNewConnection) - self.topitem = EditorTopItem() - self.connections = [] - self.dock.treeWidget.addTopLevelItem(self.topitem) - self.dock.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu) - self.dock.treeWidget.customContextMenuRequested.connect(self.on_context_menu) - self.dock.treeWidget.itemDoubleClicked.connect(self.on_tree_item_double_clicked) - - self.timer = QTimer() - - ''' - WORKAROUND: - When performing requests with self.http, it will call the - QgsNetworkAccessManager.instance(). For some reason - QgsNetworkAccessManager.instance() is connected with the QtCore SIGNAL - 'authenticationRequired' (inherited from QNetworkAccessManager) to a - method where a dialog in QGIS pops up asking for the users credentials - (when working with Basic Auth). We disable the signal here as the - case of wrong identifacation credentials is treated separately - in def: checkConnection(self) - ''' - try: - QgsNetworkAccessManager.instance().authenticationRequired.disconnect() - except: - pass - - - def on_context_menu(self, point): - - item = self.dock.treeWidget.itemAt(point) - if item is None: - return - if item.actiontype is None: - return - actionDict = {'application': - ('Copy Application', 'Load all layers to QGIS', - 'Application Settings', 'View Application in web browser', - 'Delete Application'), - 'layer': - ('Add Layer to QGIS','Layer Settings', 'Delete Layer'), - 'qgisLayerReference': - ('Upload New Style', 'Apply Original Style'), - 'applicationsItem': - ('Create New Application', 'Refresh Applications'), - 'layersItem':('Upload New Layer from QGIS', 'Refresh Layers'), - 'connection':('Refresh Connection', 'Remove Connection'), - 'topitem':['New Connection']} - - actions = actionDict[item.actiontype] - menu = QMenu() - acts = [] - for actionName in actions: - action = QAction(actionName, None) - self.connectAction(action, actionName, item) - acts.append(action) - menu.addActions(acts) - point = self.dock.treeWidget.mapToGlobal(point) - menu.exec_(point) - - # this could be re-written when refactoring: - def connectAction(self, action, actionName, item): - if actionName == 'Copy Application': - action.triggered.connect(item.copyApplication) - elif actionName == 'View Application in web browser': - action.triggered.connect(lambda: item.ressource.viewApplicationOnline(item.id)) - elif actionName == 'New Connection': - action.triggered.connect(lambda: self.showDialog(self.connectdlg)) - elif actionName == 'Application Settings': - action.triggered.connect(lambda: self.showDialog(item)) - elif actionName == 'Layer Settings': - action.triggered.connect(lambda: self.showDialog(item)) - elif actionName == 'Remove Connection': - action.triggered.connect(lambda: self.removeConnection(item)) - elif actionName == 'Refresh Connection': - action.triggered.connect(lambda: self.refreshConnection(item)) - elif actionName == 'Add Layer to QGIS': - action.triggered.connect(lambda: item.addQgsLayer(self.iface)) - elif actionName == 'Upload New Style': - action.triggered.connect(lambda: self.uploadStyle(item)) - elif actionName == 'Apply Original Style': - action.triggered.connect(lambda: self.downloadStyle(item)) - elif actionName == 'Load all layers to QGIS': - action.triggered.connect(lambda: self.loadAllAppLayers(item)) - elif actionName == 'Create New Application': - action.triggered.connect(lambda: item.createNewApplication(self.iface)) - elif actionName == 'Upload New Layer from QGIS': - action.triggered.connect(lambda: item.createNewLayer(self.iface)) - elif actionName == 'Delete Layer': - action.triggered.connect(item.deleteLayer) - elif actionName == 'Delete Application': - action.triggered.connect(item.deleteApplication) - elif actionName == 'Refresh Applications': - action.triggered.connect(item.update) - elif actionName == 'Refresh Layers': - action.triggered.connect(item.update) - - - def on_tree_item_double_clicked(self, item): - if isinstance(item, QgisLayerItem): - try: - self.iface.showLayerProperties(item.layer) - except: - pass - - - def showDialog(self, item): - if not isinstance(item, ConnectDialog): - if item.dlg is None: - if isinstance(item, ApplicationItem): - item.createStaticSettings(self.iface) - item.populateSettings() - elif isinstance(item, LayerItem): - item.createStaticSettings() - item.populateSettings() - dialog = item.dlg - else: - dialog = self.connectdlg - - dialog.show() - try: - dialog.setWindowState(Qt.WindowActive) - dialog.activateWindow() - except: - pass - - - def setupNewConnection(self): - # if the connect button gets clicked two times quickly it calls a new - # test request before the old one is finished and making qgis crash - # therefore we pause the signal for 1,5 seconds - self.connectdlg.okButton.blockSignals(True) - self.timer.timeout.connect(lambda: self.connectdlg.okButton.blockSignals(False)) - self.timer.start(1500) - - url = self.connectdlg.urlIn.text() - name = self.connectdlg.nameIn.text() - user = self.connectdlg.userIn.text() - pw = self.connectdlg.passwordIn.text() - - if len(url) == 0 or len(name) == 0: - self.showWarning(self.connectdlg, 'Please fill in all necessary ' - 'fields') - self.connectdlg.show() - return - - if name in self.connections: - self.showWarning(self.connectdlg, 'A connection with that name ' - 'exists already, please fill in a different name') - self.connectdlg.show() - return - - newRessource = ShogunRessource(self.iface, url, name, user, pw) - connectionOk = newRessource.checkConnection() - if not connectionOk[0]: - self.showWarning(self.connectdlg, connectionOk[1]) - self.connectdlg.show() - return - - bool = newRessource.updateData() - if not bool: - self.showWarning(self.connectdlg, 'Error: Could not retrieve all ' - 'data from Shogun') - - self.connectdlg.hide() - - newConnectionItem = EditorItem(newRessource) - self.connections.append(name) - self.topitem.addChild(newConnectionItem) - self.topitem.setExpanded(True) - newConnectionItem.applicationsitem.sortChildren(0, Qt.AscendingOrder) - newConnectionItem.layersitem.sortChildren(0, Qt.AscendingOrder) - self.expandEditorTree(newConnectionItem) - - def removeConnection(self, item): - if item.name in self.connections: - self.connections.remove(item.name) - self.topitem.removeChild(item) - - def refreshConnection(self, item): - item.ressource.updateData() - item.update() - self.topitem.setExpanded(True) - self.expandEditorTree(item) - - def showWarning(self, parent, text): - warn = QMessageBox.warning(parent, 'Warning', - text, QMessageBox.Ok) - - def uploadStyle(self, item): - success = item.uploadStyle() - if success: - msg = 'New style of layer '+ item.parentShogunLayer.name - msg += ' was successfully uploaded to Shogun.' - self.iface.messageBar().pushSuccess('Success', msg) - else: - msg = 'New style of layer '+ item.parentShogunLayer.name - msg += ' could not be uploaded to Shogun.' - self.iface.messageBar().pushCritical('Error',msg) - - def downloadStyle(self, item): - success = item.downloadStyle() - if not success: - msg = 'Could not download Style for layer' - self.iface.messageBar().pushCritical('Error',msg) - - def loadAllAppLayers(self, item): - layerIds, shogunConnectionItem = item.getAllAppLayersById() - for layer in shogunConnectionItem.layersitem.layerlist: - if layer.id in layerIds: - layer.addQgsLayer(self.iface) - - def expandEditorTree(self, connectionItem): - iter = QTreeWidgetItemIterator(connectionItem) - val = iter.value() - while val: - val.setExpanded(True) - iter += 1 - val = iter.value() diff --git a/src/shoguneditor/gui/editoritems.py b/src/shoguneditor/gui/editoritems.py deleted file mode 100644 index 5239e7c..0000000 --- a/src/shoguneditor/gui/editoritems.py +++ /dev/null @@ -1,1115 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ - This code is licensed under the GPL 2.0 license. -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtWidgets import QLabel, QLineEdit, QLabel, QMessageBox, QTreeWidgetItem - from qgis.PyQt.QtGui import QFont, QIcon - from qgis.PyQt.QtCore import QRect, Qt - from qgis.core import QgsPointXY, QgsProject, QgsWkbTypes - import shoguneditor.resources3 -else: - from PyQt4.QtGui import QLabel, QLineEdit, QFont, QLabel, QMessageBox, QTreeWidgetItem, QIcon - from PyQt4.QtCore import QRect, Qt - from qgis.core import QgsPoint, QgsMapLayerRegistry, QGis - import shoguneditor.resources2 - -from qgis.core import QgsMapLayer, QgsProject, QgsLayerItem -from qgis.core import QgsRectangle - -from shoguneditor.layerutils import prepareLayerForUpload, createLayer -from .dialog_bases.applicationSettings import ApplicationSettingsDialog -from .dialog_bases.layerSettings import LayerSettingsDialog, UploadLayerDialog - - -PYTHON_VERSION = sys.version_info[0] - -'''This file contains all classes used in the QTreeWidget representating the -structure of the connected shogun2-webapp ressource''' - -## TODO: the classes LayerItem and ApplicationItem could be merged or inherit -# from a common abstract class, the same is LayerSettingsDialog and -# ApplicationSettingsDialog. Note when refactoring for future release - -class TreeItem(QTreeWidgetItem): - ''' This class works as a kind of 'abstract class' from which all other - classes in this module inherit''' - def __init__(self, icon = None, text = None): - QTreeWidgetItem.__init__(self) - self.setText(0, text) - if icon is not None: - if isinstance(icon, QIcon): - self.setIcon(0, icon) - elif isinstance(icon, str): - iconPath = ':/plugins/shoguneditor/' + icon - self.setIcon(0, QIcon(iconPath)) - self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) - self.actiontype = None - - - -class EditorTopItem(TreeItem): - def __init__(self): - TreeItem.__init__( - self, 'shogun-logo-50x50px-round-blue.png', 'Shogun Connections') - self.actiontype = 'topitem' - font = QFont('Arial',14) - font.setBold(True) - self.setFont(0,font) - - - -class EditorItem(TreeItem): - def __init__(self, shogunRessource): - self.ressource = shogunRessource - self.name = shogunRessource.name - TreeItem.__init__(self, 'shogun-logo-25x-25px.png', self.name) - self.isConnected = False - self.actiontype = 'connection' - font = QFont('Arial',12) - font.setBold(True) - self.setFont(0,font) - self.populateTree(shogunRessource) - - def disconnectSignals(self): - if self.layersitem is not None: - for layer in self.layersitem.layerlist: - if PYTHON_VERSION >= 3: - try: - QgsProject.instance().layerRemoved.disconnect(layer.updateLayerList) - except: - pass - else: - try: - QgsMapLayerRegistry.instance().layerRemoved.disconnect(layer.updateLayerList) - except: - pass - - def update(self): - for x in [self.applicationsitem, self.layersitem]: - self.removeChild(x) - self.populateTree(self.ressource) - - def populateTree(self, shogunRessource): - self.applicationsitem = ApplicationsItem(shogunRessource) - self.layersitem = LayersItem(shogunRessource) - - self.addChildren([self.applicationsitem, self.layersitem]) - - - -class ApplicationsItem(TreeItem): - def __init__(self, ressource): - TreeItem.__init__(self, 'applications-logo.png', 'Applications') - self.ressource = ressource - self.applications = self.ressource.getApplicationIdsAndNames() - self.applicationlist = [] - self.actiontype = 'applicationsItem' - font = QFont('Arial',10) - font.setBold(True) - self.setFont(0,font) - self.populate() - - - def createNewApplication(self, iface): - self.newApplication = ApplicationItem(None, '', self.ressource) - self.newApplication.createStaticSettings(iface) - self.dlg = self.newApplication.dlg - self.dlg.setEditState(True) - self.dlg.editCheckBox.setHidden(True) - self.dlg.pushButtonOk.setText('Create') - self.dlg.pushButtonOk.clicked.disconnect(self.dlg.hide) - self.dlg.pushButtonOk.clicked.connect(self.checkNewApplicationSettings) - self.dlg.pushButtonCancel.clicked.disconnect(self.newApplication.stopEditing) - self.dlg.pushButtonCancel.clicked.connect(self.dlg.hide) - self.dlg.pushButtonCancel.setHidden(False) - - for toolbox in self.dlg.tools.values(): - toolbox.setChecked(True) - - exampleHomeview = {'mapconfig' : { - 'id' : None, - 'center' : {'x' : 0.0, 'y' : 0.0}, - 'zoom' : 18, - 'resolutions': [156543.03390625, 78271.516953125, 39135.7584765625, - 19567.87923828125, 9783.939619140625, 4891.9698095703125, 2445.9849047851562, - 1222.9924523925781, 611.4962261962891, 305.74811309814453, 152.87405654907226, - 76.43702827453613, 38.218514137268066, 19.109257068634033, 9.554628534317017, - 4.777314267158508, 2.388657133579254, 1.194328566789627, 0.5971642833948135] - } - } - - self.newApplication.homeview = exampleHomeview - self.newApplication.setQgsExtent(iface) - self.dlg.origExtentButton.setEnabled(False) - self.dlg.jumpButtonOrig.setEnabled(False) - self.dlg.homeviewZoomBox.setMaximum(18) - - allLayers = self.ressource.getLayerIdsAndNames() - self.dlg.layerlistwidget.populateList(allLayers) - self.dlg.layertreewidget.setupNewTree() - - self.dlg.newAppCreation() - - self.dlg.show() - - def checkNewApplicationSettings(self): - name = self.dlg.nameEdit.text() - - if len(name) == 0: - self.dlg.nameEdit.setStyleSheet('QLineEdit { background-color: #ff3333; }') - self.dlg.warnLabel.setHidden(False) - return - else: - self.dlg.nameEdit.setStyleSheet('QLineEdit { border-color : #000000; }') - self.dlg.warnLabel.setHidden(True) - - newhomeview = self.newApplication.getHomeViewChanges() - if newhomeview is None: - newhomeview = self.newApplication.homeview['mapconfig'] - data = { - 'id' : None, - 'name' : name, - 'description' : self.dlg.descriptionEdit.text(), - 'language' : self.dlg.languageBox.currentText(), - 'isPublic' : self.dlg.boxPublic.isChecked(), - 'isActive' : self.dlg.boxActive.isChecked(), - 'activeTools' : [x['id'] for x in self.newApplication.getActiveToolsChanges()], - 'projection' : 'EPSG:3857', - 'center' : {'x' : newhomeview['center']['x'], 'y' : newhomeview['center']['y']}, - 'zoom' : newhomeview['zoom'], - 'layerTree': 4535 - } - - self.dlg.hide() - uploaded = self.ressource.uploadNewApplication(data) - if uploaded: - self.update() - - def populate(self): - for app in self.applications: - item = ApplicationItem(app[0], app[1], self.ressource) - self.addChild(item) - self.applicationlist.append(item) - self.sortChildren(0, Qt.AscendingOrder) - - def update(self): - self.applications = self.ressource.getApplicationIdsAndNames(reload = True) - for item in self.applicationlist: - self.removeChild(item) - self.applicationlist = [] - for app in self.applications: - item = ApplicationItem(app[0], app[1], self.ressource) - self.addChild(item) - self.applicationlist.append(item) - self.sortChildren(0, Qt.AscendingOrder) - - - -class ApplicationItem(TreeItem): - def __init__(self, id, name, ressource): - TreeItem.__init__(self, None, name) - self.actiontype = 'application' - self.id = id - self.name = name - self.dlg = None - self.ressource = ressource - self.activeTools = [] - self.homeview = None - self.settings = { - 'id' : 0, - 'name' : '', - 'description' : '', - 'language' : '', - 'open' : False, - 'active' : False, - 'activeTools' : [], - } - - def createStaticSettings(self, iface): - self.dlg = ApplicationSettingsDialog() - self.dlg.setEditState(False) - self.dlg.pushButtonOk.clicked.connect(self.dlg.hide) - self.dlg.editCheckBox.clicked.connect(self.startEditing) - self.dlg.pushButtonCancel.clicked.connect(self.stopEditing) - - self.iface = iface - - self.dlg.origExtentButton.clicked.connect(self.setOriginalExtent) - self.dlg.qgsExtentButton.clicked.connect(lambda: self.setQgsExtent()) - self.dlg.jumpButtonOrig.clicked.connect(lambda: self.zoomToOrigExtent()) - - self.dlg.homeviewZoomBox.valueChanged.connect(lambda: self.zoomToNewExtent()) - self.dlg.homeviewCenterEditX.textEdited.connect(lambda: self.zoomToNewExtent()) - self.dlg.homeviewCenterEditY.textEdited.connect(lambda: self.zoomToNewExtent()) - self.dlg.jumpButtonNew.clicked.connect(lambda: self.zoomToNewExtent()) - - - def setOriginalExtent(self): - disableSignals = [self.dlg.homeviewZoomBox, self.dlg.homeviewCenterEditX] - disableSignals.append(self.dlg.homeviewCenterEditY) - disableSignals.append(self.dlg.jumpButtonNew) - - for qobject in disableSignals: - qobject.blockSignals(True) - - self.dlg.homeviewCenterEditX.setText(str(self.homeview['mapconfig']['center']['x'])) - self.dlg.homeviewCenterEditY.setText(str(self.homeview['mapconfig']['center']['y'])) - self.dlg.homeviewZoomBox.setValue(self.homeview['mapconfig']['zoom']) - self.populateExtentEdits(self.origExtRect) - - for qobject in disableSignals: - qobject.blockSignals(False) - - - def populateExtentEdits(self, extent): - self.dlg.extentEdits[0].setText(str(extent.xMinimum())) - self.dlg.extentEdits[1].setText(str(extent.yMinimum())) - self.dlg.extentEdits[2].setText(str(extent.xMaximum())) - self.dlg.extentEdits[3].setText(str(extent.yMinimum())) - - - def setQgsExtent(self): - rect = self.iface.mapCanvas().extent() - self.populateExtentEdits(rect) - center = self.iface.mapCanvas().center() - self.dlg.homeviewCenterEditX.setText(str(center.x())) - self.dlg.homeviewCenterEditY.setText(str(center.y())) - - zoom = self.iface.mapCanvas().mapUnitsPerPixel() - #get shogun resolutions as enumerated list - resolutions = list(enumerate(self.homeview['mapconfig']['resolutions'])) - cursor = resolutions[0] - for res in resolutions: - if abs(zoom - res[1]) < abs(zoom - cursor[1]): - cursor = res - # cursor is now the Shogun resolution closest to the qgis resolution - - self.dlg.homeviewZoomBox.setValue(cursor[0]) - - - def zoomToOrigExtent(self): - #first set the original homeview center as the new canvas center - center = self.homeview['mapconfig']['center'] - if PYTHON_VERSION >= 3: - point = QgsPointXY(center['x'],center['y']) - else: - point = QgsPoint(center['x'],center['y']) - self.iface.mapCanvas().setCenter(point) - - zoomlvl = self.homeview['mapconfig']['zoom'] - zoom = self.homeview['mapconfig']['resolutions'][zoomlvl] - - currentResolution = self.iface.mapCanvas().mapUnitsPerPixel() - if currentResolution != zoom: - diff = zoom/currentResolution - self.iface.mapCanvas().zoomByFactor(diff) - - self.iface.mapCanvas().refresh() - - - def zoomToNewExtent(self): - x = float(self.dlg.homeviewCenterEditX.text()) - y = float(self.dlg.homeviewCenterEditY.text()) - if PYTHON_VERSION >= 3: - point = QgsPointXY(x, y) - else: - point = QgsPoint(x, y) - self.iface.mapCanvas().setCenter(point) - - zoomlvl = self.dlg.homeviewZoomBox.value() - zoom = self.homeview['mapconfig']['resolutions'][zoomlvl] - - currentResolution = self.iface.mapCanvas().mapUnitsPerPixel() - if currentResolution != zoom: - diff = zoom/currentResolution - self.iface.mapCanvas().zoomByFactor(diff) - - self.iface.mapCanvas().refresh() - - - - def populateSettings(self): - # get application settings as a dict representing the json - settings = self.ressource.getApplicationAttrsById(self.id) - for attr in self.settings: - self.settings[attr] = settings[attr] - - # set the general settings - self.dlg.nameEdit.setText(settings['name']) - self.dlg.descriptionEdit.setText(settings['description']) - self.dlg.languageBox.setCurrentIndex(self.dlg.languageBox.findText( - settings['language'])) - self.dlg.boxPublic.setChecked(settings['open']) - self.dlg.boxActive.setChecked(settings['active']) - - - # set the configured homeview - self.mapConfigId = settings['viewport']['subModules'][0]['subModules'][0]['mapConfig']['id'] - self.extentId = settings['viewport']['subModules'][0]['subModules'][0]['mapConfig']['extent']['id'] - # getHomeviewByIds returns homeview as dict {'mapconfig':XY,'extent':XY} - self.homeview = self.ressource.getHomeviewByIds(self.mapConfigId, self.extentId) - - self.dlg.homeviewZoomBox.setMaximum(len(self.homeview['mapconfig']['resolutions'])-1) - origExtent = list(self.homeview['extent']['lowerLeft'].values())+list(self.homeview['extent']['upperRight'].values()) - self.origExtRect = QgsRectangle(origExtent[0],origExtent[1],origExtent[2],origExtent[3]) - self.setOriginalExtent() - - self.epsg = self.homeview['mapconfig']['projection'] - if PYTHON_VERSION >= 3: - currentQgsCrs = QgsProject.instance().crs().authid() - else: - currentQgsCrs = self.iface.mapCanvas().mapRenderer().destinationCrs().authid() - - if self.epsg != currentQgsCrs: - self.dlg.showEpsgWarning(currentQgsCrs, self.epsg) - else: - self.dlg.hideEpsgWarning() - - # set the application tools - self.activeTools = [{'id' : tool['id']} for tool in settings['activeTools']] - for tool in self.activeTools: - self.dlg.tools[tool['id']].setChecked(True) - - # populate the layer tree and the table of available layers - layerTree = settings['layerTree'] - self.dlg.layertreewidget.populateTree(layerTree) - - allLayers = self.ressource.getLayerIdsAndNames() - self.dlg.layerlistwidget.populateList(allLayers) - - # populate the permissions tables - self.userPermissions = self.ressource.getObjectPermissions(self.id, 'Application', 'User') - self.groupPermissions = self.ressource.getObjectPermissions(self.id, 'Application', 'UserGroup') - - if not self.userPermissions['success'] or not self.groupPermissions['success']: - self.dlg.noPermissionAccess() - else: - self.dlg.populateTable('users', self.userPermissions['data']['permissions']) - self.dlg.populateTable('groups', self.groupPermissions['data']['permissions']) - - def getAllAppLayersById(self): - rootConnectionItem = self.parent().parent() - settings = self.ressource.getApplicationAttrsById(self.id) - listOfIds = [] - layerTree = settings['layerTree'] - - # for reasons of simplicity we only search for belonging layers down to the - # third level of the tree - if layerTree['leaf']: - listOfIds.append(layerTree['layer']['id']) - return listOfIds, rootConnectionItem - else: - children = layerTree['children'] - for child in children: - if child['leaf']: - listOfIds.append(child['layer']['id']) - else: - children = child['children'] - for child in children: - if child['leaf']: - listOfIds.append(child['layer']['id']) - else: - children = child['children'] - for child in children: - if child['leaf']: - listOfIds.append(child['layer']['id']) - return listOfIds, rootConnectionItem - - - def getAllChanges(self): - allChanges = {} - if self.getGeneralChanges() is not None: - allChanges['general'] = self.getGeneralChanges() - - if self.getActiveToolsChanges() is not None: - if not 'general' in allChanges: - allChanges['general'] = {} - allChanges['general']['activeTools'] = self.getActiveToolsChanges() - - if self.getHomeViewChanges() is not None: - allChanges['homeview'] = self.getHomeViewChanges() - - layerTreeChanges = self.dlg.layertreewidget.getLayerTreeChanges() - if layerTreeChanges is not None: - allChanges['layerTree'] = layerTreeChanges - - userPermissionChanges = self.getPermissionChanges('User') - if userPermissionChanges is not None: - allChanges['permissions'] = {'User' : userPermissionChanges} - groupPermissionChanges = self.getPermissionChanges('UserGroup') - if groupPermissionChanges is not None: - if not 'permissions' in allChanges: - allChanges['permissions'] = {} - allChanges['permissions']['UserGroup'] = groupPermissionChanges - - return allChanges - - - def getGeneralChanges(self): - changes = {} - if self.dlg.nameEdit.text() != self.settings['name']: - changes['name'] = self.dlg.nameEdit.text() - if self.dlg.descriptionEdit.text() != self.settings['description']: - changes['description'] = self.dlg.descriptionEdit.text() - if self.dlg.languageBox.currentText() != self.settings['language']: - changes['languae'] = self.dlg.languageBox.currentText() - if self.dlg.boxPublic.isChecked() != self.settings['open']: - changes['open'] = self.dlg.boxPublic.isChecked() - if self.dlg.boxActive.isChecked() != self.settings['active']: - changes['active'] = self.dlg.boxActive.isChecked() - - if len(changes) == 0: - return None - else: - return changes - - - def getActiveToolsChanges(self): - toolsCheckedInGui = [] - for toolid in self.dlg.tools.keys(): - if self.dlg.tools[toolid].isChecked(): - toolsCheckedInGui.append(toolid) - - #convert the lists to set for in case they have different orders - if set(toolsCheckedInGui) != set([tool['id'] for tool in self.activeTools]): - return [{'id': toolid} for toolid in toolsCheckedInGui] - else: - return None - - - def getHomeViewChanges(self): - zoom = self.dlg.homeviewZoomBox.value() - x = float(self.dlg.homeviewCenterEditX.text()) - y = float(self.dlg.homeviewCenterEditY.text()) - conf = self.homeview['mapconfig'] - newhomeview = None - - if (conf['zoom'] != zoom or - conf['center']['x'] != x or - conf['center']['y'] != y): - newhomeview = { - 'id': self.homeview['mapconfig']['id'], 'zoom': zoom, - 'center':{'x':x, 'y':y} } - return newhomeview - - - def getPermissionChanges(self, type): - if type == 'User': - permissions = self.userPermissions['data'] - table = self.dlg.usertabel - elif type == 'UserGroup': - permissions = self.groupPermissions['data'] - table = self.dlg.groupstabel - else: - return - oldPermissionList = permissions['permissions'] - newPermissionList = [] - for entry in oldPermissionList: - name = entry['displayTitle'] - row = None - for x in range(table.rowCount()): - if table.verticalHeaderItem(x).text() == name: - row = x - break - if row is None: - return - - currentPermissionsInTable = [] - for p in enumerate(['READ', 'UPDATE', 'DELETE']): - if table.item(row, p[0]).checkState() == 2: - currentPermissionsInTable.append(p[1]) - - if entry['permissions'] == None: - oldPermissions = [] - else: - oldPermissions = entry['permissions']['permissions'] - if oldPermissions == ['ADMIN']: - oldPermissions = ['READ', 'UPDATE', 'DELETE'] - - if set(currentPermissionsInTable) != set(oldPermissions): - newEntry = { - 'targetEntity' : permissions['targetEntity'], - 'permissions' : [{ - 'permissions' : { - 'permissions' : currentPermissionsInTable - }, - 'targetEntity' : entry['targetEntity'] - }] - } - newPermissionList.append(newEntry) - if len(newPermissionList) == 0: - newPermissionList = None - return newPermissionList - - - def startEditing(self): - self.dlg.setEditState(True) - self.dlg.editCheckBox.clicked.disconnect(self.startEditing) - self.dlg.pushButtonOk.clicked.disconnect(self.dlg.hide) - self.dlg.editCheckBox.clicked.connect(self.stopEditing) - self.dlg.pushButtonOk.clicked.connect(self.saveChanges) - - - def stopEditing(self): - changes = self.getAllChanges() - if len(changes) > 0: - warn = QMessageBox.warning(self.dlg, 'Warning','All changes will be' - ' lost. Continue?', QMessageBox.Cancel, QMessageBox.Ok) - if warn == QMessageBox.Ok: - self.populateSettings() - else: - self.dlg.editCheckBox.setChecked(True) - return - self.stoppedEditing() - - - def saveChanges(self): - changes = self.getAllChanges() - if len(changes) > 0: - conf = QMessageBox.warning(self.dlg, 'Confirm', 'Please confirm you' - ' want to save the changes', QMessageBox.Cancel, QMessageBox.Ok) - if conf == QMessageBox.Ok: - if 'general' in changes: - #'changes' will be sent as the data, therefore append the id - #as a key, so that the data will be recognized - changes['general']['id'] = self.id - responseCode = self.ressource.editApplication(self.id, - changes['general']) - #update the name in the tree view: - if 'name' in changes['general'] and responseCode > 199 and responseCode < 299: - self.name = changes['general']['name'] - self.setText(0, self.name) - - if 'homeview' in changes: - self.ressource.editMapConfig( - self.mapConfigId, changes['homeview']) - - #update the homeview after the changes - self.ressource.updateExtentsAndMapConfigs() - self.homeview = self.ressource.getHomeviewByIds( - self.mapConfigId, self.extentId) - - if 'layerTree' in changes: - deletedItemIds = changes['layerTree']['deleteItems'] - responses = [] - if len(deletedItemIds) > 0: - for id in deletedItemIds: - responses.append(self.ressource.deleteLayerTreeItem(id)) - newItems = changes['layerTree']['newItems'] - if len(newItems) > 0: - for item in newItems: - responses.append(self.ressource.createLayerTreeItem(item)) - changeItems = changes['layerTree']['changeItems'] - if len(changeItems) > 0: - for item in changeItems: - id = item['id'] - responses.append( - self.ressource.updateLayerTreeItem(id, item)) - res = 200 - for x in responses: - if not 199 < x < 210: - res = x - self.ressource.userInfo(res, 'Layertree', 'updated') - - if 'permissions' in changes: - responses = 200 - for x in changes['permissions'].keys(): - for entry in changes['permissions'][x]: - responseBool = self.ressource.editObjectPermission( - self.id, 'ProjectApplication', x, entry) - if not responseBool: - responses = 400 - - self.userPermissions = self.ressource.getObjectPermissions( - self.id, 'Application', 'User') - self.groupPermissions = self.ressource.getObjectPermissions( - self.id, 'Application', 'UserGroup') - self.ressource.userInfo(responses, - 'Application permissions', 'updated') - - #update the class internal copy of the general settings - newSettings = self.ressource.updateSingleApplication(self.id) - for attr in self.settings: - self.settings[attr] = newSettings[attr] - self.dlg.layertreewidget.populateTree(newSettings['layerTree']) - # if the user clicked 'cancel', just return and stay in edit mode: - else: - return - - else: - info = QMessageBox.information(self.dlg, 'Note', - 'No changes were found', QMessageBox.Ok) - self.stoppedEditing() - - - def stoppedEditing(self): - self.dlg.editCheckBox.clicked.disconnect(self.stopEditing) - self.dlg.pushButtonOk.clicked.disconnect(self.saveChanges) - self.dlg.pushButtonOk.clicked.connect(self.dlg.hide) - self.dlg.editCheckBox.clicked.connect(self.startEditing) - self.dlg.editCheckBox.setChecked(False) - self.dlg.setEditState(False) - - - def deleteApplication(self): - txt = 'Please confirm you want to permanently delete ' - txt += 'the selected application in Shogun' - conf = QMessageBox.warning(self.dlg, 'Confirm',txt , QMessageBox.Cancel, QMessageBox.Ok) - if conf == QMessageBox.Ok: - self.ressource.deleteApplication(self.id) - self.ressource.updateApplications() - self.parent().update() - - - def copyApplication(self): - self.ressource.copyApplication(self.id, self.name) - self.parent().update() - - -class LayersItem(TreeItem): - def __init__(self, ressource): - TreeItem.__init__(self, 'layers-logo.png', 'Layers') - self.ressource = ressource - self.layers = self.ressource.getLayerIdsAndNames() - self.layerlist = [] - self.actiontype = 'layersItem' - font = QFont('Arial',10) - font.setBold(True) - self.setFont(0,font) - self.populate() - - def populate(self): - for layer in self.layers: - item = LayerItem(layer[0], layer[1], layer[2], layer[3], self.ressource) - self.addChild(item) - self.layerlist.append(item) - self.sortChildren(0, Qt.AscendingOrder) - - def update(self): - #update the list of current shogun layers: - self.layers = self.ressource.getLayerIdsAndNames(reload = True) - layerIdList = [layer[0] for layer in self.layers] - #remove every item that is not in the updated list: - for layer in self.layerlist: - if layer.id not in layerIdList: - self.removeChild(layer) - self.layerlist.remove(layer) - - #check if new layers appeared and append them: - currentLayerIdList = [layer.id for layer in self.layerlist] - for layer in self.layers: - if layer[0] not in currentLayerIdList: - item = LayerItem(layer[0], layer[1], layer[2], layer[3], self.ressource) - self.addChild(item) - self.layerlist.append(item) - self.sortChildren(0, Qt.AscendingOrder) - - - - def createNewLayer(self, iface): - self.uploadDialog = UploadLayerDialog() - #mapLayers = QgsMapLayerRegistry.instance().mapLayers().values() - #self.uploadDialog.layerBox.addItems([layer.name() for layer in mapLayers]) - self.uploadDialog.uploadButton.clicked.connect(self.uploadLayerAction) - self.uploadDialog.show() - - def uploadLayerAction(self): - layer = self.uploadDialog.layerBox.currentLayer() - # layerutils - pathToZipFile, pathToTempDir = prepareLayerForUpload(layer, self.uploadDialog) - if pathToZipFile: - if layer.type() == QgsMapLayer.VectorLayer: - type = 'Vector' - else: - if layer.providerType() == 'wms': - success = self.ressource.publishWmsLayer() - if success: - self.uploadDialog.log('WMS layer ' + layer.name() + '' - 'was successfully published') - self.update() - else: - self.uploadDialog.log('Publishing WMS ' - 'layer ' + layer.name() + ' was not successfull') - - return - - else: - type = 'Raster' - success = self.ressource.uploadLayer(pathToZipFile, type) - if success: - self.uploadDialog.log('Layer ' + layer.name() + ' was successfully uploaded') - self.update() - else: - self.uploadDialog.log('Uploading layer ' + layer.name() + ' was not successfull') - # after the process has finished we delete the created zipfile and - # temporary directory for cleaning up - try: - os.remove(pathToZipFile) - os.rmdir(pathToTempDir) - except: - pass - - - -class LayerItem(TreeItem): - def __init__(self, id, name, datatype, source, ressource): - - # unfortunately there is no option to retrieve the layer geometry (i. e. - # point/line/polygon for vectorlayers) just from the json object. - # Therefore they get a default polygon icon which will be updated as soon as - # layer is added as WFS to QGIS - see def addQgsLayer() - if datatype == 'Raster': - self.icon = QgsLayerItem.iconRaster() - elif datatype == 'Vector': - self.icon = QgsLayerItem.iconPolygon() - else: - self.icon = QgsLayerItem.iconDefault() - TreeItem.__init__(self, self.icon, name) - self.id = id - self.actiontype = 'layer' - self.ressource = ressource - self.dlg = None - self.name = name - self.source = source - self.datatype = datatype - self.qgisLayers = [] - self.settings = { - 'name' : '', - 'appearance' : { - 'hoverTemplate' : None, - 'opacity' : 1 - } - } - if PYTHON_VERSION >= 3: - QgsProject.instance().layerRemoved.connect(self.updateLayerList) - else: - QgsMapLayerRegistry.instance().layerRemoved.connect(self.updateLayerList) - - - def updateLayerList(self): - if self.qgisLayers == []: - return - if PYTHON_VERSION >= 3: - currentMapLayers = QgsProject.instance().mapLayers() - else: - currentMapLayers = QgsMapLayerRegistry.instance().mapLayers() - for qgisLayerItem in self.qgisLayers: - if qgisLayerItem.id not in currentMapLayers: - self.qgisLayers.remove(qgisLayerItem) - self.removeChild(qgisLayerItem) - - - def addQgsLayer(self, iface): - currentCrs = iface.mapCanvas().mapSettings().destinationCrs() - if currentCrs.isValid(): - epsg = currentCrs.authid() - else: - epsg = 'EPSG:3857' - # layerutils - layer = createLayer(self, epsg) - if not layer: - return - qgisLayerItem = QgisLayerItem(layer, self, self.ressource) - self.qgisLayers.append(qgisLayerItem) - self.addChild(qgisLayerItem) - self.setExpanded(True) - - #for an odd reason there appears a warning below the layer if it is a - #wms layer from shogun - workaround to hide this: - if layer.dataProvider().name() == 'wms' and self.source['url'].startswith('/shogun2-webapp/'): - root = QgsProject.instance().layerTreeRoot() - layerNode = root.findLayer(layer.id()) - layerNode.setExpanded(False) - - # when the layer has loaded for the first time and it's icon - # is still on iconDefault, update the icon with the correct geometry - if not self.datatype == 'Raster': - if layer.type() == QgsMapLayer.VectorLayer: - geom = layer.geometryType() - if PYTHON_VERSION >= 3: - if geom == QgsWkbTypes.PointGeometry: - self.icon = QgsLayerItem.iconPoint() - elif geom == QgsWkbTypes.LineGeometry: - self.icon = QgsLayerItem.iconLine() - elif geom == QgsWkbTypes.PolygonGeometry: - self.icon = QgsLayerItem.iconPolygon() - else: - if geom == QGis.Point: - self.icon = QgsLayerItem.iconPoint() - elif geom == QGis.Line: - self.icon = QgsLayerItem.iconLine() - elif geom == QGis.Polygon: - self.icon = QgsLayerItem.iconPolygon() - else: - self.icon = QgsLayerItem.iconRaster() - self.setIcon(0, self.icon) - - - def createStaticSettings(self): - self.dlg = LayerSettingsDialog() - self.dlg.setEditState(False) - self.dlg.pushButtonOk.clicked.connect(self.normalClose) - self.dlg.editCheckBox.clicked.connect(self.startEditing) - self.dlg.pushButtonCancel.clicked.connect(self.stopEditing) - if self.datatype == 'Vector' or self.datatype == '': - fieldNames = self.ressource.getFieldNamesFromWfs(self.source['layerNames']) - if fieldNames: - self.dlg.hoverBox.addItems(fieldNames) - else: - self.dlg.deactivateHoverEditing() - - - def populateSettings(self): - settings = self.ressource.getLayerAttrsById(self.id) - for attr in self.settings: - self.settings[attr] = settings[attr] - - self.dlg.nameEdit.setText(settings['name']) - - opac = settings['appearance']['opacity'] - if opac is None: - opac = 0 - self.dlg.sliderEdit.setText(str(opac)) - self.dlg.slider.setValue(int(opac * 100)) - if settings['appearance']['hoverTemplate'] is not None: - self.dlg.hoverEdit.setText(settings['appearance']['hoverTemplate']) - - self.userPermissions = self.ressource.getObjectPermissions(self.id, 'Layer', 'User') - self.groupPermissions = self.ressource.getObjectPermissions(self.id, 'Layer', 'UserGroup') - - if not self.userPermissions['success'] or not self.groupPermissions['success']: - self.dlg.noPermissionAccess() - else: - self.dlg.populateTable('users', self.userPermissions['data']['permissions']) - self.dlg.populateTable('groups', self.groupPermissions['data']['permissions']) - - - def getPermissionChanges(self, type): - if type == 'User': - permissions = self.userPermissions['data'] - table = self.dlg.usertabel - elif type == 'UserGroup': - permissions = self.groupPermissions['data'] - table = self.dlg.groupstabel - else: - return - oldPermissionList = permissions['permissions'] - newPermissionList = [] - for entry in oldPermissionList: - name = entry['displayTitle'] - row = None - for x in range(table.rowCount()): - if table.verticalHeaderItem(x).text() == name: - row = x - break - if row is None: - return - - currentPermissionsInTable = [] - for p in enumerate(['READ', 'UPDATE', 'DELETE']): - if table.item(row, p[0]).checkState() == 2: - currentPermissionsInTable.append(p[1]) - - if entry['permissions'] == None: - oldPermissions = [] - else: - oldPermissions = entry['permissions']['permissions'] - if oldPermissions == ['ADMIN']: - oldPermissions = ['READ', 'UPDATE', 'DELETE'] - - if set(currentPermissionsInTable) != set(oldPermissions): - newEntry = { - 'targetEntity' : permissions['targetEntity'], - 'permissions' : [{ - 'permissions' : { - 'permissions' : currentPermissionsInTable - }, - 'targetEntity' : entry['targetEntity'] - }] - } - newPermissionList.append(newEntry) - if len(newPermissionList) == 0: - newPermissionList = None - return newPermissionList - - - def getAllChanges(self): - changes = {} - generalChanges = self.getGeneralChanges() - if generalChanges is not None: - changes['general'] = generalChanges - userPermissionChanges = self.getPermissionChanges('User') - if userPermissionChanges is not None: - changes['permissions'] = {'User' : userPermissionChanges} - groupPermissionChanges = self.getPermissionChanges('UserGroup') - if groupPermissionChanges is not None: - if not 'permissions' in changes: - changes['permissions'] = {} - changes['permissions']['UserGroup'] = groupPermissionChanges - - if len(changes) == 0: - return None - else: - return changes - - - def getGeneralChanges(self): - changes = {} - opacity = self.dlg.sliderEdit.text() - if self.dlg.nameEdit.text() != self.settings['name']: - changes['name'] = self.dlg.nameEdit.text() - if type(opacity) == float or type(opacity) == int: - if float(opacity) != self.settings['appearance']['opacity']: - changes['appearance'] = {} - changes['appearance']['opacity'] = float(opacity) - hoverInput = self.dlg.hoverEdit.text() - if len(hoverInput) == 0: - hoverInput = None - if hoverInput != self.settings['appearance']['hoverTemplate']: - if not 'appearance' in changes: - changes['appearance'] = {} - changes['appearance']['hoverTemplate'] = self.dlg.hoverEdit.text() - - if len(changes) == 0: - return None - else: - return changes - - def startEditing(self): - self.dlg.setEditState(True) - self.dlg.editCheckBox.clicked.disconnect(self.startEditing) - self.dlg.pushButtonOk.clicked.disconnect(self.normalClose) - self.dlg.editCheckBox.clicked.connect(self.stopEditing) - self.dlg.pushButtonOk.clicked.connect(self.saveChanges) - - def stopEditing(self): - changes = self.getAllChanges() - if changes is not None: - warn = QMessageBox.warning(self.dlg, 'Warning','All changes will be' - ' lost. Continue?', QMessageBox.Cancel, QMessageBox.Ok) - if warn == QMessageBox.Ok: - self.populateSettings() - else: - self.dlg.editCheckBox.setChecked(True) - return - self.stoppedEditing() - - def saveChanges(self): - changes = self.getAllChanges() - if changes is not None: - conf = QMessageBox.warning(self.dlg, 'Confirm','Please confirm you ' - 'want to save the changes', QMessageBox.Cancel, QMessageBox.Ok) - if conf == QMessageBox.Ok: - if 'general' in changes: - data = changes['general'] - data['id'] = self.id - responseCode = self.ressource.editLayer(self.id, data) - if 'name' in data and responseCode > 199 and responseCode < 299: - self.name = data['name'] - self.setText(0, self.name) - - #update the settings of the class: - newSettings = self.ressource.updateSingleLayer(self.id) - for attr in self.settings: - self.settings[attr] = newSettings[attr] - if 'permissions' in changes: - responses = 200 - for x in changes['permissions'].keys(): - for entry in changes['permissions'][x]: - responseBool = self.ressource.editObjectPermission( - self.id, 'ProjectLayer', x, entry) - if not responseBool: - responses = 400 - - # update the class variable copy of the permissions - self.userPermissions = self.ressource.getObjectPermissions( - self.id, 'Layer', 'User') - self.groupPermissions = self.ressource.getObjectPermissions( - self.id, 'Layer', 'UserGroup') - self.ressource.userInfo(responses, 'Layer permissions', 'updated') - - # if the user clicked 'cancel' just return and stay in edit mode: - else: - return - else: - QMessageBox.information(self.dlg, 'Note','No changes were found', - QMessageBox.Ok) - self.stoppedEditing() - - def stoppedEditing(self): - self.dlg.editCheckBox.clicked.disconnect(self.stopEditing) - self.dlg.pushButtonOk.clicked.disconnect(self.saveChanges) - self.dlg.pushButtonOk.clicked.connect(self.normalClose) - self.dlg.editCheckBox.clicked.connect(self.startEditing) - self.dlg.editCheckBox.setChecked(False) - self.dlg.setEditState(False) - - def normalClose(self): - self.dlg.hide() - - def deleteLayer(self): - txt = 'Please confirm you want to permanently delete the selected layer in Shogun' - conf = QMessageBox.warning(self.dlg, 'Confirm',txt , QMessageBox.Cancel, QMessageBox.Ok) - if conf == QMessageBox.Ok: - self.ressource.deleteLayer(self.id) - self.parent().update() - - - -class QgisLayerItem(TreeItem): - def __init__(self, qgislayer, shogunLayerItem, ressource): - TreeItem.__init__(self, None, qgislayer.name()) - self.layer = qgislayer - self.id = self.layer.id() - self.type = self.layer.type() - self.stylename = None - #make upload and download Style only available for vector layers: - if self.type == 0: - self.actiontype = 'qgisLayerReference' - else: - self.actiontype = None - self.parentShogunLayer = shogunLayerItem - self.ressource = ressource - if PYTHON_VERSION >= 3: - QgsProject.instance().addMapLayer(self.layer) - else: - QgsMapLayerRegistry.instance().addMapLayer(self.layer) - if self.layer.type() == QgsMapLayer.VectorLayer: - self.downloadStyle() - self.layer.nameChanged.connect(self.on_name_changed) - - - def uploadStyle(self): - msg = 'Please confirm that you want to upload the style of the selected' - msg += ' layer and overwrite the style of the corresponding layer in Shogun' - confirm = QMessageBox.warning(None, 'Confirm', msg, QMessageBox.Cancel, QMessageBox.Ok) - if not confirm: - return - else: - return self.ressource.uploadStyle(self) - - - def downloadStyle(self): - sld, geoServerStyleName = self.ressource.downloadStyle(self) - self.stylename = geoServerStyleName - if sld is not None: - self.layer.loadSldStyle(sld) - self.layer.triggerRepaint() - return True - else: - return False - - - def uploadIcon(self): - self.ressource.uploadCustomIcon(self.layer.rendererV2().symbol().symbolLayer(0)) - - def on_name_changed(self): - self.setText(0, self.layer.name()) diff --git a/src/shoguneditor/icons/applications-logo.png b/src/shoguneditor/icons/applications-logo.png deleted file mode 100644 index 7fb8a5c..0000000 Binary files a/src/shoguneditor/icons/applications-logo.png and /dev/null differ diff --git a/src/shoguneditor/icons/info.txt b/src/shoguneditor/icons/info.txt deleted file mode 100644 index 24b43c8..0000000 --- a/src/shoguneditor/icons/info.txt +++ /dev/null @@ -1 +0,0 @@ -Recompile resources.py with pyrcc and resources.qrc here, then replace the file resources.py in the parent directory diff --git a/src/shoguneditor/icons/layers-logo.png b/src/shoguneditor/icons/layers-logo.png deleted file mode 100644 index ab977d1..0000000 Binary files a/src/shoguneditor/icons/layers-logo.png and /dev/null differ diff --git a/src/shoguneditor/icons/resources.qrc b/src/shoguneditor/icons/resources.qrc deleted file mode 100644 index 529cd86..0000000 --- a/src/shoguneditor/icons/resources.qrc +++ /dev/null @@ -1,8 +0,0 @@ - - - applications-logo.png - layers-logo.png - shogun-logo-25x-25px.png - shogun-logo-50x50px-round-blue.png - - diff --git a/src/shoguneditor/icons/shogun-logo-25x-25px.png b/src/shoguneditor/icons/shogun-logo-25x-25px.png deleted file mode 100644 index e0f5d30..0000000 Binary files a/src/shoguneditor/icons/shogun-logo-25x-25px.png and /dev/null differ diff --git a/src/shoguneditor/icons/shogun-logo-50x50px-round-blue.png b/src/shoguneditor/icons/shogun-logo-50x50px-round-blue.png deleted file mode 100644 index 8fda6c7..0000000 Binary files a/src/shoguneditor/icons/shogun-logo-50x50px-round-blue.png and /dev/null differ diff --git a/src/shoguneditor/layerutils.py b/src/shoguneditor/layerutils.py deleted file mode 100644 index 26fbea3..0000000 --- a/src/shoguneditor/layerutils.py +++ /dev/null @@ -1,473 +0,0 @@ -# -*- coding: utf-8 -*- -''' -(c) 2018 terrestris GmbH & Co. KG, https://www.terrestris.de/en/ -+ the last method "getLabelingAsSld()" was taken -+ from the qgis geoserver explorer plugin by -+ (C) 2016 Boundless, http://boundlessgeo.com -+ see: https://github.com/boundlessgeo/qgis-geoserver-plugin -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys -import os.path -import zipfile -import tempfile - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtWidgets import QMessageBox, QDialog, QLabel, QPushButton - from qgis.PyQt.QtCore import QRect - from qgis.PyQt.QtXml import QDomDocument - import urllib.request, urllib.parse -else: - from PyQt4.QtGui import QMessageBox, QDialog, QLabel, QPushButton - from PyQt4.QtCore import QRect - from PyQt4.QtXml import QDomDocument - import urllib - from qgis.core import QGis - -from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsMapLayer, QgsCoordinateReferenceSystem -from qgis.core import QgsVectorFileWriter, QgsRasterFileWriter, QgsRasterPipe - -from shoguneditor.gui.dialog_bases.addraster import AddRasterDialog - -PYTHON_VERSION = sys.version_info[0] - -'''This module contains some helper functions for the shogun-editor plugin''' - -def createLayer(layerItem, epsg): - #Shogun Webapp saves all layers in the following espg, therefore the - #variable can be static - layerurl = layerItem.source['url'] - - dataType = layerItem.datatype - - # every layerItem.source should have an attribute 'dataType' - if dataType == 'vector' or dataType == 'Vector': - url = layerItem.ressource.baseurl.rstrip('/shogun2-webapp/') + layerurl + '?' - return createWfsLayer(layerItem, url, epsg) - - elif dataType == 'Raster': - url = layerItem.ressource.baseurl.rstrip('/shogun2-webapp/') + layerurl + '?' - return createRasterLayer(layerItem, url, epsg) - - elif dataType == 'WMS': - if layerurl == '/shogun2-webapp/geoserver.action': - url = layerItem.ressource.baseurl.rstrip('/shogun2-webapp/') + layerurl + '?' - return createWmsLayerFromShogun(layerItem, url) - else: - return createWmsLayer(layerItem, layerurl, epsg) - - # if for any reason the parameter 'dataType' is not set correctly, we check the url - # of the layer to determine if it's a WFS/WCS from the shogun-geoserver - # (url has'shogun2-webapp') or if it's a WMS from an outer source (other url) - elif dataType == 'unknown' or dataType == None or dataType == '': - if layerurl.startswith('/shogun2-webapp'): - url = layerItem.ressource.baseurl.rstrip('/shogun2-webapp/') + layerurl + '?' - try: - lyr = createWfsLayer(layerItem, url, epsg) - if lyr.isValid(): - return lyr - except: - pass - try: - lyr = createWmsLayerFromShogun(layerItem, url, epsg) - if lyr.isValid(): - return lyr - except: - pass - try: - lyr = createRasterLayer(layerItem, url, epsg) - if lyr.isValid(): - return lyr - except: - pass - else: - return createWmsLayerNormal(layerItem, layerurl, epsg) - - else: - info = 'Layer source '+ layerurl + ' could not be loaded' - QMessageBox.warning(None, 'Warning', info, QMessageBox.Ok) - - - -def createWfsLayer(layerItem, url, epsg): - params = { - 'service': 'WFS', - 'srsname': epsg, - 'typename': layerItem.source['layerNames'], - 'typenames': layerItem.source['layerNames'], - 'url': url + 'typename=' + layerItem.source['layerNames'], - 'version': 'auto', - 'request': 'GetCapabilities' - } - if PYTHON_VERSION >= 3: - url = url + urllib.parse.unquote(urllib.parse.urlencode(params)) - else: - url = url + urllib.unquote(urllib.urlencode(params)) - - layer = QgsVectorLayer(url, 'SHOGUN-Layer: ' + layerItem.name, 'WFS') - return layer - - -def createRasterLayer(layerItem, url, epsg): - - dlg = AddRasterDialog() - dlg.show() - userSelection = dlg.exec_() - - if userSelection == 0: - return False - - elif userSelection == 1: - return createWmsLayerFromShogun(layerItem, url, epsg) - - elif userSelection == 2: - - #workaround... - ## TODO: can this be made using the qgis wcs provider? - layerName = layerItem.source['layerNames'] - - params = { - 'request' : 'GetCoverage', - 'version' : '2.0.1', - 'coverageId' : layerName, - 'namespace' : 'SHOGUN', - 'identifier' : layerName - } - - #build the fitting url from the baseurl ('http...geoserver.action?') - #and the parameters - baseurl = url + 'service=WCS&' - if PYTHON_VERSION >= 3: - url = baseurl + urllib.parse.urlencode(params) - else: - url = baseurl + urllib.urlencode(params) - - #urlretrieve stores the downloaded file in /tmp/ and returns it's path: - if PYTHON_VERSION >= 3: - response = urllib.request.urlretrieve(url) - else: - response = urllib.urlretrieve(url) - file = response[0] - #create a raster layer and return it - return QgsRasterLayer(file, 'QGIS-Layer: ' + layerItem.name) - - -def createWmsLayerNormal(layerItem, url, epsg): - params = { - 'crs': epsg, - 'dpiMode': '7', - 'format': 'image/png', - 'layers': layerItem.source['layerNames'], - 'styles': '', - 'url': url - } - - if PYTHON_VERSION >= 3: - uri = urllib.parse.urlencode(params) - else: - uri = urllib.urlencode(params) - - layer = QgsRasterLayer(uri, 'QGIS-Layer: ' + layerItem.name, 'wms') - if layer.isValid(): - return layer - else: - return False - - - -def createWmsLayerFromShogun(layerItem, url, epsg): - layerNames = layerItem.source['layerNames'] - params = { - 'IgnoreGetMapUrl' : 1, - 'crs': epsg, - 'format': 'image/png', - 'layers': layerNames.split(':')[1], - 'styles': '', - 'url': url + 'layers=' + layerNames - } - - if PYTHON_VERSION >= 3: - uri = urllib.parse.urlencode(params) - else: - uri = urllib.urlencode(params) - layer = QgsRasterLayer(uri, 'QGIS-Layer: ' + layerItem.name, 'wms') - if layer.isValid(): - return layer - else: - return False - - -def createAndParseSld(qgisLayerItem): - document = QDomDocument() - header = document.createProcessingInstruction( 'xml', 'version=\'1.0\' encoding=\'UTF-8\'' ) - document.appendChild( header ) - root = document.createElementNS( 'http://www.opengis.net/sld', 'StyledLayerDescriptor' ) - root.setAttribute( 'version', '1.0.0' ) - root.setAttribute( 'xmlns:ogc', 'http://www.opengis.net/ogc' ) - root.setAttribute( 'xmlns:sld', 'http://www.opengis.net/sld' ) - root.setAttribute( 'xmlns:gml', 'http://www.opengis.net/gml' ) - document.appendChild( root ) - - namedLayerNode = document.createElement( 'sld:NamedLayer' ) - root.appendChild( namedLayerNode ) - - - qgisLayerItem.layer.writeSld(namedLayerNode, document, '') - - nameNode = namedLayerNode.firstChildElement('se:Name') - oldNameText = nameNode.firstChild() - newname = qgisLayerItem.parentShogunLayer.source['layerNames'] - newNameText = document.createTextNode(newname) - nameNode.appendChild(newNameText) - nameNode.removeChild(oldNameText) - - userStyleNode = namedLayerNode.firstChildElement('UserStyle') - userStyleNameNode = userStyleNode.firstChildElement('se:Name') - userStyleNameText = userStyleNameNode.firstChild() - userStyleNameNode.removeChild(userStyleNameText) - userStyleNameNode.appendChild(document.createTextNode(qgisLayerItem.stylename)) - - titleNode = document.createElement('sld:Title') - title = document.createTextNode('A QGIS-Style for '+qgisLayerItem.layer.name()) - titleNode.appendChild(title) - userStyleNode.appendChild(titleNode) - defaultNode = document.createElement('sld:IsDefault') - defaultNode.appendChild(document.createTextNode('1')) - userStyleNode.appendChild(defaultNode) - - featureTypeStyleNode = userStyleNode.firstChildElement('se:FeatureTypeStyle') - featureTypeStyleNameNode = document.createElement('sld:Name') - featureTypeStyleNameNode.appendChild(document.createTextNode('name')) - featureTypeStyleNode.appendChild(featureTypeStyleNameNode) - - rules = featureTypeStyleNode.elementsByTagName('se:Rule') - for x in range(rules.length()): - rule = rules.at(x) - rule.removeChild(rule.firstChildElement('se:Description')) - - # Check if custom icons are used in symbology and replace the text: - # search if tag 'se:OnlineResource' is in the sld document - listOfGraphics = rule.toElement().elementsByTagName('se:OnlineResource') - if not listOfGraphics.isEmpty(): - for x in range(listOfGraphics.length()): - graphicNode = listOfGraphics.at(x) - currentIcon = graphicNode.attributes().namedItem('xlink:href').nodeValue() - iconUrl = qgisLayerItem.ressource.prepareIconForUpload(currentIcon) - graphicNode.toElement().setAttribute('xlink:href', iconUrl) - graphicNode.toElement().setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink') - - sld = document.toString() - - # in qgis3 layer.writeSld() also incluedes labeling in the output sld, - # whereas in qgis2 we have to do this manually by using this module's function - # getLabelingAsSld - ## TODO: The automatic sld labeling from QGIS 3 produces an extra rule for - # every labeling style, thus leading to a less beautiful viewe in the - # shogun2-webapp styler - is this a problem? - - if qgisLayerItem.layer.labelsEnabled() and PYTHON_VERSION < 3: - labelSld = getLabelingAsSld(qgisLayerItem.layer) - sld = sld.replace('', labelSld + '') - - sld = sld.replace('ogc:Filter xmlns:ogc="http://www.opengis.net/ogc"', 'ogc:Filter') - - # the following fixes weird problems with the sld compability with the - # shogun webapp - sld = sld.replace('', '') - sld = sld.replace('', '') - sld = sld.replace(' ', '') - sld = sld.replace(' ', '') - - sld = sld.replace('StyledLayerDescriptor', 'sld:StyledLayerDescriptor') - sld = sld.replace('UserStyle', 'sld:UserStyle') - sld = sld.replace('se:', 'sld:') - sld = sld.replace('SvgParameter', 'CssParameter') - sld = sld.replace('\n', '') - sld = sld.replace('\t', '') - - return sld - - -def prepareLayerForUpload(layer, uploadDialog): - tmpdir = tempfile.mkdtemp() - zipfilePath = os.path.join(tmpdir, 'uploadzip.zip') - uploadDialog.log('Writing layer as shapefile...') - - if layer.type() == QgsMapLayer.VectorLayer: - if layer.source().endswith('.shp'): - file = layer.source() - uploadDialog.log('...success\nZipping...') - zipSuccess = createZipFromShapefile(file, zipfilePath, delete = False) - else: - path = os.path.join(tmpdir, 'VectorlayerFromQGisPlugin.shp') - file = writeShapefile(layer, path) - if not file: - uploadDialog.log('Error: Could not write the shapefile ') - return - uploadDialog.log('...success\nZipping...') - zipSuccess = createZipFromShapefile(file, zipfilePath, delete = True) - - else: - if layer.providerType() == 'wms': - return True - if layer.source().endswith('.tif'): - rasterfile = layer.source() - uploadDialog.log('Zipping...') - zipSuccess = createZipFromRaster(rasterfile, zipfilePath, delete = False) - else: - path = os.path.join(tmpdir, 'RasterlayerFromQGisPlugin.tif') - uploadDialog.log('Creating raster file from layer...') - rasterfile = writeRasterFile(layer, path) - uploadDialog.log('Zipping...') - if not rasterfile: - uploadDialog.log('Error: Could not write the raster file') - return - zipSuccess = createZipFromRaster(rasterfile, zipfilePath, delete = True) - uploadDialog.log(zipSuccess) - return zipfilePath, tmpdir - - -def writeShapefile(layer, path): - ## TODO: here are some problems with the upload - also shogun-problems - - writeError = QgsVectorFileWriter.writeAsVectorFormat( - layer, path,'utf-8', layer.crs(), 'ESRI Shapefile', False) - if PYTHON_VERSION >= 3: - if writeError[0] != 0: # in QGIS 3 writeAsVectorFormat returns a tuple - return False - else: - if writeError != 0: #0 = writing success - return False - return path - - -def createZipFromShapefile(filepath, zipfilePath, delete=False): - # create a list with '/path/name.shp' as the first item - shapefileParts = [filepath] - for extension in ['.dbf', '.shx', '.prj']: - newFilename = os.path.splitext(filepath)[0] + extension - if os.path.isfile(newFilename): - # append the further shapefile parts to the list - shapefileParts.append(newFilename) - else: - return 'Error: Could not find ' + os.path.basename(newFilename) - - # create a new zip-archive at zipfilePath - newZipfile = zipfile.ZipFile(zipfilePath, 'w', zipfile.ZIP_DEFLATED) - for part in shapefileParts: - # add the parts of the shapefile to the zip-archive - newZipfile.write(part, arcname = os.path.basename(part)) - # then delete the parts because we do not need them anymore - if delete: - os.remove(part) - newZipfile.close() - - # delete further created files belonging to the shapefile, because we - # do not need them - if delete: - for extension in ['.cpg', '.qpj']: - newFilename = os.path.splitext(filepath)[0] + extension - if os.path.isfile(newFilename): - os.remove(newFilename) - return 'Written successfully' - - -def writeRasterFile(layer, filepath): - pipe = QgsRasterPipe() - provider = layer.dataProvider() - pipe.set(provider.clone()) - - writer = QgsRasterFileWriter(filepath) - writeError = writer.writeRaster(pipe, provider.xSize(), provider.ySize(), - provider.extent(), provider.crs()) - - return filepath - - -def createZipFromRaster(pathToRaster, zipfilePath, delete = False): - newZipfile = zipfile.ZipFile(zipfilePath, 'w', zipfile.ZIP_DEFLATED) - newZipfile.write(pathToRaster, arcname = os.path.basename(pathToRaster)) - newZipfile.close() - if delete: - try: - os.remove(pathToRaster) - except: - pass - return 'Written successfully' - - - -#the following function (we need it only when the plugin is used on qgis2): -# (c) 2016 Boundless, http://boundlessgeo.com -# This code is licensed under the GPL 2.0 license. -def getLabelingAsSld(layer): - SIZE_FACTOR = 4 - try: - s = "" - s += "" + layer.customProperty("labeling/fieldName") + "" - s += "" - r = int(layer.customProperty("labeling/textColorR")) - g = int(layer.customProperty("labeling/textColorG")) - b = int(layer.customProperty("labeling/textColorB")) - rgb = '#%02x%02x%02x' % (r, g, b) - s += '' + rgb + "" - s += "" - s += '' + layer.customProperty("labeling/fontFamily") +'' - s += ('' + - str(int(layer.customProperty("labeling/fontSize"))) - +'') - - italic = False - bold = False - if layer.customProperty("labeling/fontItalic") == "true": - s += 'italic' - italic = True - if layer.customProperty("labeling/fontBold") == "true": - bold = True - s += 'bold' - if not italic and not bold: - s += 'normal' - s += 'normal' - s += "" - s += "" - if layer.geometryType() == QGis.Point: - s += ("" - "" - "0.5" - "0.5" - "") - s += "" - s += "" + str(int(layer.customProperty("labeling/xOffset"))) + "" - s += "" + str(int(layer.customProperty("labeling/yOffset"))) + "" - s += "" - s += "" + str(abs(int(layer.customProperty("labeling/angleOffset")))) + "" - s += "" - elif layer.geometryType() == QGis.Line: - mode = layer.customProperty("labeling/placement") - if mode != 4: - follow = 'true' if mode == 3 else '' - s += ''' - - %s - - - %s''' % (str(layer.customProperty("labeling/dist")), follow) - s += "" - - if layer.customProperty("labeling/bufferDraw") == "true": - r = int(layer.customProperty("labeling/bufferColorR")) - g = int(layer.customProperty("labeling/bufferColorG")) - b = int(layer.customProperty("labeling/bufferColorB")) - rgb = '#%02x%02x%02x' % (r, g, b) - haloSize = str(layer.customProperty("labeling/bufferSize")) - opacity = str(float(layer.customProperty("labeling/bufferColorA")) / 255.0) - s += "%s" % haloSize - s += '%s' % rgb - s += '%s' % opacity - s +="" - return s - except: - return "" diff --git a/src/shoguneditor/license.txt b/src/shoguneditor/license.txt deleted file mode 100644 index 1f963da..0000000 --- a/src/shoguneditor/license.txt +++ /dev/null @@ -1,340 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. - diff --git a/src/shoguneditor/logo-with-tag.png b/src/shoguneditor/logo-with-tag.png deleted file mode 100644 index c6d1470..0000000 Binary files a/src/shoguneditor/logo-with-tag.png and /dev/null differ diff --git a/src/shoguneditor/metadata.txt b/src/shoguneditor/metadata.txt deleted file mode 100644 index da4f17e..0000000 --- a/src/shoguneditor/metadata.txt +++ /dev/null @@ -1,37 +0,0 @@ -# Mandatory items: - -[general] -name=Shogun Editor -qgisMinimumVersion=2.14 -qgisMaximumVersion=3.99 -description=A QGIS plugin to connect with a Shogun GIS client instance on a remote or local server and edit it's content from QGIS. -version=0.3 -author=J. Grieb -email=jgrieb@terrestris.de, info@terrestris.de - -about=A QGIS plugin to connect with a Shogun GIS client instance on a remote or local server and edit it's content from QGIS. Supports editing and creating layers and applications, upload new styles from QGIS to Shogun and more. It's funcionalities are in some way similar to the Geoserver Exploerer plugin, but instead of accessing the base geoserver it only interacts with the Shogun2-Webapp client (RESTful) interface. - -tracker=https://github.com/terrestris/qgis-shogun-editor/issues -repository=https://github.com/terrestris/qgis-shogun-editor -# End of mandatory metadata - -# Recommended items: - -changelog= - 0.2: - - Migration to QGIS 3, add layer tree editing - 0.3: - - Supports QGIS 3.5 - 3.8 - - Bugfixes - -# Tags are comma separated with spaces allowed -tags=python, shogun, rest, wfs, wms, sld - -homepage=https://www.terrestris.de/en/ -category=Web -icon=logo-with-tag.png -# experimental flag -experimental=True - -# deprecated flag (applies to the whole plugin, not just a single version) -deprecated=False diff --git a/src/shoguneditor/resources2.py b/src/shoguneditor/resources2.py deleted file mode 100644 index 67eba30..0000000 --- a/src/shoguneditor/resources2.py +++ /dev/null @@ -1,529 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt4 (Qt v4.8.7) -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore - -qt_resource_data = "\ -\x00\x00\x03\x12\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x19\x00\x00\x00\x19\x08\x06\x00\x00\x00\xc4\xe9\x85\x63\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ -\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x06\x07\x38\x0b\x85\x11\xe0\x13\x00\x00\x00\x19\x74\x45\ -\x58\x74\x43\x6f\x6d\x6d\x65\x6e\x74\x00\x43\x72\x65\x61\x74\x65\ -\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x57\x81\x0e\x17\x00\ -\x00\x02\x7a\x49\x44\x41\x54\x48\xc7\xad\x96\x4f\x48\x14\x51\x1c\ -\xc7\x3f\xef\xcf\x8e\xeb\xb2\x42\x12\x29\x88\xd8\x2a\x06\x51\xe0\ -\xc5\xa0\x63\x14\x9e\x02\x2f\x99\xe7\xed\xd2\xa1\x83\x9e\xba\x75\ -\xf1\xd0\x31\x08\xdc\xba\x54\x07\x17\xba\xa5\x10\x49\xd8\x29\x3a\ -\x75\xca\x88\x8e\x85\x29\x59\x8b\x08\x91\x58\x64\xee\xec\x7b\xaf\ -\x43\x6f\x74\xc6\x66\x9b\x55\xf6\x07\xc3\xc0\x9b\xdf\xef\xf7\x9d\ -\xf7\xfd\xfd\x15\x67\xaf\xde\xa4\x89\x94\x81\x71\x60\x14\xe8\x03\ -\x82\x14\x9d\x3a\x50\x03\x96\x81\x45\xa0\x9a\xe6\x48\x36\x71\xbe\ -\x0a\xcc\x01\x13\x40\xa9\x09\x00\xfe\xbc\xe4\xf5\xe6\xbc\x5d\x39\ -\x0b\x64\xd6\x2b\x97\x38\x9a\x94\xbc\xfd\x6c\x33\x90\x59\x60\x8a\ -\xf6\xc8\x54\x1c\x48\xc7\x28\x4a\x00\x58\x63\x30\x0e\xc4\x21\x3c\ -\x0b\xa5\x50\x22\x01\xb4\x0c\x54\x23\x90\x99\xb8\xb2\x09\x1d\xfd\ -\xa7\x86\x18\x28\x6a\xac\x6b\x15\xa2\xc1\xe6\xfa\x3a\x2b\x5b\x06\ -\x2d\xf7\x90\x66\x22\x90\x72\x3c\x06\x66\x37\xe4\xf4\x95\x32\xd5\ -\xc9\x61\xbe\x6d\xed\x20\x44\x6b\x77\x11\xb2\x83\x2e\x53\xe3\xd6\ -\xed\x47\xbc\xdc\x84\x9c\xdc\x8b\x51\x59\xfb\x34\xf5\xe2\x68\xd8\ -\x1c\xc3\xfd\x3d\xac\xbe\x78\xc8\xa5\x7b\x1f\x29\xe4\x65\x36\x80\ -\x70\x38\xd9\xcb\xdd\xca\x0d\x4e\xf6\x16\x60\xe3\x17\xec\xdf\x66\ -\x5c\xfb\x3a\x48\x88\x71\x0e\xa7\xf2\x1c\xeb\x2a\x50\xe8\x90\xb8\ -\x4c\x10\x40\x06\x04\x16\xec\xbf\xfc\x8e\x4a\x5f\x68\x29\x96\xd1\ -\xdd\xb2\xc5\x45\x5a\xe9\xcc\xf6\xc9\xff\x14\x5a\xbb\x24\x90\xed\ -\xf0\x92\x95\x1a\x3a\xed\xf2\x7f\x5f\x8e\x86\xb1\x18\xd3\x0a\x8a\ -\x03\x67\x9b\x52\xab\x53\xff\xcb\x3a\x4e\x0c\x9d\x61\x72\xac\x9b\ -\x20\x27\x5b\x8b\x8c\xe8\x66\xa8\xe0\x78\xdf\x1a\x08\x60\x2d\x3d\ -\x23\x17\xb9\x33\x72\x48\xde\x7e\xd4\x98\x77\xe9\x5d\xb8\x9e\x4c\ -\x47\xc3\xcf\xb0\x7e\xa4\xd8\xb8\xb0\xc1\xce\x6e\x03\x97\x0c\x52\ -\x5d\xfb\x79\x50\x8a\xa8\x0a\x3a\xe0\xf5\xd3\xe7\x54\xc4\x79\x06\ -\xf2\x02\x0b\x38\x0b\xc5\xe3\x7d\x5c\x18\x1d\xa0\xe8\x1d\xd8\xed\ -\xaf\x3c\x7b\xb3\x4e\xdd\xec\xd7\xca\xc6\x87\x77\xbc\x5a\xf9\x4d\ -\x4e\x25\x08\xaa\x69\xdf\xc4\x4a\xb1\x2e\x07\xdb\x9f\xb9\xff\xe0\ -\xd3\x5e\xdf\x0a\x9d\x62\x6c\xf2\x3a\x97\xcf\xc5\x28\x28\x36\x58\ -\x7a\xfc\x84\xa5\xcd\x10\x15\x9d\x29\x4d\x3e\xd0\x07\xb3\x6d\x59\ -\xfb\x89\x36\x71\xa0\x9d\xd2\x99\x57\xfb\x21\x42\xb1\xb1\xf2\x96\ -\xca\xfc\x17\x94\x71\x38\x01\x32\xfc\x4e\x2d\xcc\x51\xec\x94\x64\ -\xd4\xc1\xa2\xf0\xe3\x77\x35\x6b\x50\x39\x6b\x31\xce\x25\xb2\x50\ -\x29\x99\x55\x23\x6b\xc0\xa0\x4c\x6b\xf5\xe9\x5d\x56\xa2\x95\x8a\ -\x3d\xb2\x95\x59\x33\x13\x9f\x8c\x55\xa0\xd2\xe6\x76\x52\x89\x16\ -\x8b\x38\x9d\xd3\x6d\x04\xaa\x78\x7f\xa9\x8b\xc4\x34\x70\xcd\x73\ -\x79\x14\x59\xf3\xf6\xd3\x59\x2b\x51\x15\x18\xf4\xca\x0b\xde\xb0\ -\x59\x75\xd6\xfd\xf7\x05\xaf\x3f\x98\xb6\x7b\xfd\x01\x3c\xde\xc0\ -\xc7\xae\x7e\x85\x10\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ -\x82\ -\x00\x00\x03\xd4\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x19\x00\x00\x00\x19\x08\x06\x00\x00\x00\xc4\xe9\x85\x63\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ -\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x06\x07\x38\x21\x5e\xaa\x29\xc5\x00\x00\x00\x19\x74\x45\ -\x58\x74\x43\x6f\x6d\x6d\x65\x6e\x74\x00\x43\x72\x65\x61\x74\x65\ -\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x57\x81\x0e\x17\x00\ -\x00\x03\x3c\x49\x44\x41\x54\x48\xc7\xad\x96\x4d\x68\x54\x57\x14\ -\xc7\x7f\xf7\xbe\x9b\x79\x33\x19\x33\x33\x3a\x49\x2c\x49\x63\x27\ -\xb1\x28\xa2\x5d\x48\xa4\xdd\x94\x42\xa0\xd0\x6e\x82\xb4\x69\x97\ -\x65\xb4\x90\x45\x17\x89\x08\xa5\x75\x23\x66\x55\xd0\x4d\x69\x02\ -\xdd\xfa\x01\xb6\x5d\xd6\xb6\x9b\x2e\xac\xcb\x22\x4d\x52\x14\x51\ -\x5b\xc5\x24\x33\x25\x8c\xe2\x24\xea\xc4\xc4\x99\xf7\xde\xbd\x2e\ -\xe6\xa6\xce\x64\xbe\x82\xe6\xc0\x83\xc7\xcc\x39\xf7\xc7\xf9\x9f\ -\x73\xde\xb9\x22\x39\x34\x4a\x03\x4b\x03\xc3\xc0\x20\xd0\x03\x84\ -\xea\xf8\x94\x80\x45\x60\x06\xf8\x15\x38\x5f\xef\x20\xd9\xe0\xf0\ -\x39\xe0\x1c\x30\x02\xa4\x1a\x00\xb0\xbf\xa7\xac\xdf\x39\x1b\x97\ -\x6e\x05\x99\xb4\xce\x29\x5e\xce\x52\x36\x7e\xb2\x11\x64\x12\x18\ -\x63\x6b\x6c\xac\x12\x24\x2b\x24\xda\x2a\x40\x25\x28\x0d\x20\x6c\ -\xe1\xe7\xca\xa9\x1a\x02\x5f\x63\x00\x21\x24\x42\x68\xb4\x06\xc4\ -\x26\x8e\xd4\x20\x94\x83\x53\xed\x3b\x0f\xf4\x2b\x4b\x2b\x03\x74\ -\x94\xc1\x77\xf6\x90\x6a\x87\xbb\xff\xdc\x21\xdf\xd1\xcb\xa1\x54\ -\x1c\x27\x30\x98\xa6\x04\x81\x32\x2b\x4c\x5f\xfd\x97\x85\x67\xc1\ -\xc6\x1a\xa5\x95\x6d\x53\x04\x01\x6b\xc5\xd7\xf9\xf2\xeb\x51\x3e\ -\xec\xf4\xf8\xe5\xec\x05\xae\xf4\x8c\xf0\xed\x07\x5d\x9b\x54\xe7\ -\x29\xa7\xd3\x27\x39\x9d\x29\x20\x45\x55\x3a\xc3\xca\xce\x01\x06\ -\x49\x9b\x5c\xe6\xca\x5f\xd7\x71\x63\x9a\x3f\x6e\xe6\xb8\x93\x9b\ -\xe1\xf7\xae\x37\x68\xf3\x83\xa6\x99\x18\x23\x09\xbb\x39\xae\x15\ -\x3c\x44\xad\xb6\x83\xca\x0e\x9a\xed\x02\x8f\x4c\x26\xc7\xed\x0e\ -\xcd\x83\xc7\xab\xe4\x9d\x87\xdc\x9c\x6f\x23\xec\x07\xe8\x66\x14\ -\x21\x09\xc9\x07\x2c\x7b\xba\xde\xbf\x3d\x6a\x7d\xd0\x04\x9a\x92\ -\x4e\xf2\xe9\xe1\x21\x3e\x4e\xfa\x24\x56\xb2\xfc\xb9\xf3\x3d\x8e\ -\x1d\xde\x85\x09\x74\x53\xa1\x0c\x02\xe5\xe4\x59\xbc\x34\xcd\xd5\ -\x95\xd2\xc6\x6c\x42\xea\x85\xa3\xc4\x71\x56\x98\x9d\xbe\xc1\xf6\ -\x58\xc0\xf5\x85\x3c\x99\xc2\x6d\x2e\x4f\x17\x70\x74\xeb\xc2\x3b\ -\xf2\x21\x77\xd7\xfc\xba\x8d\x28\x92\x43\xa3\xff\xc7\xeb\x40\xd1\ -\xb7\xb7\x8f\xfe\x90\xe1\x5e\x26\x4b\x21\xd2\xc5\xbe\x9e\x38\x2e\ -\x34\x87\x08\x81\x09\x9e\x70\xeb\x56\x8e\x25\xbf\x36\x6b\xf5\xe2\ -\xd5\xe7\x99\xb7\x8b\x2f\x8e\x1f\xe5\x93\xed\x01\x17\x2f\xfe\xc4\ -\x6c\xe7\xfb\x9c\xf9\x68\x37\xb2\x85\x5c\x18\x89\x1b\x5e\xe4\xd4\ -\xe7\xdf\x73\xe1\xfe\x2a\x42\x34\x80\x08\xc0\xd0\x46\x57\x34\x4a\ -\x3c\xee\xd3\x1d\x09\x11\x89\x27\x48\x6c\x0b\x83\xdf\x42\x2e\x23\ -\x70\xdd\x04\x09\x25\xca\x83\x5c\x47\xae\xe2\x7a\xf1\x8d\x11\x74\ -\x74\xc4\x88\xb9\xf0\x78\xf9\x09\x5e\x38\xc2\x8e\x68\x18\x89\x69\ -\xae\x97\x10\x48\xbf\xc4\xfd\x47\x4f\xf1\x4c\x8d\x63\x49\xd9\x7d\ -\x90\x02\x4d\xb1\xf8\x1a\x63\x13\x9f\x31\xd2\xe9\xf1\xf3\x8f\x97\ -\xf8\xbb\xfb\x5d\x4e\x0c\xbf\x89\xeb\xf9\x2d\x0a\xef\x10\x71\xfe\ -\xe3\x9b\xaf\x7e\xe0\xb7\xa5\x35\x44\xb5\x5e\x8b\xca\x2e\x9c\x94\ -\x40\x13\x98\x18\x87\xf6\x0f\xf0\x56\xa7\xcf\xbd\x81\x24\xf9\xde\ -\x7d\xbc\xdd\xdf\xbd\xc9\x89\x8f\xb3\x67\x9b\xc2\x2c\xd5\xc8\x35\ -\x23\x92\x43\xa3\xe9\xf2\x0e\x30\x04\x3a\xc2\x81\x83\x03\xf4\xb9\ -\x86\xec\x42\x96\xa5\xf0\x4e\x0e\xf4\x46\x91\xda\xb4\x9e\x13\xb1\ -\xca\x8d\xd9\x39\xb2\xc5\x60\x23\xe4\x48\xcd\x57\xd8\xf7\x03\x02\ -\x03\x8e\xe3\x20\x8d\xc6\x6f\x01\xa8\x02\x35\xf9\x0a\x03\x4c\x94\ -\xb3\x11\x28\xa5\x2a\xfa\xda\x21\xe4\xbc\xd2\x4e\x99\xa8\x5c\x5a\ -\xe7\x81\xa9\x2d\x5e\x5a\x53\xeb\x17\x8b\xca\xf5\x3b\xbe\x85\xa0\ -\x29\x7b\x5e\xdd\x8b\xc4\x38\x70\xc4\x6a\xf9\x32\x36\x6f\xe3\xc7\ -\xab\x1a\xbc\xbd\x7f\x70\xa3\xe3\x35\xe0\x3b\x1b\x10\x00\xed\xf6\ -\x71\x1a\xdc\xbb\xb2\xc0\x65\xe0\x0c\x70\xd4\xc6\x57\xd9\x73\x46\ -\x77\x21\x31\x4b\xbb\x34\xee\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ -\x42\x60\x82\ -\x00\x00\x02\x5a\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x19\x00\x00\x00\x19\x08\x06\x00\x00\x00\xc4\xe9\x85\x63\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x04\xc1\x00\x00\ -\x04\xc1\x01\x11\x76\xb1\x75\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x06\x07\x32\x1a\x15\x4e\x28\x6b\x00\x00\x01\xe7\x49\x44\ -\x41\x54\x48\xc7\xed\xd4\x3f\x48\x55\x61\x18\x06\xf0\xdf\x31\x25\ -\x92\x68\x48\x34\x54\x1a\x8c\xa8\x96\x90\x20\xda\xac\x3c\x44\x53\ -\xc7\x86\x42\x10\x1b\x4a\x44\x88\x0c\xce\x14\x0d\x8d\x42\x34\x75\ -\x06\x87\x72\xc8\x08\xd4\xfe\x80\x14\x47\x08\x1a\xce\x45\x82\x1a\ -\x82\x5c\x9a\x25\x0a\x84\x68\x28\xc8\x6a\xd0\xdb\x6d\x39\x37\x2e\ -\xb7\x7b\xb3\xc5\x28\xf2\x85\x0f\xce\xf7\x9e\xe7\xfd\x9e\xf7\x79\ -\xf8\xde\x8f\x8d\xf8\xaf\x22\x4e\xb3\x3f\x43\x10\xa7\xd9\xe9\x86\ -\x75\xe4\x39\x19\xa7\xd9\x22\xb6\x04\xbf\x5b\x31\x30\x32\x6a\x66\ -\x62\xbc\x56\xb7\x1d\x38\x8f\x00\xcd\xd8\x8f\x63\x39\xe4\x53\x12\ -\x85\xdb\x1a\xd7\x92\x9c\x44\x21\x98\x99\x18\x37\x30\x32\x3a\x89\ -\x3e\x7c\xc4\x70\x12\x85\x85\x38\xcd\x1e\xe1\x60\xbd\x23\x60\x2d\ -\xbb\xba\xe3\x34\xfb\x9a\x2b\xb9\x83\xb3\xd8\x8e\x5d\xc8\xfa\xcf\ -\x0c\xb5\xe3\x3a\x0a\x35\x6a\xdf\x24\x51\x78\x0b\x1a\xeb\x28\x08\ -\xd0\x8d\x69\xb4\xe4\xe9\xc1\x6a\xdc\xa6\xe6\xe6\x73\x1d\x27\x7a\ -\xaf\x2e\xcd\x15\xba\xd0\x5b\xed\x70\xf9\xa3\xa6\x92\x24\x0a\x4b\ -\x78\x8e\x56\x5c\xc9\xd3\x4b\xd5\xb8\xd2\xea\xea\xeb\xa5\xb9\xc2\ -\x2c\xc6\xaa\x7e\xdd\x48\xa2\xf0\x59\x79\x13\xd4\xb9\x7a\xfd\xb8\ -\x87\xf7\x68\x55\x2a\x75\xbe\x4b\x67\xdb\x04\xc1\x42\x05\xf4\xc5\ -\xcc\xc4\xf8\xa1\x5c\xf5\xb7\x8a\xfc\xd3\x24\x0a\x0f\x57\x9e\xd9\ -\x50\x43\x05\x2c\xe0\x5a\xae\x64\x5e\x10\x7c\xd9\xd1\x77\x6a\x77\ -\xf1\xf3\xf2\xd6\x52\xb1\x78\xa1\xb4\xb2\x12\xe5\x04\x2f\x71\xb7\ -\xa2\xfc\x15\x8e\x56\x0f\x61\x63\x0d\x15\x0d\x98\xc3\x9e\x3c\x7d\ -\x04\x1f\xe0\xfe\xd4\xed\x20\x4e\xb3\x76\x74\x99\xbc\x39\x87\x03\ -\xf9\x82\xe9\x24\x0a\x07\x2b\x6f\xe4\x4f\x76\xe5\x04\x2d\x78\x98\ -\x44\x61\x4f\x9c\x66\x63\xb8\x84\xa6\x0a\xfc\x04\x86\x73\x7b\x8a\ -\xd8\x9c\x37\x70\x31\x89\xc2\xa9\x5a\x04\x3f\x48\x72\x5f\x77\xe2\ -\x49\x12\x85\xfb\xca\xe0\x38\xcd\x9a\x70\x3c\x5f\x3d\xd8\x9b\x93\ -\xbe\xc5\x3c\x1e\x24\x51\xf8\x78\xad\x41\x0e\x72\x05\x5d\x58\x44\ -\x27\x96\xca\xdd\xd4\xeb\xac\xde\xc0\xfe\x8a\xa4\x09\xcb\x18\x4a\ -\xa2\x70\x6a\x3d\x1e\xb1\x06\x5c\x46\x5b\xd9\xd3\x7f\xf7\xdd\xdf\ -\x88\xbf\x2e\xbe\x03\xa4\x81\xab\x62\xf1\xf6\x73\x67\x00\x00\x00\ -\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x0c\x0c\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x32\x00\x00\x00\x32\x08\x06\x00\x00\x00\x1e\x3f\x88\xb1\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ -\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x09\x09\x20\x09\xbb\xf9\x24\x3a\x00\x00\x00\x19\x74\x45\ -\x58\x74\x43\x6f\x6d\x6d\x65\x6e\x74\x00\x43\x72\x65\x61\x74\x65\ -\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x57\x81\x0e\x17\x00\ -\x00\x0b\x74\x49\x44\x41\x54\x68\xde\xdd\x9a\x7b\x70\x53\xd7\x9d\ -\xc7\x3f\xf7\x5e\x49\xb6\xfc\x92\x1f\x92\x65\x4b\x96\xb1\xe4\x37\ -\x7e\xc6\xc6\x60\x30\x71\x78\x85\x26\x04\x68\x26\x1b\xb6\x09\xb3\ -\x79\x30\xb3\x1b\xda\x99\x26\x99\xd0\xa5\x9b\xc9\xd2\x1d\x6f\xca\ -\x64\x9a\x36\x4b\x1a\x36\x69\x66\x68\x5a\x76\x02\x69\x97\x65\x03\ -\x69\x12\x92\xb0\x3c\x42\xc0\x06\xc7\x26\x18\x8c\x6d\xc0\xf8\x8d\ -\xdf\x6f\x23\x64\xbd\xa5\xfd\xe3\x2a\x02\x17\xc7\xd0\x42\xbb\xf1\ -\xfe\xfe\xba\x73\xcf\x39\x3a\xe7\x7b\x7e\xaf\xef\xef\x77\x25\xf8\ -\xfd\x7e\x3f\x01\x39\x7f\xb1\x85\x2d\xaf\xef\xe4\x5c\x7b\x3f\x36\ -\xb7\x80\x5f\x10\xf9\x36\x89\xe0\xf7\x11\xae\xf4\x53\x60\x4e\x60\ -\xeb\x0b\x1b\xc8\xcb\x4a\xbb\x61\x2c\x00\x64\xd3\xd6\xed\xec\x3a\ -\x52\xff\xad\x3b\xfc\x4c\xa0\x9e\x58\x9e\xcf\xb6\x2d\xcf\x5d\x07\ -\xb2\x69\xeb\x76\xde\x3d\xda\xc0\x6c\x94\x27\x97\xe5\xb2\x6d\xcb\ -\x73\x88\xe7\x2f\xb6\xb0\xeb\x48\x3d\xb3\x55\x76\x1d\xa9\xe7\xfc\ -\xc5\x16\xa4\x1e\x77\x64\x45\xd7\xa8\x7d\xd6\x02\x41\x10\x68\x69\ -\x6e\x46\x3c\xd7\xde\xcf\x6c\x97\x73\xed\xfd\x88\x36\xb7\x30\xeb\ -\x81\xd8\xdc\x02\xe2\x6c\x89\x52\x33\x89\x5f\x10\x99\xfd\x28\x02\ -\xf2\xff\x06\x88\x62\xd6\x6b\x42\x12\x89\xd3\xc7\xce\x6e\x8d\xa8\ -\xc3\xd5\xcc\x5f\x5a\x4c\x58\x44\xd8\xec\xd5\x48\x7a\x7e\x1a\x19\ -\xf9\x69\x88\xa2\x40\x7d\x75\xc3\xec\x02\xa2\x50\x2a\xc8\x2c\x4c\ -\x27\x3d\x2f\x8d\xb0\x48\x35\xa2\x28\xd2\x50\xd3\xc4\xe4\x35\xfb\ -\xb7\x03\x88\x28\x89\x28\x14\x52\x30\x53\x03\x08\x80\x32\x44\x45\ -\x8c\x56\x83\xd6\xa0\x25\xc9\x6c\x20\x5a\x1b\x3d\x65\x9d\x63\xd2\ -\x41\x6b\x63\xdb\x5f\xc7\xd9\x05\x01\x14\x02\x78\x7c\xe0\xff\x86\ -\x39\xf9\xa5\xb9\x64\xe4\xa7\x81\x20\x20\x8a\x02\xa2\x24\x22\x49\ -\xd2\x2d\x7f\xbb\xa7\xbd\x0f\xeb\xf8\xb5\xbf\x2c\x10\x51\x80\x39\ -\xd1\x12\x31\x6a\x11\x85\x28\x03\xb1\x3a\x7d\x74\x8e\x7b\x71\x79\ -\xaf\xcf\x33\x9a\x0d\xa4\xe5\x5a\x08\x51\x87\xfc\x69\x49\xd0\xef\ -\xe7\xcc\xf1\xba\xbf\x6c\xf8\x0d\x91\xa0\xc4\xa4\x42\xad\x00\x41\ -\xb8\x4e\x81\xe2\xc2\x04\x8c\x1a\x89\xda\x6e\x37\x36\x97\x1f\x41\ -\x10\xc8\x2c\x48\x47\x1d\xae\xfe\x93\xf7\x68\xa8\x69\xc2\x61\x77\ -\xde\x9d\x84\x28\x08\x02\xd9\x45\x99\x3c\xfe\xec\x3a\x22\x34\x11\ -\xc1\xf7\xa5\xc9\x2a\xc2\x94\xc2\x14\x10\x5f\xcf\x57\x49\x02\x0b\ -\x4c\x4a\x24\x41\xbe\xd5\x2b\x6d\x3d\x38\x26\x1d\xb8\x9c\x6e\x6e\ -\x28\x56\x67\x14\xc7\xa4\x83\xf3\x5f\x36\xde\x9d\x84\x18\x19\x1d\ -\xc1\xc2\xfb\x17\xa0\x37\xc5\x33\x32\x30\xca\xb5\x09\xd9\x56\x53\ -\x63\x25\x42\x14\x33\x13\x51\x85\x28\x90\xa9\x53\xd0\x34\xe8\xe1\ -\x72\x7d\x0b\x57\x5a\xba\x09\x0d\x0b\xa1\xfc\xa1\x32\x34\x71\x9a\ -\x5b\xee\x7d\xe6\xc4\x59\xbc\x1e\xef\x9d\x03\x09\x8b\x08\x63\xf9\ -\x23\x4b\x18\x1f\x9e\xe0\xc8\xbe\x63\x41\x10\x00\xf1\x11\xb7\xa7\ -\x64\x6d\x98\x38\xe5\x86\x1d\x93\x0e\x6c\xd7\x26\x6f\x09\xa4\xaf\ -\xab\x9f\xae\xcb\xdd\x77\x87\xa2\xdc\xbf\x6e\x29\x91\xd1\x91\xb4\ -\x36\xb6\xd3\xdb\xd1\x77\xd3\x6d\xdf\x6e\x34\x53\x8a\xe0\xf6\x41\ -\x8c\x2e\x06\xbd\x29\x9e\xe8\xd8\x99\x41\xb8\x1c\x2e\x1a\x6a\x9a\ -\x70\xbb\xdc\x77\x0e\x64\xfe\xb2\x79\x44\xc5\x44\x01\x10\x1e\x19\ -\x46\x68\x58\x28\x5e\xaf\x17\xb7\x53\xfe\x71\xaf\xcf\x1f\xc8\x02\ -\x33\x8b\xcf\x7f\x1d\xc4\xaa\xf5\x2b\x11\xa5\x5b\x6b\xb2\xb5\xb1\ -\x8d\xfe\xae\x81\x3b\x27\x8d\x9a\x38\x0d\x99\x85\xe9\x53\x68\x42\ -\x5a\x5e\x2a\x8e\x49\x07\x47\x3f\x38\xce\xe8\xc0\x28\xfd\x56\x1f\ -\x69\x21\xb7\x3e\xd4\xc8\xa4\x2f\x60\xa6\xea\xdb\x72\xf2\xbe\xae\ -\x7e\x4e\x7f\x51\x77\x77\x68\x7c\x7a\x5e\xea\x4d\x9b\x0a\x82\x80\ -\x3a\x5c\x4d\x92\xd9\x00\x40\xfb\xb8\x97\x6b\x4e\xdf\xcc\x26\xe2\ -\xf1\x73\x79\xd8\x13\x3c\x60\xf5\xe1\x9a\x69\xcd\x25\x08\x7a\x60\ -\x94\x13\x07\x4e\xde\xbd\x7a\x44\x97\xa8\x45\x10\x04\x86\x7a\x87\ -\xa7\xf8\xc6\xc8\xc0\x28\x0d\xb5\x4d\x81\x44\x05\xa7\x7b\xdc\x58\ -\xbf\x01\x8c\xdd\xed\xa7\xa6\xdb\x8d\x3b\x30\xec\xf3\xfa\x68\xbf\ -\xd0\x89\xa4\x98\x3e\x93\xdb\xae\xda\xa8\xfa\xf4\x14\xce\x1b\x72\ -\xc6\x1d\x99\x96\x3a\x5c\xcd\x40\xf7\x20\xb5\xc7\xbe\x42\x9b\x10\ -\x47\xc9\xd2\xe2\xe0\xd8\xc9\x83\x5f\xe2\xf3\xca\x27\x53\xaa\x14\ -\xb8\x5c\x1e\xaa\xbb\x5c\x24\x69\x24\x0c\x51\x12\x6a\x85\x80\xcb\ -\xeb\xa7\xd7\xea\xa3\x73\xcc\x8b\x3f\x90\xd1\x4d\x69\x49\x9c\xad\ -\xaa\xe7\x9e\xc5\xf9\x88\xe2\xcd\x77\x6a\xb7\xd9\x39\x79\xf0\x4b\ -\x26\x46\xaf\xde\x3a\x78\xc4\x2d\xfd\x07\xff\x6d\x83\x89\x50\x53\ -\xba\xa2\x84\x24\x8b\x71\xca\xfb\xb1\xa1\x71\x8e\x7f\x5c\x49\x62\ -\x72\x02\x19\x85\xe9\x5c\xac\x6b\xa6\xa5\xa1\x0d\xbf\x6f\x7a\xad\ -\xc4\xe8\xa2\x59\xfd\xc4\x83\x41\x8d\x4c\xe7\xe8\x36\xab\x8d\xea\ -\x43\xb5\x37\x45\xc5\x3b\xd6\x88\xde\x14\x4f\xe9\x8a\xf9\x44\xc5\ -\x44\x4e\x7b\xb0\xfb\xd7\x2d\x23\x34\x2c\x14\x51\x14\x29\x5d\x51\ -\x42\x57\x73\x17\x4e\x87\x0b\xa5\x4a\x49\x76\x51\x26\x08\x70\xb9\ -\xbe\x05\xbb\xcd\xc1\xd5\x31\xeb\x14\xe6\xfb\xc7\x32\xd8\x3b\x44\ -\xcd\x91\xd3\x8c\x0d\x8d\xdf\xdd\x52\xd7\x68\x36\xb0\x60\xf9\x3c\ -\x54\x33\x10\xbb\xb0\x88\xb0\xe0\xb3\xd7\xe3\xc5\xe9\x70\xc9\x99\ -\x3e\xd7\x42\x5e\x69\x4e\xf0\xf6\x1b\x6a\x9a\x10\x25\x11\xa7\xdd\ -\x39\x2d\x51\x6c\x6b\x6a\xe7\xf4\xb1\x33\xc1\xf5\x77\x0d\x48\x5a\ -\xae\x85\x79\x4b\x8a\xf8\xe2\xa3\x4a\xfa\x3a\xfb\x59\xb2\xf6\x5e\ -\x8c\x66\xc3\xcc\x71\x5f\x90\xd7\xf5\x76\xf4\x91\x96\x63\x0e\xda\ -\x7f\x7a\x5e\x2a\xda\x44\x2d\xa6\x54\xe3\x4d\x4b\x9c\x76\x27\xf5\ -\xd5\x0d\x5c\xac\x6b\xfe\xf3\x78\xdf\x37\xf9\x88\x28\x89\x14\x2c\ -\xcc\x23\xa3\x20\x9d\x2f\x3e\x3c\x41\xff\x95\xeb\x89\xc8\x9c\x9d\ -\x42\x6a\x8e\x19\x6d\x42\x1c\x4a\x95\xf2\xce\x9a\x6b\xd6\x49\x7a\ -\x3b\xfa\x68\xac\xbd\x80\x75\xdc\xfa\xe7\x13\xd8\xe9\x80\x48\x0a\ -\x89\x05\xcb\x4b\x98\x93\x61\xe2\xd0\x7f\x1f\x65\xb8\x6f\xe4\x66\ -\x55\x2a\x24\x62\xf5\xb1\xe8\x0c\x5a\x0c\x29\x89\xe8\x12\xb5\xdf\ -\x18\x42\xa7\x93\x81\xee\x41\xba\x5a\xba\xe9\xef\x1a\x60\x7c\x78\ -\x9c\x3b\x95\x69\x81\x2c\x7d\xb8\x9c\x24\x8b\x91\x23\xfb\x8e\xdd\ -\x56\xd4\x50\x28\x24\x44\x85\x44\xa4\x26\x02\xbd\x29\x1e\x4d\x9c\ -\x86\xa8\xe8\x48\x42\xd4\x21\x08\x82\x80\xcf\xe7\xc3\x76\xd5\x86\ -\x75\xdc\xca\x40\xf7\x20\x83\x3d\xc3\x78\x5c\x6e\x3c\x7f\xc4\x60\ -\xef\x5a\x5f\x4b\x10\x04\x96\x7e\xb7\x1c\xa3\xc5\x40\x43\x6d\x13\ -\x7d\x9d\xb7\xd7\xe0\xf6\x78\xbc\xe0\xf1\x32\xe2\x18\x65\x64\x60\ -\xf4\xff\xb6\x41\xa7\x50\x48\xdc\xbb\xba\x0c\xa3\xc5\xc0\xc4\xc8\ -\x04\x97\xea\x9a\x6f\xbb\xd0\xf9\x56\xb5\x4c\x0b\xca\xf2\x83\x89\ -\xae\xa1\x56\x6e\xb1\xcc\xba\x96\x69\x72\x7a\x12\x73\x8b\xb3\x18\ -\x1d\x1c\xe3\xc4\x81\xaa\x29\x09\x6b\xd6\x00\x51\x47\xa8\x29\x2e\ -\xbf\x87\x73\xa7\xce\x53\x7f\x6a\x76\x7e\x47\x04\x50\x58\xb2\x52\ -\xa8\xfa\xac\x1a\xa5\xdb\xcb\xcf\x9e\x7d\x8c\xc5\x85\x59\xe8\x62\ -\x23\x99\xb4\xbb\x78\x6d\xd7\xc7\xbc\xf7\x69\x15\xab\x16\x17\xf2\ -\xe0\xa2\x42\xae\x4d\x3a\xd8\xf6\xde\x01\x86\xc6\xac\xa4\x18\x74\ -\xfc\xf0\x7b\x2b\x51\xab\x54\xfc\xf6\xc3\x63\x7c\x75\xa1\x1d\x4d\ -\x44\x18\x4f\x3c\xb4\x98\xc7\x56\x2e\x42\x1f\xa7\xc1\xe1\x74\xb1\ -\xfb\xd3\x4a\xb6\xff\xfe\x20\x76\xa7\x9c\xa9\xc3\x43\x43\x78\x78\ -\xe9\x3c\x36\x7c\x77\x09\xc9\xfa\x38\x04\x41\x60\xef\xe1\x6a\x5e\ -\xde\xb1\x0f\xc7\x0c\x54\xfe\x96\x3e\xd2\x78\xfa\x02\xe9\xda\x18\ -\xaa\x76\x56\xf0\xd0\xbd\x45\x6c\xda\xb6\x8b\xbf\xdb\xf2\x2b\x0c\ -\xba\x18\xde\xd8\xfc\x14\xa9\x49\xf1\xac\x29\x2f\xe6\xf1\x07\x16\ -\xb1\x7c\x41\x2e\x5f\xfb\x7f\x41\x46\x32\x4f\xaf\xb9\x8f\x75\xf7\ -\x2f\x20\x2c\x54\x85\x3e\x56\xc3\x81\x37\x7e\x4c\xc5\xc6\x47\xd9\ -\xf9\xd1\x17\xac\xfb\xf1\x2f\xb1\xbb\xdc\x6c\x7e\x72\x0d\x9b\x9f\ -\x5a\x0d\x40\xa2\x36\x9a\xfd\xff\xb6\x89\x37\x36\x3f\xc5\xc1\x93\ -\xe7\x78\xfc\xa5\x7f\x67\xcc\x6a\xe3\x99\x47\x96\xf3\x8f\x4f\xae\ -\xbe\x33\x8d\x28\x24\x89\x67\x1e\x59\x4e\x84\x3a\x94\xc3\xd5\x0d\ -\xd4\x34\xb6\x12\xaa\x52\x32\xe9\x70\x12\x19\xae\xc6\xe1\xf2\x60\ -\xd4\xc5\xc8\x9d\xbd\xc1\x51\x46\x02\x8d\x86\x94\x44\x9d\x5c\x8b\ -\x4c\x5c\xa3\xa3\x77\x98\x8f\xde\xd8\x8c\xc5\x18\xcf\xef\x3f\x3b\ -\xc9\x6f\x3e\xf8\x1c\x80\xcf\x4f\x37\x62\x31\xc6\xb3\xfe\x81\x32\ -\x5e\xde\xb1\x8f\x57\x9f\x5f\x4f\x51\xb6\x99\xcf\x6b\x1b\xf9\xc5\ -\xbb\x1f\x03\x50\xdb\xd0\x8a\xc5\x18\x4f\x88\x52\xc1\x5c\x8b\x91\ -\x55\x65\xf7\xa0\x52\x4a\xbc\xf2\xdb\x3f\xa0\x54\x48\x6c\x58\xbb\ -\x84\xac\x14\x03\x3f\xdb\xf9\x07\xf4\x71\x1a\x1e\x5e\x5a\x42\x65\ -\xdd\x45\x42\x54\x4a\x1e\xff\xce\x22\x3c\x5e\x2f\x5b\xdf\xd9\x8f\ -\x42\xa5\x94\x88\x89\x0a\x97\x5b\x97\x19\xc9\x98\x0d\x3a\x3a\xfa\ -\x86\xf9\xdd\x67\x55\x7c\x70\xec\x2b\x6c\x76\x07\x26\x7d\x1c\x00\ -\x2d\x57\x06\xf0\xfb\xe5\xc6\x5a\x9a\x29\x01\x80\xab\x36\x3b\x2b\ -\x4b\xf3\xb0\x18\xe3\x01\xd8\xfd\x49\x65\xf0\x96\x54\x0a\x39\xba\ -\x6b\xa3\x23\xc9\x4f\x4f\x66\x55\x59\x21\x00\x5b\xde\xfe\xaf\xe0\ -\x9c\x09\xdb\x24\x07\x2a\xeb\xf8\xe9\x3b\xfb\xd9\xfb\xf3\xe7\x29\ -\x2b\xc8\x64\xcf\xc1\x53\x72\x7f\x2c\x2f\x8d\x57\x7e\xf8\x3d\x00\ -\x36\x6d\xdb\xc5\xcf\x9f\x5f\xcf\xea\xf2\x22\x72\x53\x93\x88\x0a\ -\x57\x93\x14\x1f\x4b\xa2\x2e\x06\xb3\x31\x1e\x85\xdd\xee\xa0\xb1\ -\xb5\x9b\x45\x05\x19\x58\x8c\xf1\xbc\xff\xda\x0b\xbc\xfa\x1f\x1f\ -\xf1\xcf\x6f\xc9\x9b\xcd\x49\xd4\x62\x4a\x90\x81\x74\xf6\x0d\xa1\ -\x8f\xd5\xa0\x0e\x55\x91\x1d\x08\xd5\x13\xd6\x49\x8a\xe7\x5a\x02\ -\xda\xb1\xd2\x3f\x72\x9d\x6e\x24\x27\x6a\x83\xed\xcd\x17\x37\xac\ -\x0d\xce\xb9\x74\x03\x5b\x78\xe9\xcd\x3d\xc1\xe7\x1c\x4b\x12\x00\ -\x07\xab\xe5\xef\xfe\x65\x05\x99\x00\x5c\x68\xef\x01\x20\x63\x4e\ -\xa2\x7c\x41\x4a\x05\x0f\x3e\xfb\x2a\xeb\x1f\x2c\x63\xfb\xe6\xa7\ -\x48\x4d\xd2\xa3\x08\x57\xfa\xf9\xc9\xdb\x7b\x59\x98\x9f\x4e\x6e\ -\x9a\x89\xe4\x04\x2d\x6f\xfe\xd3\xd3\x64\xa6\x24\xf2\xf2\x8e\x7d\ -\xc1\x9b\x07\xa8\xd8\xf8\x28\x15\x1b\x1f\x9d\x62\x9b\x97\xbb\xfa\ -\x49\x31\xca\x66\x36\x3c\x66\xc5\x6a\x73\x04\xc7\xb2\x52\xe4\x3a\ -\xbe\x7f\x78\x9c\x65\x25\x32\x95\x3f\xdd\xd4\x3e\x65\xfd\x9a\xf2\ -\x22\x32\xe7\x18\xa8\x6d\x6a\x25\x3a\x32\x3c\x60\x92\x72\xd9\xbc\ -\xa8\x20\x03\x80\x73\xcd\x9d\x48\xa2\x18\x04\xf2\xf6\xde\x43\x72\ -\x89\x90\xa4\x07\xa0\xa9\xb5\x0b\xb1\xc0\x9c\x80\xc7\xeb\x65\xc9\ -\x33\x3f\xe5\x97\xbf\xfb\x14\x87\xcb\x8d\x20\x08\x3c\xf7\xd8\x03\ -\xa4\x9b\x12\x58\x90\x9b\x1a\xdc\x74\xe1\xd3\xff\x42\xf9\xdf\xff\ -\x2b\x7f\xb3\xf9\xf5\xe0\xbb\xba\xe6\x0e\x34\xe1\x61\x41\x7f\xb1\ -\xd9\x65\x20\x31\x51\xe1\x84\x06\x98\x71\x7b\xef\x10\x8a\x40\x77\ -\xfd\x46\x8d\x65\xa6\x24\xb2\xb3\xe2\xfb\xbc\xb8\x61\x2d\xfa\x40\ -\x63\x6e\xec\xaa\x0d\xab\x4d\x4e\xc6\xb9\xa9\x49\x01\xf0\x6d\xe4\ -\xa5\x99\x82\xeb\x0e\xd7\xc8\x69\x62\x65\x69\x9e\xec\xbb\x7d\x03\ -\x88\xaf\xbd\xb8\x91\x7d\xbf\x78\x81\x35\xe5\x45\x6c\x7d\x67\x3f\ -\x3f\xda\xb6\x3b\xb8\xa0\x30\x73\x0e\xf3\xe6\xca\x40\x1a\x5b\xbb\ -\xb9\xdc\xd5\x4f\x53\x5b\x4f\xd0\xf6\x01\x6a\x1b\x5b\x11\x25\xb9\ -\x8f\x35\xe9\x70\xe2\x74\x7b\x82\x87\x08\x09\x00\xd9\x77\xb4\x26\ -\x38\x3f\x42\x1d\x1a\x7c\x2e\xca\x32\x03\x70\xa0\xb2\x8e\x05\xb9\ -\xf2\x3f\x7d\xaa\xce\x5e\x02\x20\x49\x1f\x8b\x52\xa9\x08\x98\x56\ -\x2f\x4b\xe6\xcd\x05\xa0\x77\x68\x0c\x9f\xcf\x4f\x84\x3a\x04\x73\ -\xc0\x2f\xef\xc9\x4e\x45\x4c\x37\x27\x53\x5e\x9c\x8d\x14\x28\x7e\ -\xf6\xfc\xcf\xa9\xe0\x46\xe7\x5b\xae\xb0\xa8\x40\xee\x63\x1d\x3f\ -\x73\x21\xf8\xbe\x24\xc7\x12\x7c\x3e\xd7\xdc\xc5\xe8\x0d\x2d\xd3\ -\xaf\x65\x59\x49\x2e\xa1\x2a\x25\x2d\x57\x06\x78\xf7\xc0\x09\xc6\ -\xae\xda\x00\x58\xb5\xb8\x10\x65\x80\xee\x17\x67\xcb\x40\xce\x5e\ -\xea\x24\x33\x60\x36\x97\xaf\xf4\x07\xc2\xfb\x1c\x14\x92\xc8\xb8\ -\xd5\x46\xef\xd0\x18\xf7\x15\x67\xcb\x8d\x8e\x73\x72\xe1\x95\x93\ -\x6a\x42\x12\x05\x9c\x2e\x37\x73\x92\x12\xaf\x93\xc6\xef\x3f\x7c\ -\x2f\x1d\x3d\x03\x3c\xb2\xa2\x14\x80\xf7\x3e\xa9\xc4\xe5\xf6\x04\ -\x4d\xe2\xf8\x99\x8b\xc1\x43\x5a\x8c\xfa\x29\x66\xf2\xc1\xb1\xd3\ -\x94\xe6\xc9\x3e\xb6\xf6\xbe\x62\xb4\x9a\x48\x9e\x7d\xec\x3b\xb4\ -\xf7\x0c\xf2\x83\x57\x7e\x83\xcf\xe7\xe7\xe5\x5f\xbf\xcf\xeb\x3f\ -\x7a\x12\x75\x88\x8a\xfa\x3d\xaf\x72\xa9\xb3\x8f\xf9\x39\xb2\xb6\ -\x7b\x86\x46\xe9\x0b\xd4\x24\xb9\xa9\x26\xf2\xd2\x4c\xfc\xe0\xd1\ -\x15\x28\x15\x0a\x24\xd1\x8d\x26\x42\x4d\x59\xc0\x5f\xbe\x6c\x68\ -\x01\x20\x21\x36\x0a\xe5\x0d\x96\x21\x55\x54\x54\x54\x00\x18\xf4\ -\x3a\xfe\x76\xe5\x42\x24\xbf\x97\x97\xb6\xef\xe6\xad\xbd\x87\x89\ -\x8f\x8d\xc6\xed\xf1\x70\xf6\x52\x07\xbb\x3f\xa9\xc4\x1d\xa8\x1f\ -\x26\x6c\x93\xdc\x57\x9c\x4d\x75\x7d\x0b\x1f\x1e\xff\x8a\x33\x17\ -\x3b\x68\x6a\xeb\x21\x2b\xc5\xc0\x03\x8b\x0a\x48\x33\xe9\x79\x6b\ -\xef\x21\x5e\x7a\xf3\x3f\x69\xeb\x19\x94\x89\x68\xeb\x15\xfa\x87\ -\x27\x30\x1b\x75\xb8\x3d\x5e\x0e\x55\xd7\xb3\x63\xdf\x51\xe6\xe7\ -\xa4\xf2\xfe\xd1\x5a\xde\xfb\xa4\x8a\xac\x14\x03\x39\xa9\x49\x98\ -\x0d\x3a\x76\xec\x3f\x8a\x41\x17\x83\xdd\xe9\xa6\xb6\xa9\x8d\x14\ -\x83\x8e\xd3\x8d\xad\xec\x39\x70\x94\xac\x84\x48\x7e\xb2\x71\x1d\ -\xba\xb8\x98\x20\x90\xff\x05\xf1\x39\x56\x09\x19\xda\x21\xae\x00\ -\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x06\xc8\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x32\x00\x00\x00\x32\x08\x06\x00\x00\x00\x1e\x3f\x88\xb1\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ -\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x05\x0a\x13\x1d\x1d\xd8\x94\x96\x00\x00\x00\x19\x74\x45\ -\x58\x74\x43\x6f\x6d\x6d\x65\x6e\x74\x00\x43\x72\x65\x61\x74\x65\ -\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x57\x81\x0e\x17\x00\ -\x00\x06\x30\x49\x44\x41\x54\x68\xde\xed\x9a\x5b\x4f\x5b\xd9\x15\ -\x80\xbf\xbd\xcf\x31\x60\x63\x30\x17\x73\x33\x90\x89\xc1\x06\xd2\ -\x70\xc9\x24\x21\x61\x94\x4e\x14\x42\x52\xa9\x33\xa3\xb6\xaa\x94\ -\x87\x56\xca\x48\x79\xe8\x63\x23\x65\x9e\xf3\xd0\x07\x9e\x53\x69\ -\xfe\x40\xa5\xe6\xb5\x1a\x55\x6d\x47\xed\x68\x4a\x94\xcb\x24\x21\ -\x40\xc3\x04\x0c\x61\x08\x81\xe0\x70\xb1\xb9\x18\x13\x63\xf0\xf5\ -\x9c\x3e\x98\x9a\x30\x10\xc3\x0c\x4c\xcb\x89\xba\x9e\xec\xb3\xcf\ -\x3e\x7b\x7f\x7b\xad\xbd\xd6\xda\xeb\x1c\xa1\xeb\xba\xce\xba\x0c\ -\x8e\x8c\x71\xfd\xf7\x7f\xe0\xc9\x84\x8f\x70\x5c\xa0\x0b\xc9\x41\ -\x12\xa1\x6b\xe4\x9a\x74\x5a\x9c\xe5\x74\x5e\xbb\x42\x53\x83\xeb\ -\xb5\xb6\x75\x90\x4f\x3a\x3f\xe5\x66\xd7\xc0\x81\x9b\x7c\x26\xa8\ -\xcb\x1d\xcd\xdc\xb8\x7e\x75\x03\xe4\x93\xce\x4f\xf9\xe3\x2d\x0f\ -\x46\x94\x8f\xcf\x37\x72\xe3\xfa\x55\xe4\xe0\xc8\x18\x37\xbb\x06\ -\x30\xaa\xdc\xec\x1a\x60\x70\x64\x0c\x65\x3a\x9e\xf7\x3b\x6f\x60\ -\xcd\xb0\x20\x08\xc1\xd8\xe8\x28\xf2\xc9\x84\x0f\xa3\xcb\x93\x09\ -\x1f\x32\x1c\x17\x86\x07\x09\xc7\x05\xd2\x28\x5e\x2a\x93\xe8\x42\ -\x62\x7c\x8a\x75\x79\x6b\x40\x54\xc3\x6b\x42\x91\x14\x97\x15\x19\ -\x5b\x23\xe6\x5c\x33\xa7\xda\x4f\x60\xb1\x5a\x8c\xab\x11\x77\xb3\ -\x8b\xba\x66\x17\x52\x0a\x06\xba\x3d\xc6\x02\x51\x4d\x2a\xf5\xc7\ -\xdc\xb8\x9b\x5c\x58\xf2\xcc\x48\x29\xf1\xf4\x0c\xb3\xba\xb2\x76\ -\x30\x40\xa4\x22\x51\x55\x25\x1d\xa9\x01\x04\x60\xca\xce\xa2\xd0\ -\x6e\xc3\xee\xb0\x53\xe5\x74\x50\x60\x2f\xd8\xd4\x2f\xb2\x1a\xe1\ -\xf9\xd0\xf8\x7f\x67\xb3\x0b\x01\xaa\x80\x84\x06\xfa\x1b\xee\x69\ -\x6e\x6b\xa4\xae\xd9\x05\x42\x20\xa5\x40\x2a\x12\x45\x51\x76\x7c\ -\xf6\xf4\xc4\x2c\xa1\xe0\xca\x0f\x0b\x22\x05\xbc\x53\xa0\x50\x68\ -\x96\xa8\x32\x05\x12\x8a\x6a\x4c\x06\x93\xc4\x92\x1b\xf7\x55\x3a\ -\x1d\xb8\x1a\x6b\xc8\x36\x67\x7f\xb7\x20\xa8\xeb\x3c\xbe\xdb\xff\ -\xc3\xba\xdf\x6c\x05\x5a\xab\xb3\x30\xab\x20\xc4\x46\x0a\x54\x6c\ -\x11\x54\xda\x14\x7a\xa7\xe2\x84\x63\x3a\x42\x08\xea\x5b\xdc\x98\ -\x73\xcd\xdf\x79\x0c\x4f\xcf\x30\x91\xb5\xe8\xfe\x04\x44\x21\x04\ -\x47\x8e\xd7\xf3\xab\xdf\x5e\xc2\x6a\xb3\xa6\xaf\xb7\x1d\xca\xc2\ -\x62\x12\x9b\x20\xfe\x73\x7f\x96\x22\x38\x5d\x6d\x42\x11\xa9\x55\ -\x7d\x39\x3e\x4d\x64\x35\x42\x2c\x1a\xe7\xb5\xc3\x6a\x46\x89\xac\ -\x46\x18\x7c\x34\xb4\x3f\x01\x31\xaf\xc0\xca\x7b\x17\x4f\x53\x56\ -\x5d\xca\xa2\x3f\xc0\xca\x72\xca\x56\x6b\x8b\x14\xb2\xd5\xcc\x89\ -\xa8\x2a\x05\xf5\x25\x2a\xc3\x73\x09\x9e\x0d\x8c\xf1\x72\x6c\x8a\ -\x1c\x4b\x36\x67\x3f\x3c\x83\xad\xd8\xb6\xe3\xd8\x8f\xef\x7d\x4d\ -\x32\x91\xdc\x3b\x88\xc5\x6a\xa1\xe3\x97\xe7\x08\x2e\x2c\xd3\xf5\ -\xd9\xed\x34\x04\x40\xa9\x75\x77\x4a\xb6\x5b\xe4\xa6\x15\x8e\xac\ -\x46\x08\xaf\xac\xee\x08\x32\xeb\xf5\xe1\x7d\x36\xb5\x3f\x29\xca\ -\xc5\x4b\xed\xe4\x15\xe4\xf1\x7c\x68\x82\x99\x17\xb3\x5b\x56\x7b\ -\xb7\xde\xcc\x24\x21\xae\x41\x61\x49\x21\x65\xd5\xa5\x14\x14\x65\ -\x86\x88\x45\x62\x78\x7a\x86\x89\xc7\xe2\x7b\x07\x39\x75\xfe\x24\ -\xf9\x85\xf9\x00\xe4\xe6\x59\xc8\xb1\xe4\x90\x4c\x26\x89\x47\x53\ -\x0f\x4f\x6a\xfa\x7a\x14\xc8\x2c\x9a\xbe\x01\xf1\xc1\xaf\x7f\x82\ -\x54\x76\xd6\xe4\xf3\xa1\x71\x7c\x5e\xff\xde\x93\x46\x5b\xb1\x8d\ -\xfa\x63\xee\x4d\x69\x82\xab\xa9\x96\xc8\x6a\x84\x5b\x7f\xbe\x4b\ -\xc0\x1f\xc0\x17\xd2\x70\x65\xef\x3c\xa9\xc5\x55\x6d\xdd\x4c\xcd\ -\xbb\xda\xe4\xb3\x5e\x1f\x7d\x77\xfa\xf7\x27\x8d\x77\x37\xd5\x6e\ -\x19\x54\x08\x81\x39\xd7\x4c\x95\xd3\x01\xc0\x44\x30\xc9\x4a\x54\ -\xcb\x6c\x22\x09\x9d\x67\x0b\x89\xf4\x04\xbb\xff\xd9\xb3\xad\xb9\ -\xa4\xa1\xfd\x01\xee\x7d\xfe\x60\xff\xce\x23\x25\x15\x76\x84\x10\ -\xcc\xcf\x2c\x6c\xda\x1b\x8b\xfe\x00\x9e\xde\xe1\xf5\x40\x05\x7d\ -\xd3\x71\x42\x6f\x80\x59\x8b\xeb\xf4\x4c\xc5\x89\xaf\x37\x6b\x49\ -\x8d\x89\xa7\x93\x28\xea\xf6\x91\x3c\xfc\x2a\xcc\xfd\xbf\x3f\x24\ -\xfa\x5a\xcc\xd8\x93\x69\x99\x73\xcd\xf8\xa7\xe6\xe8\xbd\xfd\x2f\ -\xec\xe5\xc5\xb4\xb6\x9f\x48\xb7\x3d\xf8\xe2\x11\x5a\x32\x35\x33\ -\x53\x96\x4a\x2c\x96\xa0\xdb\x1b\xa3\xca\xa6\xe0\xc8\x57\x30\xab\ -\x82\x58\x52\x67\x26\xa4\x31\xb9\x94\x44\x5f\x8f\xe8\xd5\xae\x2a\ -\xbe\xbe\x3f\xc0\xbb\x3f\x6e\x46\xca\xad\x6b\xba\x16\x5e\xe3\xc1\ -\x17\x8f\x58\x0e\xbc\xda\xd9\x79\x14\xb7\xff\x46\xdf\x35\x8c\xd5\ -\x4c\xdb\x85\x56\xaa\x6a\x2a\x37\x5d\x5f\x9a\x0f\x72\xf7\x6f\x5f\ -\x51\x71\xa8\x9c\xba\x63\x6e\x46\xfa\x47\x19\xf3\x8c\xa3\x6b\xdb\ -\x6b\xa5\xb0\xa4\x80\x8f\x2e\xff\x34\xad\x91\xed\x36\x7a\x38\x14\ -\xa6\xfb\xcb\xde\x2d\x5e\x71\xcf\x1a\x29\xab\x2e\xa5\xed\xc2\x29\ -\xf2\x0b\xf3\xb6\x9d\xd8\xc5\x4b\xe7\xc9\xb1\xe4\x20\xa5\xa4\xed\ -\x42\x2b\xde\x51\x2f\xd1\x48\x0c\x53\x96\x89\x23\xc7\xeb\x41\xc0\ -\xb3\x81\x31\xd6\xc2\x11\x5e\x2d\x85\x36\x65\xbe\xdf\x96\xb9\x99\ -\x79\x7a\xba\xfa\x58\x9a\x0f\xee\xef\x51\xb7\xd2\xe9\xe0\x74\xc7\ -\x49\xb2\x32\x24\x76\x16\xab\x25\xfd\x3b\x99\x48\x12\x8d\xc4\x52\ -\x91\xbe\xb1\x86\xa6\xb6\xa3\xe9\xd5\xf7\xf4\x0c\x23\x15\x49\x74\ -\x2d\xba\x6d\xa2\x38\x3e\x3c\x41\xdf\xed\xc7\xe9\xfe\xfb\x06\xe2\ -\x6a\xac\xe1\xe4\xb9\xe3\xdc\xf9\xeb\x57\xcc\x4e\xfa\x38\xf7\xb3\ -\xf7\xa9\x74\x3a\x32\xfb\x7d\x91\xea\x37\xf3\x62\x16\xd7\x51\x67\ -\xda\xfe\xdd\x4d\xb5\xd8\x2b\xec\x54\xd7\x56\x6e\xe9\x12\x5d\x8b\ -\x32\xd0\xed\x61\xa4\x7f\xf4\xfb\xe5\x7d\x6f\xda\x23\x52\x91\xb4\ -\xbc\xd7\x44\x5d\x8b\x9b\x3b\x7f\xb9\x87\xef\xe5\x46\x20\x72\x1e\ -\x39\x4c\xed\x51\x27\xf6\xf2\x62\x4c\x59\xa6\xbd\x15\xd7\x42\xab\ -\xcc\xbc\x98\x65\xa8\xf7\x29\xa1\x60\xe8\xfb\x27\xb0\xdb\x81\x28\ -\xaa\xc2\xe9\x8e\x56\xde\xa9\xab\xe6\xcb\x3f\xdd\x62\x61\x76\x71\ -\xab\x2a\x55\x85\xa2\xb2\x22\x4a\x1c\x76\x1c\x87\x2b\x28\xa9\xb0\ -\xbf\xd1\x85\x6e\x27\xfe\xa9\x39\xbc\x63\x53\xf8\xbc\x7e\x82\x0b\ -\x41\xf6\x2a\xdb\x82\xb4\xff\xe2\x2c\x55\x35\x95\x74\x7d\x76\x7b\ -\x57\x5e\x43\x55\x15\xa4\xaa\x90\x67\xb3\x52\x56\x5d\x8a\xad\xd8\ -\x46\x7e\x41\x1e\xd9\xe6\x6c\x84\x10\x68\x9a\x46\xf8\x55\x98\x50\ -\x30\x84\x7f\x6a\x8e\xb9\xe9\x05\x12\xb1\x38\x89\x6f\x65\xb0\xfb\ -\x56\xd7\x12\x42\xd0\xfe\xf3\xb3\x54\xd6\x38\xf0\xf4\x0e\x33\x3b\ -\xb9\xbb\x02\x77\x22\x91\x84\x44\x92\xc5\x48\x80\x45\x7f\xe0\x7f\ -\x5b\xa0\x53\x55\x85\xf7\x3f\x3a\x43\x65\x8d\x83\xe5\xc5\x65\xbe\ -\xe9\x1f\xdd\xf5\x41\xe7\x40\x95\x4c\x5b\xce\x34\xa7\x03\x9d\xa7\ -\x37\x55\x62\x31\x5c\xc9\xf4\x90\xbb\x8a\x1f\x9d\x68\x20\x30\xb7\ -\xc4\xbd\xcf\xef\x6f\x0a\x58\x86\x01\x31\x5b\xcd\x9c\x38\xfb\x2e\ -\x4f\x1e\x0e\x32\xf0\xd0\x98\xef\x11\x01\xd4\x9a\x86\xc3\xdc\xff\ -\x47\x37\x73\xd3\xf3\x46\x2e\x03\xa3\x0e\xf5\x3d\xe5\x6d\x90\xff\ -\xbf\xe8\x39\x70\x20\x42\xd7\x0c\x0f\x21\x74\x0d\x99\x6b\xd2\x0d\ -\x0f\x92\x6b\xd2\x91\x2d\xce\x72\xc3\x83\xb4\x38\xcb\x91\x9d\xd7\ -\xae\x60\x64\xf3\x12\xba\x46\xe7\xb5\x2b\xc8\xa6\x06\x17\x97\x3b\ -\x9a\x0d\x0b\x72\xb9\xa3\x99\xa6\x06\x57\xca\x6b\xdd\xb8\x7e\x95\ -\x8f\xcf\x37\x1a\x4a\x33\x42\xd7\xd2\x5f\x06\xa5\xfe\xbf\x25\x1f\ -\x9e\xfd\x1b\xbe\x24\x72\x61\x2d\x7b\xa9\xe3\x00\x00\x00\x00\x49\ -\x45\x4e\x44\xae\x42\x60\x82\ -" - -qt_resource_name = "\ -\x00\x07\ -\x07\x3b\xe0\xb3\ -\x00\x70\ -\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ -\x00\x0c\ -\x0b\x85\xb6\xe2\ -\x00\x73\ -\x00\x68\x00\x6f\x00\x67\x00\x75\x00\x6e\x00\x65\x00\x64\x00\x69\x00\x74\x00\x6f\x00\x72\ -\x00\x15\ -\x01\xd9\xbe\x27\ -\x00\x61\ -\x00\x70\x00\x70\x00\x6c\x00\x69\x00\x63\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\ -\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x0f\ -\x0a\x75\xc5\x07\ -\x00\x6c\ -\x00\x61\x00\x79\x00\x65\x00\x72\x00\x73\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x18\ -\x04\x35\xb9\x07\ -\x00\x73\ -\x00\x68\x00\x6f\x00\x67\x00\x75\x00\x6e\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x32\x00\x35\x00\x78\x00\x2d\x00\x32\ -\x00\x35\x00\x70\x00\x78\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x11\ -\x02\xf6\x79\x47\ -\x00\x6c\ -\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x77\x00\x69\x00\x74\x00\x68\x00\x2d\x00\x74\x00\x61\x00\x67\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\ -\x00\x22\ -\x02\x09\x81\xc7\ -\x00\x73\ -\x00\x68\x00\x6f\x00\x67\x00\x75\x00\x6e\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x35\x00\x30\x00\x78\x00\x35\x00\x30\ -\x00\x70\x00\x78\x00\x2d\x00\x72\x00\x6f\x00\x75\x00\x6e\x00\x64\x00\x2d\x00\x62\x00\x6c\x00\x75\x00\x65\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -" - -qt_resource_struct = "\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x14\x00\x02\x00\x00\x00\x05\x00\x00\x00\x03\ -\x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x15\x5c\ -\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x09\x4c\ -\x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x06\xee\ -\x00\x00\x00\x62\x00\x00\x00\x00\x00\x01\x00\x00\x03\x16\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/src/shoguneditor/resources3.py b/src/shoguneditor/resources3.py deleted file mode 100644 index 9c8fe7e..0000000 --- a/src/shoguneditor/resources3.py +++ /dev/null @@ -1,353 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.9.5) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - -qt_resource_data = b"\ -\x00\x00\x02\x5a\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x19\x00\x00\x00\x19\x08\x06\x00\x00\x00\xc4\xe9\x85\x63\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x04\xc1\x00\x00\ -\x04\xc1\x01\x11\x76\xb1\x75\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x06\x07\x32\x1a\x15\x4e\x28\x6b\x00\x00\x01\xe7\x49\x44\ -\x41\x54\x48\xc7\xed\xd4\x3f\x48\x55\x61\x18\x06\xf0\xdf\x31\x25\ -\x92\x68\x48\x34\x54\x1a\x8c\xa8\x96\x90\x20\xda\xac\x3c\x44\x53\ -\xc7\x86\x42\x10\x1b\x4a\x44\x88\x0c\xce\x14\x0d\x8d\x42\x34\x75\ -\x06\x87\x72\xc8\x08\xd4\xfe\x80\x14\x47\x08\x1a\xce\x45\x82\x1a\ -\x82\x5c\x9a\x25\x0a\x84\x68\x28\xc8\x6a\xd0\xdb\x6d\x39\x37\x2e\ -\xb7\x7b\xb3\xc5\x28\xf2\x85\x0f\xce\xf7\x9e\xe7\xfd\x9e\xf7\x79\ -\xf8\xde\x8f\x8d\xf8\xaf\x22\x4e\xb3\x3f\x43\x10\xa7\xd9\xe9\x86\ -\x75\xe4\x39\x19\xa7\xd9\x22\xb6\x04\xbf\x5b\x31\x30\x32\x6a\x66\ -\x62\xbc\x56\xb7\x1d\x38\x8f\x00\xcd\xd8\x8f\x63\x39\xe4\x53\x12\ -\x85\xdb\x1a\xd7\x92\x9c\x44\x21\x98\x99\x18\x37\x30\x32\x3a\x89\ -\x3e\x7c\xc4\x70\x12\x85\x85\x38\xcd\x1e\xe1\x60\xbd\x23\x60\x2d\ -\xbb\xba\xe3\x34\xfb\x9a\x2b\xb9\x83\xb3\xd8\x8e\x5d\xc8\xfa\xcf\ -\x0c\xb5\xe3\x3a\x0a\x35\x6a\xdf\x24\x51\x78\x0b\x1a\xeb\x28\x08\ -\xd0\x8d\x69\xb4\xe4\xe9\xc1\x6a\xdc\xa6\xe6\xe6\x73\x1d\x27\x7a\ -\xaf\x2e\xcd\x15\xba\xd0\x5b\xed\x70\xf9\xa3\xa6\x92\x24\x0a\x4b\ -\x78\x8e\x56\x5c\xc9\xd3\x4b\xd5\xb8\xd2\xea\xea\xeb\xa5\xb9\xc2\ -\x2c\xc6\xaa\x7e\xdd\x48\xa2\xf0\x59\x79\x13\xd4\xb9\x7a\xfd\xb8\ -\x87\xf7\x68\x55\x2a\x75\xbe\x4b\x67\xdb\x04\xc1\x42\x05\xf4\xc5\ -\xcc\xc4\xf8\xa1\x5c\xf5\xb7\x8a\xfc\xd3\x24\x0a\x0f\x57\x9e\xd9\ -\x50\x43\x05\x2c\xe0\x5a\xae\x64\x5e\x10\x7c\xd9\xd1\x77\x6a\x77\ -\xf1\xf3\xf2\xd6\x52\xb1\x78\xa1\xb4\xb2\x12\xe5\x04\x2f\x71\xb7\ -\xa2\xfc\x15\x8e\x56\x0f\x61\x63\x0d\x15\x0d\x98\xc3\x9e\x3c\x7d\ -\x04\x1f\xe0\xfe\xd4\xed\x20\x4e\xb3\x76\x74\x99\xbc\x39\x87\x03\ -\xf9\x82\xe9\x24\x0a\x07\x2b\x6f\xe4\x4f\x76\xe5\x04\x2d\x78\x98\ -\x44\x61\x4f\x9c\x66\x63\xb8\x84\xa6\x0a\xfc\x04\x86\x73\x7b\x8a\ -\xd8\x9c\x37\x70\x31\x89\xc2\xa9\x5a\x04\x3f\x48\x72\x5f\x77\xe2\ -\x49\x12\x85\xfb\xca\xe0\x38\xcd\x9a\x70\x3c\x5f\x3d\xd8\x9b\x93\ -\xbe\xc5\x3c\x1e\x24\x51\xf8\x78\xad\x41\x0e\x72\x05\x5d\x58\x44\ -\x27\x96\xca\xdd\xd4\xeb\xac\xde\xc0\xfe\x8a\xa4\x09\xcb\x18\x4a\ -\xa2\x70\x6a\x3d\x1e\xb1\x06\x5c\x46\x5b\xd9\xd3\x7f\xf7\xdd\xdf\ -\x88\xbf\x2e\xbe\x03\xa4\x81\xab\x62\xf1\xf6\x73\x67\x00\x00\x00\ -\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x06\xc8\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x32\x00\x00\x00\x32\x08\x06\x00\x00\x00\x1e\x3f\x88\xb1\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ -\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x05\x0a\x13\x1d\x1d\xd8\x94\x96\x00\x00\x00\x19\x74\x45\ -\x58\x74\x43\x6f\x6d\x6d\x65\x6e\x74\x00\x43\x72\x65\x61\x74\x65\ -\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x57\x81\x0e\x17\x00\ -\x00\x06\x30\x49\x44\x41\x54\x68\xde\xed\x9a\x5b\x4f\x5b\xd9\x15\ -\x80\xbf\xbd\xcf\x31\x60\x63\x30\x17\x73\x33\x90\x89\xc1\x06\xd2\ -\x70\xc9\x24\x21\x61\x94\x4e\x14\x42\x52\xa9\x33\xa3\xb6\xaa\x94\ -\x87\x56\xca\x48\x79\xe8\x63\x23\x65\x9e\xf3\xd0\x07\x9e\x53\x69\ -\xfe\x40\xa5\xe6\xb5\x1a\x55\x6d\x47\xed\x68\x4a\x94\xcb\x24\x21\ -\x40\xc3\x04\x0c\x61\x08\x81\xe0\x70\xb1\xb9\x18\x13\x63\xf0\xf5\ -\x9c\x3e\x98\x9a\x30\x10\xc3\x0c\x4c\xcb\x89\xba\x9e\xec\xb3\xcf\ -\x3e\x7b\x7f\x7b\xad\xbd\xd6\xda\xeb\x1c\xa1\xeb\xba\xce\xba\x0c\ -\x8e\x8c\x71\xfd\xf7\x7f\xe0\xc9\x84\x8f\x70\x5c\xa0\x0b\xc9\x41\ -\x12\xa1\x6b\xe4\x9a\x74\x5a\x9c\xe5\x74\x5e\xbb\x42\x53\x83\xeb\ -\xb5\xb6\x75\x90\x4f\x3a\x3f\xe5\x66\xd7\xc0\x81\x9b\x7c\x26\xa8\ -\xcb\x1d\xcd\xdc\xb8\x7e\x75\x03\xe4\x93\xce\x4f\xf9\xe3\x2d\x0f\ -\x46\x94\x8f\xcf\x37\x72\xe3\xfa\x55\xe4\xe0\xc8\x18\x37\xbb\x06\ -\x30\xaa\xdc\xec\x1a\x60\x70\x64\x0c\x65\x3a\x9e\xf7\x3b\x6f\x60\ -\xcd\xb0\x20\x08\xc1\xd8\xe8\x28\xf2\xc9\x84\x0f\xa3\xcb\x93\x09\ -\x1f\x32\x1c\x17\x86\x07\x09\xc7\x05\xd2\x28\x5e\x2a\x93\xe8\x42\ -\x62\x7c\x8a\x75\x79\x6b\x40\x54\xc3\x6b\x42\x91\x14\x97\x15\x19\ -\x5b\x23\xe6\x5c\x33\xa7\xda\x4f\x60\xb1\x5a\x8c\xab\x11\x77\xb3\ -\x8b\xba\x66\x17\x52\x0a\x06\xba\x3d\xc6\x02\x51\x4d\x2a\xf5\xc7\ -\xdc\xb8\x9b\x5c\x58\xf2\xcc\x48\x29\xf1\xf4\x0c\xb3\xba\xb2\x76\ -\x30\x40\xa4\x22\x51\x55\x25\x1d\xa9\x01\x04\x60\xca\xce\xa2\xd0\ -\x6e\xc3\xee\xb0\x53\xe5\x74\x50\x60\x2f\xd8\xd4\x2f\xb2\x1a\xe1\ -\xf9\xd0\xf8\x7f\x67\xb3\x0b\x01\xaa\x80\x84\x06\xfa\x1b\xee\x69\ -\x6e\x6b\xa4\xae\xd9\x05\x42\x20\xa5\x40\x2a\x12\x45\x51\x76\x7c\ -\xf6\xf4\xc4\x2c\xa1\xe0\xca\x0f\x0b\x22\x05\xbc\x53\xa0\x50\x68\ -\x96\xa8\x32\x05\x12\x8a\x6a\x4c\x06\x93\xc4\x92\x1b\xf7\x55\x3a\ -\x1d\xb8\x1a\x6b\xc8\x36\x67\x7f\xb7\x20\xa8\xeb\x3c\xbe\xdb\xff\ -\xc3\xba\xdf\x6c\x05\x5a\xab\xb3\x30\xab\x20\xc4\x46\x0a\x54\x6c\ -\x11\x54\xda\x14\x7a\xa7\xe2\x84\x63\x3a\x42\x08\xea\x5b\xdc\x98\ -\x73\xcd\xdf\x79\x0c\x4f\xcf\x30\x91\xb5\xe8\xfe\x04\x44\x21\x04\ -\x47\x8e\xd7\xf3\xab\xdf\x5e\xc2\x6a\xb3\xa6\xaf\xb7\x1d\xca\xc2\ -\x62\x12\x9b\x20\xfe\x73\x7f\x96\x22\x38\x5d\x6d\x42\x11\xa9\x55\ -\x7d\x39\x3e\x4d\x64\x35\x42\x2c\x1a\xe7\xb5\xc3\x6a\x46\x89\xac\ -\x46\x18\x7c\x34\xb4\x3f\x01\x31\xaf\xc0\xca\x7b\x17\x4f\x53\x56\ -\x5d\xca\xa2\x3f\xc0\xca\x72\xca\x56\x6b\x8b\x14\xb2\xd5\xcc\x89\ -\xa8\x2a\x05\xf5\x25\x2a\xc3\x73\x09\x9e\x0d\x8c\xf1\x72\x6c\x8a\ -\x1c\x4b\x36\x67\x3f\x3c\x83\xad\xd8\xb6\xe3\xd8\x8f\xef\x7d\x4d\ -\x32\x91\xdc\x3b\x88\xc5\x6a\xa1\xe3\x97\xe7\x08\x2e\x2c\xd3\xf5\ -\xd9\xed\x34\x04\x40\xa9\x75\x77\x4a\xb6\x5b\xe4\xa6\x15\x8e\xac\ -\x46\x08\xaf\xac\xee\x08\x32\xeb\xf5\xe1\x7d\x36\xb5\x3f\x29\xca\ -\xc5\x4b\xed\xe4\x15\xe4\xf1\x7c\x68\x82\x99\x17\xb3\x5b\x56\x7b\ -\xb7\xde\xcc\x24\x21\xae\x41\x61\x49\x21\x65\xd5\xa5\x14\x14\x65\ -\x86\x88\x45\x62\x78\x7a\x86\x89\xc7\xe2\x7b\x07\x39\x75\xfe\x24\ -\xf9\x85\xf9\x00\xe4\xe6\x59\xc8\xb1\xe4\x90\x4c\x26\x89\x47\x53\ -\x0f\x4f\x6a\xfa\x7a\x14\xc8\x2c\x9a\xbe\x01\xf1\xc1\xaf\x7f\x82\ -\x54\x76\xd6\xe4\xf3\xa1\x71\x7c\x5e\xff\xde\x93\x46\x5b\xb1\x8d\ -\xfa\x63\xee\x4d\x69\x82\xab\xa9\x96\xc8\x6a\x84\x5b\x7f\xbe\x4b\ -\xc0\x1f\xc0\x17\xd2\x70\x65\xef\x3c\xa9\xc5\x55\x6d\xdd\x4c\xcd\ -\xbb\xda\xe4\xb3\x5e\x1f\x7d\x77\xfa\xf7\x27\x8d\x77\x37\xd5\x6e\ -\x19\x54\x08\x81\x39\xd7\x4c\x95\xd3\x01\xc0\x44\x30\xc9\x4a\x54\ -\xcb\x6c\x22\x09\x9d\x67\x0b\x89\xf4\x04\xbb\xff\xd9\xb3\xad\xb9\ -\xa4\xa1\xfd\x01\xee\x7d\xfe\x60\xff\xce\x23\x25\x15\x76\x84\x10\ -\xcc\xcf\x2c\x6c\xda\x1b\x8b\xfe\x00\x9e\xde\xe1\xf5\x40\x05\x7d\ -\xd3\x71\x42\x6f\x80\x59\x8b\xeb\xf4\x4c\xc5\x89\xaf\x37\x6b\x49\ -\x8d\x89\xa7\x93\x28\xea\xf6\x91\x3c\xfc\x2a\xcc\xfd\xbf\x3f\x24\ -\xfa\x5a\xcc\xd8\x93\x69\x99\x73\xcd\xf8\xa7\xe6\xe8\xbd\xfd\x2f\ -\xec\xe5\xc5\xb4\xb6\x9f\x48\xb7\x3d\xf8\xe2\x11\x5a\x32\x35\x33\ -\x53\x96\x4a\x2c\x96\xa0\xdb\x1b\xa3\xca\xa6\xe0\xc8\x57\x30\xab\ -\x82\x58\x52\x67\x26\xa4\x31\xb9\x94\x44\x5f\x8f\xe8\xd5\xae\x2a\ -\xbe\xbe\x3f\xc0\xbb\x3f\x6e\x46\xca\xad\x6b\xba\x16\x5e\xe3\xc1\ -\x17\x8f\x58\x0e\xbc\xda\xd9\x79\x14\xb7\xff\x46\xdf\x35\x8c\xd5\ -\x4c\xdb\x85\x56\xaa\x6a\x2a\x37\x5d\x5f\x9a\x0f\x72\xf7\x6f\x5f\ -\x51\x71\xa8\x9c\xba\x63\x6e\x46\xfa\x47\x19\xf3\x8c\xa3\x6b\xdb\ -\x6b\xa5\xb0\xa4\x80\x8f\x2e\xff\x34\xad\x91\xed\x36\x7a\x38\x14\ -\xa6\xfb\xcb\xde\x2d\x5e\x71\xcf\x1a\x29\xab\x2e\xa5\xed\xc2\x29\ -\xf2\x0b\xf3\xb6\x9d\xd8\xc5\x4b\xe7\xc9\xb1\xe4\x20\xa5\xa4\xed\ -\x42\x2b\xde\x51\x2f\xd1\x48\x0c\x53\x96\x89\x23\xc7\xeb\x41\xc0\ -\xb3\x81\x31\xd6\xc2\x11\x5e\x2d\x85\x36\x65\xbe\xdf\x96\xb9\x99\ -\x79\x7a\xba\xfa\x58\x9a\x0f\xee\xef\x51\xb7\xd2\xe9\xe0\x74\xc7\ -\x49\xb2\x32\x24\x76\x16\xab\x25\xfd\x3b\x99\x48\x12\x8d\xc4\x52\ -\x91\xbe\xb1\x86\xa6\xb6\xa3\xe9\xd5\xf7\xf4\x0c\x23\x15\x49\x74\ -\x2d\xba\x6d\xa2\x38\x3e\x3c\x41\xdf\xed\xc7\xe9\xfe\xfb\x06\xe2\ -\x6a\xac\xe1\xe4\xb9\xe3\xdc\xf9\xeb\x57\xcc\x4e\xfa\x38\xf7\xb3\ -\xf7\xa9\x74\x3a\x32\xfb\x7d\x91\xea\x37\xf3\x62\x16\xd7\x51\x67\ -\xda\xfe\xdd\x4d\xb5\xd8\x2b\xec\x54\xd7\x56\x6e\xe9\x12\x5d\x8b\ -\x32\xd0\xed\x61\xa4\x7f\xf4\xfb\xe5\x7d\x6f\xda\x23\x52\x91\xb4\ -\xbc\xd7\x44\x5d\x8b\x9b\x3b\x7f\xb9\x87\xef\xe5\x46\x20\x72\x1e\ -\x39\x4c\xed\x51\x27\xf6\xf2\x62\x4c\x59\xa6\xbd\x15\xd7\x42\xab\ -\xcc\xbc\x98\x65\xa8\xf7\x29\xa1\x60\xe8\xfb\x27\xb0\xdb\x81\x28\ -\xaa\xc2\xe9\x8e\x56\xde\xa9\xab\xe6\xcb\x3f\xdd\x62\x61\x76\x71\ -\xab\x2a\x55\x85\xa2\xb2\x22\x4a\x1c\x76\x1c\x87\x2b\x28\xa9\xb0\ -\xbf\xd1\x85\x6e\x27\xfe\xa9\x39\xbc\x63\x53\xf8\xbc\x7e\x82\x0b\ -\x41\xf6\x2a\xdb\x82\xb4\xff\xe2\x2c\x55\x35\x95\x74\x7d\x76\x7b\ -\x57\x5e\x43\x55\x15\xa4\xaa\x90\x67\xb3\x52\x56\x5d\x8a\xad\xd8\ -\x46\x7e\x41\x1e\xd9\xe6\x6c\x84\x10\x68\x9a\x46\xf8\x55\x98\x50\ -\x30\x84\x7f\x6a\x8e\xb9\xe9\x05\x12\xb1\x38\x89\x6f\x65\xb0\xfb\ -\x56\xd7\x12\x42\xd0\xfe\xf3\xb3\x54\xd6\x38\xf0\xf4\x0e\x33\x3b\ -\xb9\xbb\x02\x77\x22\x91\x84\x44\x92\xc5\x48\x80\x45\x7f\xe0\x7f\ -\x5b\xa0\x53\x55\x85\xf7\x3f\x3a\x43\x65\x8d\x83\xe5\xc5\x65\xbe\ -\xe9\x1f\xdd\xf5\x41\xe7\x40\x95\x4c\x5b\xce\x34\xa7\x03\x9d\xa7\ -\x37\x55\x62\x31\x5c\xc9\xf4\x90\xbb\x8a\x1f\x9d\x68\x20\x30\xb7\ -\xc4\xbd\xcf\xef\x6f\x0a\x58\x86\x01\x31\x5b\xcd\x9c\x38\xfb\x2e\ -\x4f\x1e\x0e\x32\xf0\xd0\x98\xef\x11\x01\xd4\x9a\x86\xc3\xdc\xff\ -\x47\x37\x73\xd3\xf3\x46\x2e\x03\xa3\x0e\xf5\x3d\xe5\x6d\x90\xff\ -\xbf\xe8\x39\x70\x20\x42\xd7\x0c\x0f\x21\x74\x0d\x99\x6b\xd2\x0d\ -\x0f\x92\x6b\xd2\x91\x2d\xce\x72\xc3\x83\xb4\x38\xcb\x91\x9d\xd7\ -\xae\x60\x64\xf3\x12\xba\x46\xe7\xb5\x2b\xc8\xa6\x06\x17\x97\x3b\ -\x9a\x0d\x0b\x72\xb9\xa3\x99\xa6\x06\x57\xca\x6b\xdd\xb8\x7e\x95\ -\x8f\xcf\x37\x1a\x4a\x33\x42\xd7\xd2\x5f\x06\xa5\xfe\xbf\x25\x1f\ -\x9e\xfd\x1b\xbe\x24\x72\x61\x2d\x7b\xa9\xe3\x00\x00\x00\x00\x49\ -\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x03\xd4\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x19\x00\x00\x00\x19\x08\x06\x00\x00\x00\xc4\xe9\x85\x63\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ -\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x06\x07\x38\x21\x5e\xaa\x29\xc5\x00\x00\x00\x19\x74\x45\ -\x58\x74\x43\x6f\x6d\x6d\x65\x6e\x74\x00\x43\x72\x65\x61\x74\x65\ -\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x57\x81\x0e\x17\x00\ -\x00\x03\x3c\x49\x44\x41\x54\x48\xc7\xad\x96\x4d\x68\x54\x57\x14\ -\xc7\x7f\xf7\xbe\x9b\x79\x33\x19\x33\x33\x3a\x49\x2c\x49\x63\x27\ -\xb1\x28\xa2\x5d\x48\xa4\xdd\x94\x42\xa0\xd0\x6e\x82\xb4\x69\x97\ -\x65\xb4\x90\x45\x17\x89\x08\xa5\x75\x23\x66\x55\xd0\x4d\x69\x02\ -\xdd\xfa\x01\xb6\x5d\xd6\xb6\x9b\x2e\xac\xcb\x22\x4d\x52\x14\x51\ -\x5b\xc5\x24\x33\x25\x8c\xe2\x24\xea\xc4\xc4\x99\xf7\xde\xbd\x2e\ -\xe6\xa6\xce\x64\xbe\x82\xe6\xc0\x83\xc7\xcc\x39\xf7\xc7\xf9\x9f\ -\x73\xde\xb9\x22\x39\x34\x4a\x03\x4b\x03\xc3\xc0\x20\xd0\x03\x84\ -\xea\xf8\x94\x80\x45\x60\x06\xf8\x15\x38\x5f\xef\x20\xd9\xe0\xf0\ -\x39\xe0\x1c\x30\x02\xa4\x1a\x00\xb0\xbf\xa7\xac\xdf\x39\x1b\x97\ -\x6e\x05\x99\xb4\xce\x29\x5e\xce\x52\x36\x7e\xb2\x11\x64\x12\x18\ -\x63\x6b\x6c\xac\x12\x24\x2b\x24\xda\x2a\x40\x25\x28\x0d\x20\x6c\ -\xe1\xe7\xca\xa9\x1a\x02\x5f\x63\x00\x21\x24\x42\x68\xb4\x06\xc4\ -\x26\x8e\xd4\x20\x94\x83\x53\xed\x3b\x0f\xf4\x2b\x4b\x2b\x03\x74\ -\x94\xc1\x77\xf6\x90\x6a\x87\xbb\xff\xdc\x21\xdf\xd1\xcb\xa1\x54\ -\x1c\x27\x30\x98\xa6\x04\x81\x32\x2b\x4c\x5f\xfd\x97\x85\x67\xc1\ -\xc6\x1a\xa5\x95\x6d\x53\x04\x01\x6b\xc5\xd7\xf9\xf2\xeb\x51\x3e\ -\xec\xf4\xf8\xe5\xec\x05\xae\xf4\x8c\xf0\xed\x07\x5d\x9b\x54\xe7\ -\x29\xa7\xd3\x27\x39\x9d\x29\x20\x45\x55\x3a\xc3\xca\xce\x01\x06\ -\x49\x9b\x5c\xe6\xca\x5f\xd7\x71\x63\x9a\x3f\x6e\xe6\xb8\x93\x9b\ -\xe1\xf7\xae\x37\x68\xf3\x83\xa6\x99\x18\x23\x09\xbb\x39\xae\x15\ -\x3c\x44\xad\xb6\x83\xca\x0e\x9a\xed\x02\x8f\x4c\x26\xc7\xed\x0e\ -\xcd\x83\xc7\xab\xe4\x9d\x87\xdc\x9c\x6f\x23\xec\x07\xe8\x66\x14\ -\x21\x09\xc9\x07\x2c\x7b\xba\xde\xbf\x3d\x6a\x7d\xd0\x04\x9a\x92\ -\x4e\xf2\xe9\xe1\x21\x3e\x4e\xfa\x24\x56\xb2\xfc\xb9\xf3\x3d\x8e\ -\x1d\xde\x85\x09\x74\x53\xa1\x0c\x02\xe5\xe4\x59\xbc\x34\xcd\xd5\ -\x95\xd2\xc6\x6c\x42\xea\x85\xa3\xc4\x71\x56\x98\x9d\xbe\xc1\xf6\ -\x58\xc0\xf5\x85\x3c\x99\xc2\x6d\x2e\x4f\x17\x70\x74\xeb\xc2\x3b\ -\xf2\x21\x77\xd7\xfc\xba\x8d\x28\x92\x43\xa3\xff\xc7\xeb\x40\xd1\ -\xb7\xb7\x8f\xfe\x90\xe1\x5e\x26\x4b\x21\xd2\xc5\xbe\x9e\x38\x2e\ -\x34\x87\x08\x81\x09\x9e\x70\xeb\x56\x8e\x25\xbf\x36\x6b\xf5\xe2\ -\xd5\xe7\x99\xb7\x8b\x2f\x8e\x1f\xe5\x93\xed\x01\x17\x2f\xfe\xc4\ -\x6c\xe7\xfb\x9c\xf9\x68\x37\xb2\x85\x5c\x18\x89\x1b\x5e\xe4\xd4\ -\xe7\xdf\x73\xe1\xfe\x2a\x42\x34\x80\x08\xc0\xd0\x46\x57\x34\x4a\ -\x3c\xee\xd3\x1d\x09\x11\x89\x27\x48\x6c\x0b\x83\xdf\x42\x2e\x23\ -\x70\xdd\x04\x09\x25\xca\x83\x5c\x47\xae\xe2\x7a\xf1\x8d\x11\x74\ -\x74\xc4\x88\xb9\xf0\x78\xf9\x09\x5e\x38\xc2\x8e\x68\x18\x89\x69\ -\xae\x97\x10\x48\xbf\xc4\xfd\x47\x4f\xf1\x4c\x8d\x63\x49\xd9\x7d\ -\x90\x02\x4d\xb1\xf8\x1a\x63\x13\x9f\x31\xd2\xe9\xf1\xf3\x8f\x97\ -\xf8\xbb\xfb\x5d\x4e\x0c\xbf\x89\xeb\xf9\x2d\x0a\xef\x10\x71\xfe\ -\xe3\x9b\xaf\x7e\xe0\xb7\xa5\x35\x44\xb5\x5e\x8b\xca\x2e\x9c\x94\ -\x40\x13\x98\x18\x87\xf6\x0f\xf0\x56\xa7\xcf\xbd\x81\x24\xf9\xde\ -\x7d\xbc\xdd\xdf\xbd\xc9\x89\x8f\xb3\x67\x9b\xc2\x2c\xd5\xc8\x35\ -\x23\x92\x43\xa3\xe9\xf2\x0e\x30\x04\x3a\xc2\x81\x83\x03\xf4\xb9\ -\x86\xec\x42\x96\xa5\xf0\x4e\x0e\xf4\x46\x91\xda\xb4\x9e\x13\xb1\ -\xca\x8d\xd9\x39\xb2\xc5\x60\x23\xe4\x48\xcd\x57\xd8\xf7\x03\x02\ -\x03\x8e\xe3\x20\x8d\xc6\x6f\x01\xa8\x02\x35\xf9\x0a\x03\x4c\x94\ -\xb3\x11\x28\xa5\x2a\xfa\xda\x21\xe4\xbc\xd2\x4e\x99\xa8\x5c\x5a\ -\xe7\x81\xa9\x2d\x5e\x5a\x53\xeb\x17\x8b\xca\xf5\x3b\xbe\x85\xa0\ -\x29\x7b\x5e\xdd\x8b\xc4\x38\x70\xc4\x6a\xf9\x32\x36\x6f\xe3\xc7\ -\xab\x1a\xbc\xbd\x7f\x70\xa3\xe3\x35\xe0\x3b\x1b\x10\x00\xed\xf6\ -\x71\x1a\xdc\xbb\xb2\xc0\x65\xe0\x0c\x70\xd4\xc6\x57\xd9\x73\x46\ -\x77\x21\x31\x4b\xbb\x34\xee\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ -\x42\x60\x82\ -\x00\x00\x03\x12\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x19\x00\x00\x00\x19\x08\x06\x00\x00\x00\xc4\xe9\x85\x63\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\ -\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xe2\x07\x06\x07\x38\x0b\x85\x11\xe0\x13\x00\x00\x00\x19\x74\x45\ -\x58\x74\x43\x6f\x6d\x6d\x65\x6e\x74\x00\x43\x72\x65\x61\x74\x65\ -\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\x57\x81\x0e\x17\x00\ -\x00\x02\x7a\x49\x44\x41\x54\x48\xc7\xad\x96\x4f\x48\x14\x51\x1c\ -\xc7\x3f\xef\xcf\x8e\xeb\xb2\x42\x12\x29\x88\xd8\x2a\x06\x51\xe0\ -\xc5\xa0\x63\x14\x9e\x02\x2f\x99\xe7\xed\xd2\xa1\x83\x9e\xba\x75\ -\xf1\xd0\x31\x08\xdc\xba\x54\x07\x17\xba\xa5\x10\x49\xd8\x29\x3a\ -\x75\xca\x88\x8e\x85\x29\x59\x8b\x08\x91\x58\x64\xee\xec\x7b\xaf\ -\x43\x6f\x74\xc6\x66\x9b\x55\xf6\x07\xc3\xc0\x9b\xdf\xef\xf7\x9d\ -\xf7\xfd\xfd\x15\x67\xaf\xde\xa4\x89\x94\x81\x71\x60\x14\xe8\x03\ -\x82\x14\x9d\x3a\x50\x03\x96\x81\x45\xa0\x9a\xe6\x48\x36\x71\xbe\ -\x0a\xcc\x01\x13\x40\xa9\x09\x00\xfe\xbc\xe4\xf5\xe6\xbc\x5d\x39\ -\x0b\x64\xd6\x2b\x97\x38\x9a\x94\xbc\xfd\x6c\x33\x90\x59\x60\x8a\ -\xf6\xc8\x54\x1c\x48\xc7\x28\x4a\x00\x58\x63\x30\x0e\xc4\x21\x3c\ -\x0b\xa5\x50\x22\x01\xb4\x0c\x54\x23\x90\x99\xb8\xb2\x09\x1d\xfd\ -\xa7\x86\x18\x28\x6a\xac\x6b\x15\xa2\xc1\xe6\xfa\x3a\x2b\x5b\x06\ -\x2d\xf7\x90\x66\x22\x90\x72\x3c\x06\x66\x37\xe4\xf4\x95\x32\xd5\ -\xc9\x61\xbe\x6d\xed\x20\x44\x6b\x77\x11\xb2\x83\x2e\x53\xe3\xd6\ -\xed\x47\xbc\xdc\x84\x9c\xdc\x8b\x51\x59\xfb\x34\xf5\xe2\x68\xd8\ -\x1c\xc3\xfd\x3d\xac\xbe\x78\xc8\xa5\x7b\x1f\x29\xe4\x65\x36\x80\ -\x70\x38\xd9\xcb\xdd\xca\x0d\x4e\xf6\x16\x60\xe3\x17\xec\xdf\x66\ -\x5c\xfb\x3a\x48\x88\x71\x0e\xa7\xf2\x1c\xeb\x2a\x50\xe8\x90\xb8\ -\x4c\x10\x40\x06\x04\x16\xec\xbf\xfc\x8e\x4a\x5f\x68\x29\x96\xd1\ -\xdd\xb2\xc5\x45\x5a\xe9\xcc\xf6\xc9\xff\x14\x5a\xbb\x24\x90\xed\ -\xf0\x92\x95\x1a\x3a\xed\xf2\x7f\x5f\x8e\x86\xb1\x18\xd3\x0a\x8a\ -\x03\x67\x9b\x52\xab\x53\xff\xcb\x3a\x4e\x0c\x9d\x61\x72\xac\x9b\ -\x20\x27\x5b\x8b\x8c\xe8\x66\xa8\xe0\x78\xdf\x1a\x08\x60\x2d\x3d\ -\x23\x17\xb9\x33\x72\x48\xde\x7e\xd4\x98\x77\xe9\x5d\xb8\x9e\x4c\ -\x47\xc3\xcf\xb0\x7e\xa4\xd8\xb8\xb0\xc1\xce\x6e\x03\x97\x0c\x52\ -\x5d\xfb\x79\x50\x8a\xa8\x0a\x3a\xe0\xf5\xd3\xe7\x54\xc4\x79\x06\ -\xf2\x02\x0b\x38\x0b\xc5\xe3\x7d\x5c\x18\x1d\xa0\xe8\x1d\xd8\xed\ -\xaf\x3c\x7b\xb3\x4e\xdd\xec\xd7\xca\xc6\x87\x77\xbc\x5a\xf9\x4d\ -\x4e\x25\x08\xaa\x69\xdf\xc4\x4a\xb1\x2e\x07\xdb\x9f\xb9\xff\xe0\ -\xd3\x5e\xdf\x0a\x9d\x62\x6c\xf2\x3a\x97\xcf\xc5\x28\x28\x36\x58\ -\x7a\xfc\x84\xa5\xcd\x10\x15\x9d\x29\x4d\x3e\xd0\x07\xb3\x6d\x59\ -\xfb\x89\x36\x71\xa0\x9d\xd2\x99\x57\xfb\x21\x42\xb1\xb1\xf2\x96\ -\xca\xfc\x17\x94\x71\x38\x01\x32\xfc\x4e\x2d\xcc\x51\xec\x94\x64\ -\xd4\xc1\xa2\xf0\xe3\x77\x35\x6b\x50\x39\x6b\x31\xce\x25\xb2\x50\ -\x29\x99\x55\x23\x6b\xc0\xa0\x4c\x6b\xf5\xe9\x5d\x56\xa2\x95\x8a\ -\x3d\xb2\x95\x59\x33\x13\x9f\x8c\x55\xa0\xd2\xe6\x76\x52\x89\x16\ -\x8b\x38\x9d\xd3\x6d\x04\xaa\x78\x7f\xa9\x8b\xc4\x34\x70\xcd\x73\ -\x79\x14\x59\xf3\xf6\xd3\x59\x2b\x51\x15\x18\xf4\xca\x0b\xde\xb0\ -\x59\x75\xd6\xfd\xf7\x05\xaf\x3f\x98\xb6\x7b\xfd\x01\x3c\xde\xc0\ -\xc7\xae\x7e\x85\x10\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ -\x82\ -" - -qt_resource_name = b"\ -\x00\x07\ -\x07\x3b\xe0\xb3\ -\x00\x70\ -\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ -\x00\x0c\ -\x0b\x85\xb6\xe2\ -\x00\x73\ -\x00\x68\x00\x6f\x00\x67\x00\x75\x00\x6e\x00\x65\x00\x64\x00\x69\x00\x74\x00\x6f\x00\x72\ -\x00\x18\ -\x04\x35\xb9\x07\ -\x00\x73\ -\x00\x68\x00\x6f\x00\x67\x00\x75\x00\x6e\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x32\x00\x35\x00\x78\x00\x2d\x00\x32\ -\x00\x35\x00\x70\x00\x78\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x22\ -\x02\x09\x81\xc7\ -\x00\x73\ -\x00\x68\x00\x6f\x00\x67\x00\x75\x00\x6e\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x35\x00\x30\x00\x78\x00\x35\x00\x30\ -\x00\x70\x00\x78\x00\x2d\x00\x72\x00\x6f\x00\x75\x00\x6e\x00\x64\x00\x2d\x00\x62\x00\x6c\x00\x75\x00\x65\x00\x2e\x00\x70\x00\x6e\ -\x00\x67\ -\x00\x0f\ -\x0a\x75\xc5\x07\ -\x00\x6c\ -\x00\x61\x00\x79\x00\x65\x00\x72\x00\x73\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x15\ -\x01\xd9\xbe\x27\ -\x00\x61\ -\x00\x70\x00\x70\x00\x6c\x00\x69\x00\x63\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\x00\x2d\x00\x6c\x00\x6f\x00\x67\x00\x6f\ -\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x14\x00\x02\x00\x00\x00\x04\x00\x00\x00\x03\ -\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x02\ -\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x00\x02\x5e\ -\x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x09\x2a\ -" - -qt_resource_struct_v2 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x14\x00\x02\x00\x00\x00\x04\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x02\ -\x00\x00\x01\x64\x84\x0f\xfb\xd9\ -\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x00\x02\x5e\ -\x00\x00\x01\x64\x84\x0f\xfb\xd9\ -\x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x64\x84\x0f\xfb\xd9\ -\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x09\x2a\ -\x00\x00\x01\x64\x84\x0f\xfb\xd9\ -" - -qt_version = QtCore.qVersion().split('.') -if qt_version < ['5', '8', '0']: - rcc_version = 1 - qt_resource_struct = qt_resource_struct_v1 -else: - rcc_version = 2 - qt_resource_struct = qt_resource_struct_v2 - -def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/src/shoguneditor/shogun_editor.py b/src/shoguneditor/shogun_editor.py deleted file mode 100644 index 613a1f8..0000000 --- a/src/shoguneditor/shogun_editor.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- -''' -/*************************************************************************** - ShogunEditor - A QGIS plugin to connect with a Shogun - GIS client instance on a remote or local server - and edit it's content from QGIS - - ------------------- - begin : 2018-05-11 - copyright : (C) 2018 by terrestris GmbH & Co. KG - email : jgrieb (at) terrestris.de, info (at) terrestris.de - git sha : $Format:%H$ - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ -''' - -__author__ = 'Jonas Grieb' -__date__ = 'July 2018' - -import sys -import os.path - -if sys.version_info[0] >= 3: - from qgis.PyQt.QtWidgets import QAction - from qgis.PyQt.QtGui import QIcon - from qgis.PyQt.QtCore import Qt - from . import resources3 -else: - from PyQt4.QtCore import Qt - from PyQt4.QtGui import QAction, QIcon - from . import resources2 - -from .gui.editor import Editor - - -class ShogunEditor: - '''This class establishes the plugin icon in the qgis gui and menu, and - calls the class Editor() when the plugin is opened''' - - def __init__(self, iface): - - self.iface = iface - self.plugin_dir = os.path.dirname(__file__) - - # the following would be needed to add internationalization, maybe in a - # future release - - # locale = QSettings().value('locale/userLocale')[0:2] - # locale_path = os.path.join( - # self.plugin_dir, - # 'i18n', - # 'ShogunEditorPrototyp_{}.qm'.format(locale)) - - # if os.path.exists(locale_path): - # self.translator = QTranslator() - # self.translator.load(locale_path) - - # if qVersion() > '4.3.3': - # QCoreApplication.installTranslator(self.translator) - - self.actions = [] - self.menu = '&Shogun Editor' - self.toolbar = self.iface.addToolBar(u'ShogunEditor') - self.toolbar.setObjectName(u'ShogunEditor') - - self.pluginIsActive = False - self.editor = None - - - def initGui(self): - - iconPath = ':/plugins/shoguneditor/shogun-logo-50x50px-round-blue.png' - openEditorAction = QAction( - QIcon(iconPath), 'Shogun Editor', self.iface.mainWindow()) - self.actions.append(openEditorAction) - openEditorAction.triggered.connect(self.openEditor) - self.toolbar.addAction(openEditorAction) - self.iface.addPluginToWebMenu(self.menu, openEditorAction) - - - def onClosePlugin(self): - if self.editor is None: - return - connections_nr = self.editor.topitem.childCount() - for x in range(connections_nr): - try: - connection = self.editor.topitem.child(x) - connection.disconnectSignals() - except: - pass - - - def unload(self): - for action in self.actions: - self.iface.removePluginWebMenu( - self.menu, - action) - self.iface.removeToolBarIcon(action) - del self.toolbar - - - def openEditor(self): - if not self.pluginIsActive: - self.pluginIsActive = True - - self.editor = Editor(self.iface) - self.iface.addDockWidget(Qt.RightDockWidgetArea, self.editor.dock) - self.editor.dock.show() - - else: - self.editor.dock.show()