├── test ├── tenbytenraster.keywords ├── __init__.py ├── tenbytenraster.prj ├── tenbytenraster.asc ├── tenbytenraster.asc.aux.xml ├── tenbytenraster.lic ├── test_resources.py ├── test_raster_tracer_dockwidget.py ├── tenbytenraster.qml ├── test_translations.py ├── utilities.py ├── test_init.py ├── test_qgis_environment.py └── qgis_interface.py ├── exceptions.py ├── icon.png ├── screen.gif ├── resources.qrc ├── scripts ├── compile-strings.sh ├── run-env-linux.sh └── update-strings.sh ├── i18n └── af.ts ├── help ├── source │ ├── index.rst │ └── conf.py ├── make.bat └── Makefile ├── LICENSE ├── line_simplification.py ├── __init__.py ├── .gitignore ├── raster_tracer_dockwidget.py ├── utils.py ├── metadata.txt ├── README.md ├── pb_tool.cfg ├── raster_tracer_dockwidget_base.ui ├── plugin_upload.py ├── autotrace.py ├── astar.py ├── pointtool_states.py ├── Makefile ├── pylintrc ├── raster_tracer.py ├── pointtool.py └── resources.py /test/tenbytenraster.keywords: -------------------------------------------------------------------------------- 1 | title: Tenbytenraster 2 | -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class OutsideMapError(Exception): 3 | pass 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkondratyev85/raster_tracer/HEAD/icon.png -------------------------------------------------------------------------------- /screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkondratyev85/raster_tracer/HEAD/screen.gif -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # import qgis libs so that ve set the correct sip api version 2 | import qgis # pylint: disable=W0611 # NOQA -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/tenbytenraster.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /scripts/compile-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LRELEASE=$1 3 | LOCALES=$2 4 | 5 | 6 | for LOCALE in ${LOCALES} 7 | do 8 | echo "Processing: ${LOCALE}.ts" 9 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 10 | # about what is made available. 11 | $LRELEASE i18n/${LOCALE}.ts 12 | done 13 | -------------------------------------------------------------------------------- /i18n/af.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @default 5 | 6 | 7 | Good morning 8 | Goeie more 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/tenbytenraster.asc: -------------------------------------------------------------------------------- 1 | NCOLS 10 2 | NROWS 10 3 | XLLCENTER 1535380.000000 4 | YLLCENTER 5083260.000000 5 | DX 10 6 | DY 10 7 | NODATA_VALUE -9999 8 | 0 1 2 3 4 5 6 7 8 9 9 | 0 1 2 3 4 5 6 7 8 9 10 | 0 1 2 3 4 5 6 7 8 9 11 | 0 1 2 3 4 5 6 7 8 9 12 | 0 1 2 3 4 5 6 7 8 9 13 | 0 1 2 3 4 5 6 7 8 9 14 | 0 1 2 3 4 5 6 7 8 9 15 | 0 1 2 3 4 5 6 7 8 9 16 | 0 1 2 3 4 5 6 7 8 9 17 | 0 1 2 3 4 5 6 7 8 9 18 | CRS 19 | NOTES 20 | -------------------------------------------------------------------------------- /test/tenbytenraster.asc.aux.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Point 4 | 5 | 6 | 7 | 9 8 | 4.5 9 | 0 10 | 2.872281323269 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /help/source/index.rst: -------------------------------------------------------------------------------- 1 | .. RasterTracer documentation master file, created by 2 | sphinx-quickstart on Sun Feb 12 17:11:03 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to RasterTracer's documentation! 7 | ============================================ 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | Indices and tables 15 | ================== 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | 21 | -------------------------------------------------------------------------------- /test/tenbytenraster.lic: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tim Sutton, Linfiniti Consulting CC 5 | 6 | 7 | 8 | tenbytenraster.asc 9 | 2700044251 10 | Yes 11 | Tim Sutton 12 | Tim Sutton (QGIS Source Tree) 13 | Tim Sutton 14 | This data is publicly available from QGIS Source Tree. The original 15 | file was created and contributed to QGIS by Tim Sutton. 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /scripts/run-env-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | QGIS_PREFIX_PATH=/usr/local/qgis-2.0 4 | if [ -n "$1" ]; then 5 | QGIS_PREFIX_PATH=$1 6 | fi 7 | 8 | echo ${QGIS_PREFIX_PATH} 9 | 10 | 11 | export QGIS_PREFIX_PATH=${QGIS_PREFIX_PATH} 12 | export QGIS_PATH=${QGIS_PREFIX_PATH} 13 | export LD_LIBRARY_PATH=${QGIS_PREFIX_PATH}/lib 14 | export PYTHONPATH=${QGIS_PREFIX_PATH}/share/qgis/python:${QGIS_PREFIX_PATH}/share/qgis/python/plugins:${PYTHONPATH} 15 | 16 | echo "QGIS PATH: $QGIS_PREFIX_PATH" 17 | export QGIS_DEBUG=0 18 | export QGIS_LOG_FILE=/tmp/inasafe/realtime/logs/qgis.log 19 | 20 | export PATH=${QGIS_PREFIX_PATH}/bin:$PATH 21 | 22 | echo "This script is intended to be sourced to set up your shell to" 23 | echo "use a QGIS 2.0 built in $QGIS_PREFIX_PATH" 24 | echo 25 | echo "To use it do:" 26 | echo "source $BASH_SOURCE /your/optional/install/path" 27 | echo 28 | echo "Then use the make file supplied here e.g. make guitest" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mkondratyev85 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test_resources.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Resources test. 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | """ 10 | 11 | __author__ = 'mkondratyev85@gmail.com' 12 | __date__ = '2019-11-09' 13 | __copyright__ = 'Copyright 2019, Mikhail Kondratyev' 14 | 15 | import unittest 16 | 17 | from qgis.PyQt.QtGui import QIcon 18 | 19 | 20 | 21 | class RasterTracerDialogTest(unittest.TestCase): 22 | """Test rerources work.""" 23 | 24 | def setUp(self): 25 | """Runs before each test.""" 26 | pass 27 | 28 | def tearDown(self): 29 | """Runs after each test.""" 30 | pass 31 | 32 | def test_icon_png(self): 33 | """Test we can click OK.""" 34 | path = ':/plugins/RasterTracer/icon.png' 35 | icon = QIcon(path) 36 | self.assertFalse(icon.isNull()) 37 | 38 | if __name__ == "__main__": 39 | suite = unittest.makeSuite(RasterTracerResourcesTest) 40 | runner = unittest.TextTestRunner(verbosity=2) 41 | runner.run(suite) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /line_simplification.py: -------------------------------------------------------------------------------- 1 | from math import tan, radians, atan2 2 | 3 | def smooth(path, size=2): 4 | smoothed = [] 5 | smoothed.append(path[0]) 6 | for i in range(size, len(path)-size): 7 | xx = [x for (x,y) in path[i-size:i+size]] 8 | yy = [y for (x,y) in path[i-size:i+size]] 9 | x = sum(xx)/(len(xx)*1.0) 10 | y = sum(yy)/(len(yy)*1.0) 11 | smoothed.append((x,y)) 12 | smoothed.append(path[-1]) 13 | return smoothed 14 | 15 | def simplify(path, tolerance = 2): 16 | previous = None 17 | previousfactor = None 18 | nodes_to_delete = [] 19 | tolerance = tan(radians(tolerance)) 20 | for i, node in enumerate(path): 21 | if not previous: 22 | previous = node 23 | continue 24 | factor = atan2((node[0]-previous[0]),(node[1]-previous[1])) 25 | #factor = ((node[0]-previous[0]),(node[1]-previous[1])) 26 | if not previousfactor: 27 | previousfactor = factor 28 | continue 29 | if abs(factor-previousfactor) < tolerance: nodes_to_delete.append(i-1) 30 | #print factor, previousfactor, abs(factor-previousfactor), tolerance 31 | previous = node 32 | previousfactor = factor 33 | 34 | for i in nodes_to_delete[::-1]: 35 | path.pop(i) 36 | return path 37 | -------------------------------------------------------------------------------- /test/test_raster_tracer_dockwidget.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """DockWidget test. 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | """ 10 | 11 | __author__ = 'mkondratyev85@gmail.com' 12 | __date__ = '2019-11-09' 13 | __copyright__ = 'Copyright 2019, Mikhail Kondratyev' 14 | 15 | import unittest 16 | 17 | from qgis.PyQt.QtGui import QDockWidget 18 | 19 | from raster_tracer_dockwidget import RasterTracerDockWidget 20 | 21 | from utilities import get_qgis_app 22 | 23 | QGIS_APP = get_qgis_app() 24 | 25 | 26 | class RasterTracerDockWidgetTest(unittest.TestCase): 27 | """Test dockwidget works.""" 28 | 29 | def setUp(self): 30 | """Runs before each test.""" 31 | self.dockwidget = RasterTracerDockWidget(None) 32 | 33 | def tearDown(self): 34 | """Runs after each test.""" 35 | self.dockwidget = None 36 | 37 | def test_dockwidget_ok(self): 38 | """Test we can click OK.""" 39 | pass 40 | 41 | if __name__ == "__main__": 42 | suite = unittest.makeSuite(RasterTracerDialogTest) 43 | runner = unittest.TextTestRunner(verbosity=2) 44 | runner.run(suite) 45 | 46 | -------------------------------------------------------------------------------- /scripts/update-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LOCALES=$* 3 | 4 | # Get newest .py files so we don't update strings unnecessarily 5 | 6 | CHANGED_FILES=0 7 | PYTHON_FILES=`find . -regex ".*\(ui\|py\)$" -type f` 8 | for PYTHON_FILE in $PYTHON_FILES 9 | do 10 | CHANGED=$(stat -c %Y $PYTHON_FILE) 11 | if [ ${CHANGED} -gt ${CHANGED_FILES} ] 12 | then 13 | CHANGED_FILES=${CHANGED} 14 | fi 15 | done 16 | 17 | # Qt translation stuff 18 | # for .ts file 19 | UPDATE=false 20 | for LOCALE in ${LOCALES} 21 | do 22 | TRANSLATION_FILE="i18n/$LOCALE.ts" 23 | if [ ! -f ${TRANSLATION_FILE} ] 24 | then 25 | # Force translation string collection as we have a new language file 26 | touch ${TRANSLATION_FILE} 27 | UPDATE=true 28 | break 29 | fi 30 | 31 | MODIFICATION_TIME=$(stat -c %Y ${TRANSLATION_FILE}) 32 | if [ ${CHANGED_FILES} -gt ${MODIFICATION_TIME} ] 33 | then 34 | # Force translation string collection as a .py file has been updated 35 | UPDATE=true 36 | break 37 | fi 38 | done 39 | 40 | if [ ${UPDATE} == true ] 41 | # retrieve all python files 42 | then 43 | echo ${PYTHON_FILES} 44 | # update .ts 45 | echo "Please provide translations by editing the translation files below:" 46 | for LOCALE in ${LOCALES} 47 | do 48 | echo "i18n/"${LOCALE}".ts" 49 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 50 | # about what is made available. 51 | pylupdate4 -noobsolete ${PYTHON_FILES} -ts i18n/${LOCALE}.ts 52 | done 53 | else 54 | echo "No need to edit any translation files (.ts) because no python files" 55 | echo "has been updated since the last update translation. " 56 | fi 57 | -------------------------------------------------------------------------------- /test/tenbytenraster.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | 27 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | RasterTracer 5 | A QGIS plugin 6 | This plugin traces the underlying raster map 7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 8 | ------------------- 9 | begin : 2019-11-09 10 | copyright : (C) 2019 by Mikhail Kondratyev 11 | email : mkondratyev85@gmail.com 12 | git sha : $Format:%H$ 13 | ***************************************************************************/ 14 | 15 | /*************************************************************************** 16 | * * 17 | * This program is free software; you can redistribute it and/or modify * 18 | * it under the terms of the GNU General Public License as published by * 19 | * the Free Software Foundation; either version 2 of the License, or * 20 | * (at your option) any later version. * 21 | * * 22 | ***************************************************************************/ 23 | This script initializes the plugin, making it known to QGIS. 24 | """ 25 | 26 | 27 | # noinspection PyPep8Naming 28 | def classFactory(iface): # pylint: disable=invalid-name 29 | """Load RasterTracer class from file RasterTracer. 30 | 31 | :param iface: A QGIS interface instance. 32 | :type iface: QgsInterface 33 | """ 34 | # 35 | from .raster_tracer import RasterTracer 36 | return RasterTracer(iface) 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .noseids 107 | .tags 108 | -------------------------------------------------------------------------------- /test/test_translations.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Safe Translations Test. 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | """ 10 | from .utilities import get_qgis_app 11 | 12 | __author__ = 'ismailsunni@yahoo.co.id' 13 | __date__ = '12/10/2011' 14 | __copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' 15 | 'Disaster Reduction') 16 | import unittest 17 | import os 18 | 19 | from qgis.PyQt.QtCore import QCoreApplication, QTranslator 20 | 21 | QGIS_APP = get_qgis_app() 22 | 23 | 24 | class SafeTranslationsTest(unittest.TestCase): 25 | """Test translations work.""" 26 | 27 | def setUp(self): 28 | """Runs before each test.""" 29 | if 'LANG' in iter(os.environ.keys()): 30 | os.environ.__delitem__('LANG') 31 | 32 | def tearDown(self): 33 | """Runs after each test.""" 34 | if 'LANG' in iter(os.environ.keys()): 35 | os.environ.__delitem__('LANG') 36 | 37 | def test_qgis_translations(self): 38 | """Test that translations work.""" 39 | parent_path = os.path.join(__file__, os.path.pardir, os.path.pardir) 40 | dir_path = os.path.abspath(parent_path) 41 | file_path = os.path.join( 42 | dir_path, 'i18n', 'af.qm') 43 | translator = QTranslator() 44 | translator.load(file_path) 45 | QCoreApplication.installTranslator(translator) 46 | 47 | expected_message = 'Goeie more' 48 | real_message = QCoreApplication.translate("@default", 'Good morning') 49 | self.assertEqual(real_message, expected_message) 50 | 51 | 52 | if __name__ == "__main__": 53 | suite = unittest.makeSuite(SafeTranslationsTest) 54 | runner = unittest.TextTestRunner(verbosity=2) 55 | runner.run(suite) 56 | -------------------------------------------------------------------------------- /test/utilities.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Common functionality used by regression tests.""" 3 | 4 | import sys 5 | import logging 6 | 7 | 8 | LOGGER = logging.getLogger('QGIS') 9 | QGIS_APP = None # Static variable used to hold hand to running QGIS app 10 | CANVAS = None 11 | PARENT = None 12 | IFACE = None 13 | 14 | 15 | def get_qgis_app(): 16 | """ Start one QGIS application to test against. 17 | 18 | :returns: Handle to QGIS app, canvas, iface and parent. If there are any 19 | errors the tuple members will be returned as None. 20 | :rtype: (QgsApplication, CANVAS, IFACE, PARENT) 21 | 22 | If QGIS is already running the handle to that app will be returned. 23 | """ 24 | 25 | try: 26 | from qgis.PyQt import QtGui, QtCore 27 | from qgis.core import QgsApplication 28 | from qgis.gui import QgsMapCanvas 29 | from .qgis_interface import QgisInterface 30 | except ImportError: 31 | return None, None, None, None 32 | 33 | global QGIS_APP # pylint: disable=W0603 34 | 35 | if QGIS_APP is None: 36 | gui_flag = True # All test will run qgis in gui mode 37 | #noinspection PyPep8Naming 38 | QGIS_APP = QgsApplication(sys.argv, gui_flag) 39 | # Make sure QGIS_PREFIX_PATH is set in your env if needed! 40 | QGIS_APP.initQgis() 41 | s = QGIS_APP.showSettings() 42 | LOGGER.debug(s) 43 | 44 | global PARENT # pylint: disable=W0603 45 | if PARENT is None: 46 | #noinspection PyPep8Naming 47 | PARENT = QtGui.QWidget() 48 | 49 | global CANVAS # pylint: disable=W0603 50 | if CANVAS is None: 51 | #noinspection PyPep8Naming 52 | CANVAS = QgsMapCanvas(PARENT) 53 | CANVAS.resize(QtCore.QSize(400, 400)) 54 | 55 | global IFACE # pylint: disable=W0603 56 | if IFACE is None: 57 | # QgisInterface is a stub implementation of the QGIS plugin interface 58 | #noinspection PyPep8Naming 59 | IFACE = QgisInterface(CANVAS) 60 | 61 | return QGIS_APP, CANVAS, IFACE, PARENT 62 | -------------------------------------------------------------------------------- /test/test_init.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests QGIS plugin init.""" 3 | 4 | __author__ = 'Tim Sutton ' 5 | __revision__ = '$Format:%H$' 6 | __date__ = '17/10/2010' 7 | __license__ = "GPL" 8 | __copyright__ = 'Copyright 2012, Australia Indonesia Facility for ' 9 | __copyright__ += 'Disaster Reduction' 10 | 11 | import os 12 | import unittest 13 | import logging 14 | import configparser 15 | 16 | LOGGER = logging.getLogger('QGIS') 17 | 18 | 19 | class TestInit(unittest.TestCase): 20 | """Test that the plugin init is usable for QGIS. 21 | 22 | Based heavily on the validator class by Alessandro 23 | Passoti available here: 24 | 25 | http://github.com/qgis/qgis-django/blob/master/qgis-app/ 26 | plugins/validator.py 27 | 28 | """ 29 | 30 | def test_read_init(self): 31 | """Test that the plugin __init__ will validate on plugins.qgis.org.""" 32 | 33 | # You should update this list according to the latest in 34 | # https://github.com/qgis/qgis-django/blob/master/qgis-app/ 35 | # plugins/validator.py 36 | 37 | required_metadata = [ 38 | 'name', 39 | 'description', 40 | 'version', 41 | 'qgisMinimumVersion', 42 | 'email', 43 | 'author'] 44 | 45 | file_path = os.path.abspath(os.path.join( 46 | os.path.dirname(__file__), os.pardir, 47 | 'metadata.txt')) 48 | LOGGER.info(file_path) 49 | metadata = [] 50 | parser = configparser.ConfigParser() 51 | parser.optionxform = str 52 | parser.read(file_path) 53 | message = 'Cannot find a section named "general" in %s' % file_path 54 | assert parser.has_section('general'), message 55 | metadata.extend(parser.items('general')) 56 | 57 | for expectation in required_metadata: 58 | message = ('Cannot find metadata "%s" in metadata source (%s).' % ( 59 | expectation, file_path)) 60 | 61 | self.assertIn(expectation, dict(metadata), message) 62 | 63 | if __name__ == '__main__': 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /test/test_qgis_environment.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests for QGIS functionality. 3 | 4 | 5 | .. note:: This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | """ 11 | __author__ = 'tim@linfiniti.com' 12 | __date__ = '20/01/2011' 13 | __copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' 14 | 'Disaster Reduction') 15 | 16 | import os 17 | import unittest 18 | from qgis.core import ( 19 | QgsProviderRegistry, 20 | QgsCoordinateReferenceSystem, 21 | QgsRasterLayer) 22 | 23 | from .utilities import get_qgis_app 24 | QGIS_APP = get_qgis_app() 25 | 26 | 27 | class QGISTest(unittest.TestCase): 28 | """Test the QGIS Environment""" 29 | 30 | def test_qgis_environment(self): 31 | """QGIS environment has the expected providers""" 32 | 33 | r = QgsProviderRegistry.instance() 34 | self.assertIn('gdal', r.providerList()) 35 | self.assertIn('ogr', r.providerList()) 36 | self.assertIn('postgres', r.providerList()) 37 | 38 | def test_projection(self): 39 | """Test that QGIS properly parses a wkt string. 40 | """ 41 | crs = QgsCoordinateReferenceSystem() 42 | wkt = ( 43 | 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",' 44 | 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' 45 | 'PRIMEM["Greenwich",0.0],UNIT["Degree",' 46 | '0.0174532925199433]]') 47 | crs.createFromWkt(wkt) 48 | auth_id = crs.authid() 49 | expected_auth_id = 'EPSG:4326' 50 | self.assertEqual(auth_id, expected_auth_id) 51 | 52 | # now test for a loaded layer 53 | path = os.path.join(os.path.dirname(__file__), 'tenbytenraster.asc') 54 | title = 'TestRaster' 55 | layer = QgsRasterLayer(path, title) 56 | auth_id = layer.crs().authid() 57 | self.assertEqual(auth_id, expected_auth_id) 58 | 59 | if __name__ == '__main__': 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /raster_tracer_dockwidget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | RasterTracerDockWidget 5 | A QGIS plugin 6 | This plugin traces the underlying raster map 7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 8 | ------------------- 9 | begin : 2019-11-09 10 | git sha : $Format:%H$ 11 | copyright : (C) 2019 by Mikhail Kondratyev 12 | email : mkondratyev85@gmail.com 13 | ***************************************************************************/ 14 | 15 | /*************************************************************************** 16 | * * 17 | * This program is free software; you can redistribute it and/or modify * 18 | * it under the terms of the GNU General Public License as published by * 19 | * the Free Software Foundation; either version 2 of the License, or * 20 | * (at your option) any later version. * 21 | * * 22 | ***************************************************************************/ 23 | """ 24 | 25 | import os 26 | 27 | from qgis.PyQt import QtGui, QtWidgets, uic 28 | from qgis.PyQt.QtCore import pyqtSignal 29 | 30 | FORM_CLASS, _ = uic.loadUiType(os.path.join( 31 | os.path.dirname(__file__), 'raster_tracer_dockwidget_base.ui')) 32 | 33 | 34 | class RasterTracerDockWidget(QtWidgets.QDockWidget, FORM_CLASS): 35 | 36 | closingPlugin = pyqtSignal() 37 | 38 | def __init__(self, parent=None): 39 | """Constructor.""" 40 | super(RasterTracerDockWidget, self).__init__(parent) 41 | # Set up the user interface from Designer. 42 | # After setupUI you can access any designer object by doing 43 | # self., and you can use autoconnect slots - see 44 | # http://doc.qt.io/qt-5/designer-using-a-ui-file.html 45 | # #widgets-and-dialogs-with-auto-connect 46 | self.setupUi(self) 47 | 48 | def closeEvent(self, event): 49 | self.closingPlugin.emit() 50 | event.accept() 51 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from osgeo import gdal 2 | from qgis.core import QgsCoordinateTransform 3 | import numpy as np 4 | 5 | 6 | class PossiblyIndexedImageError(Exception): 7 | pass 8 | 9 | 10 | def get_indxs_from_raster_coords(geo_ref, xy): 11 | x, y = xy 12 | top_left_x, top_left_y, we_resolution, ns_resolution = geo_ref 13 | i = int((y - top_left_y) / ns_resolution) * -1 14 | j = int((x - top_left_x) / we_resolution) 15 | return i, j 16 | 17 | 18 | def get_coords_from_raster_indxs(geo_ref, ij): 19 | i, j = ij 20 | top_left_x, top_left_y, we_resolution, ns_resolution = geo_ref 21 | y = (top_left_y - (i + 0.5) * ns_resolution) 22 | x = top_left_x - (j + 0.5) * we_resolution * -1 23 | return x, y 24 | 25 | 26 | def get_whole_raster(layer, project_instance): 27 | provider = layer.dataProvider() 28 | extent = provider.extent() 29 | 30 | project_crs = project_instance.crs() 31 | trfm_from_src = QgsCoordinateTransform(provider.crs(), 32 | project_crs, 33 | project_instance) 34 | trfm_to_src = QgsCoordinateTransform(project_crs, 35 | provider.crs(), 36 | project_instance) 37 | 38 | dx = layer.rasterUnitsPerPixelX() 39 | dy = layer.rasterUnitsPerPixelY() 40 | top_left_x = extent.xMinimum() 41 | top_left_y = extent.yMaximum() 42 | 43 | geo_ref = (top_left_x, top_left_y, dx, dy) 44 | 45 | to_indexes = lambda x, y: get_indxs_from_raster_coords( 46 | geo_ref, 47 | trfm_to_src.transform(x, y)) 48 | to_coords = lambda i, j: trfm_from_src.transform( 49 | *get_coords_from_raster_indxs(geo_ref, (i, j))) 50 | to_coords_provider = lambda i, j:\ 51 | get_coords_from_raster_indxs(geo_ref, 52 | (i, j)) 53 | to_coords_provider2 = lambda x, y: trfm_to_src.transform(x, y) 54 | raster_path = layer.source() 55 | ds = gdal.Open(raster_path) 56 | try: 57 | band1 = np.array(ds.GetRasterBand(1).ReadAsArray()) 58 | band2 = np.array(ds.GetRasterBand(2).ReadAsArray()) 59 | band3 = np.array(ds.GetRasterBand(3).ReadAsArray()) 60 | except AttributeError: 61 | raise PossiblyIndexedImageError 62 | 63 | return ((band1, band2, band3), to_indexes, to_coords, 64 | to_coords_provider, to_coords_provider2) 65 | -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | # This file contains metadata for your plugin. 2 | 3 | # This file should be included when you package your plugin.# Mandatory items: 4 | 5 | [general] 6 | name=Raster Tracer 7 | qgisMinimumVersion=3.0 8 | description=This plugin allows user to automaticaly trace lineal features of the underlaying raster map, simply by clicking on knots of lines on map. 9 | version=0.3.3 10 | author=Mikhail Kondratyev 11 | email=mkondratyev85@gmail.com 12 | 13 | about=RasterTracer is a plugin for semi-automatic digitizing of underlying raster layer in QGis. It is useful, for example, when you need to digitize a scanned topographic map, with curved black lines representing lines of equal heights of the surface. Instead of creating this curved vector line by manually clicking at each segment of this curved line to create multi-line, with this plugin you can click at the beginning of the curved line and at the end of the curved line, and it will automatically trace over black pixels (or pixels that are almost black) from the beginning to the end. By using this plugin you reduce clicks while digitizing raster maps. See https://github.com/mkondratyev85/raster_tracer for more explanation. 14 | 15 | tracker=https://github.com/mkondratyev85/raster_tracer/issues 16 | repository=https://github.com/mkondratyev85/raster_tracer/ 17 | # End of mandatory metadata 18 | 19 | # Recommended items: 20 | 21 | hasProcessingProvider=no 22 | # Uncomment the following line and add your changelog: 23 | changelog=0.3.3 -- Fixed bug in QGis 3.30.x (#39). 24 | 0.3.2 25 | -- Fixed important bug when raster and vector layers have different CS (#26). 26 | 0.3.1 27 | -- Snapping to vector layer while drawing new segments. 28 | -- Using the correct way of closing the docker. 29 | 0.3.0 30 | -- Tracing is on the background. No freezing of QGgis anymore (#22) 31 | 0.2.0 32 | -- Make smoothing optional (#15) 33 | -- Warn the user when geometry type is not MultiLineString (#7, #13) 34 | 0.1.1 35 | -- Update in details and homepage address 36 | 0.1 37 | -- Initial version 38 | 39 | # Tags are comma separated with spaces allowed 40 | tags=python, digitizing, raster, vector 41 | 42 | homepage=https://github.com/mkondratyev85/raster_tracer/ 43 | category=Plugins 44 | icon=icon.png 45 | # experimental flag 46 | experimental=False 47 | 48 | # deprecated flag (applies to the whole plugin, not just a single version) 49 | deprecated=False 50 | 51 | # Since QGIS 3.8, a comma separated list of plugins to be installed 52 | # (or upgraded) can be specified. 53 | # Check the documentation for more information. 54 | # plugin_dependencies= 55 | 56 | Category of the plugin: Raster, Vector, Database or Web 57 | #category=Raster 58 | 59 | # If the plugin can run on QGIS Server. 60 | server=False 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RasterTracer 2 | 3 | RasterTracer is a plugin for semi-automatic digitizing of an underlying raster 4 | layer in QGis. 5 | It is useful, for example, when you need to digitize a scanned 6 | topographic map, with curved black lines representing lines of equal heights of 7 | the surface (contours). 8 | Instead of creating this curved vector line by manually clicking 9 | at each segment of this curved line, with this plugin you 10 | can click at the beginning of the curved line and at the end of the curved 11 | line, and it will automatically trace over black pixels (or pixels that are 12 | almost black) starting from the beginning to the end. 13 | By using this plugin you reduce 14 | clicks while digitizing raster maps. 15 | 16 | The process is show here: 17 | 18 | 19 | 20 | ## Usage 21 | 22 | Tracing is enabled only if the selected vector layer is in the editing mode. 23 | 24 | The geometry type of the vector layer has to be MultiLineString / MultiCurve. 25 | 26 | You can choose the color that will be traced over in the raster image. 27 | To do this, check the box `trace color` and select the desired color in 28 | the dialog window. 29 | 30 | If `trace color` is not checked, the plugin will try to trace the color that is 31 | similar to the color of the pixel on the map at the place where you clicked the 32 | last time. 33 | This means that each time you click on the map, it will trace a slightly 34 | different color. 35 | This slows down tracing a bit, but may be useful if the color of the line you are 36 | tracing varies over the map. 37 | 38 | ## What image can it trace? 39 | 40 | Right now the plugin can trace images that have a standard RGB color space. 41 | It has no support for any black and white, grey, or indexed images. 42 | This means that if your image has an unsupported colorspace, 43 | you have to convert the colorspace of your image to RGB first. This can be done in QGis with: 44 | 45 | `Processing >> Toolbox >> GDAL >> Raster conversion >> PCT to RGB` 46 | 47 | or directly in the CLI with: 48 | 49 | `pct2rgb.py -of GTiff -b 1` 50 | 51 | Also in the current version there are some issues when coordinate system 52 | of the raster layer differs from the coordinate system of the project. 53 | It might be useful to convert the image that will be processed to the same coordinate 54 | system used by the QGis project before importing. For example, the command bellow 55 | converts a geotiff image (already georeferenced) to an `EPSG:4326` coordinate system. 56 | 57 | `gdalwarp -t_srs EPSG:4326 -of GTiff infile.tif outfile.tif` 58 | 59 | __NOTE__: `pct2rgb.py` and `gdalwarp` are part of the GDAL package. 60 | 61 | ## Useful keys 62 | 63 | 64 | `b` - delete last segment 65 | 66 | `a` - switch between "trace" mode and "straight-line" mode. 67 | 68 | `Esc` - cancel tracing segment. Useful when raster_tracer struggles to find 69 | a good path between clicked points (Usually when points are far from each other). 70 | -------------------------------------------------------------------------------- /pb_tool.cfg: -------------------------------------------------------------------------------- 1 | #/*************************************************************************** 2 | # RasterTracer 3 | # 4 | # Configuration file for plugin builder tool (pb_tool) 5 | # Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 6 | # ------------------- 7 | # begin : 2019-11-09 8 | # copyright : (C) 2019 by Mikhail Kondratyev 9 | # email : mkondratyev85@gmail.com 10 | # ***************************************************************************/ 11 | # 12 | #/*************************************************************************** 13 | # * * 14 | # * This program is free software; you can redistribute it and/or modify * 15 | # * it under the terms of the GNU General Public License as published by * 16 | # * the Free Software Foundation; either version 2 of the License, or * 17 | # * (at your option) any later version. * 18 | # * * 19 | # ***************************************************************************/ 20 | # 21 | # 22 | # You can install pb_tool using: 23 | # pip install http://geoapt.net/files/pb_tool.zip 24 | # 25 | # Consider doing your development (and install of pb_tool) in a virtualenv. 26 | # 27 | # For details on setting up and using pb_tool, see: 28 | # http://g-sherman.github.io/plugin_build_tool/ 29 | # 30 | # Issues and pull requests here: 31 | # https://github.com/g-sherman/plugin_build_tool: 32 | # 33 | # Sane defaults for your plugin generated by the Plugin Builder are 34 | # already set below. 35 | # 36 | # As you add Python source files and UI files to your plugin, add 37 | # them to the appropriate [files] section below. 38 | 39 | [plugin] 40 | # Name of the plugin. This is the name of the directory that will 41 | # be created in .qgis2/python/plugins 42 | name: raster_tracer 43 | 44 | # Full path to where you want your plugin directory copied. If empty, 45 | # the QGIS default path will be used. Don't include the plugin name in 46 | # the path. 47 | plugin_path: 48 | 49 | [files] 50 | # Python files that should be deployed with the plugin 51 | python_files: __init__.py raster_tracer.py raster_tracer_dockwidget.py 52 | 53 | # The main dialog file that is loaded (not compiled) 54 | main_dialog: raster_tracer_dockwidget_base.ui 55 | 56 | # Other ui files for dialogs you create (these will be compiled) 57 | compiled_ui_files: 58 | 59 | # Resource file(s) that will be compiled 60 | resource_files: resources.qrc 61 | 62 | # Other files required for the plugin 63 | extras: metadata.txt icon.png 64 | 65 | # Other directories to be deployed with the plugin. 66 | # These must be subdirectories under the plugin directory 67 | extra_dirs: 68 | 69 | # ISO code(s) for any locales (translations), separated by spaces. 70 | # Corresponding .ts files must exist in the i18n directory 71 | locales: 72 | 73 | [help] 74 | # the built help directory that should be deployed with the plugin 75 | dir: help/build/html 76 | # the name of the directory to target in the deployed plugin 77 | target: help 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /raster_tracer_dockwidget_base.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RasterTracerDockWidgetBase 4 | 5 | 6 | 7 | 0 8 | 0 9 | 252 10 | 225 11 | 12 | 13 | 14 | Raster Tracer 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | true 23 | 24 | 25 | Snap to vector layer 26 | 27 | 28 | 29 | 30 | 31 | 32 | false 33 | 34 | 35 | 1 36 | 37 | 38 | 1 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | false 47 | 48 | 49 | Snap to nearest 50 | 51 | 52 | 53 | 54 | 55 | 56 | Trace color 57 | 58 | 59 | 60 | 61 | 62 | 63 | false 64 | 65 | 66 | 67 | 0 68 | 0 69 | 0 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Layer to trace 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | false 88 | 89 | 90 | 1 91 | 92 | 93 | 1 94 | 95 | 96 | 97 | 98 | 99 | 100 | Smooth lines 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | QgsColorButton 110 | QToolButton 111 |
qgscolorbutton.h
112 |
113 | 114 | QgsMapLayerComboBox 115 | QComboBox 116 |
qgsmaplayercombobox.h
117 |
118 | 119 | QgsSpinBox 120 | QSpinBox 121 |
qgsspinbox.h
122 |
123 |
124 | 125 | 126 |
127 | -------------------------------------------------------------------------------- /plugin_upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """This script uploads a plugin package to the plugin repository. 4 | Authors: A. Pasotti, V. Picavet 5 | git sha : $TemplateVCSFormat 6 | """ 7 | 8 | import sys 9 | import getpass 10 | import xmlrpc.client 11 | from optparse import OptionParser 12 | 13 | standard_library.install_aliases() 14 | 15 | # Configuration 16 | PROTOCOL = 'https' 17 | SERVER = 'plugins.qgis.org' 18 | PORT = '443' 19 | ENDPOINT = '/plugins/RPC2/' 20 | VERBOSE = False 21 | 22 | 23 | def main(parameters, arguments): 24 | """Main entry point. 25 | 26 | :param parameters: Command line parameters. 27 | :param arguments: Command line arguments. 28 | """ 29 | address = "{protocol}://{username}:{password}@{server}:{port}{endpoint}".format( 30 | protocol=PROTOCOL, 31 | username=parameters.username, 32 | password=parameters.password, 33 | server=parameters.server, 34 | port=parameters.port, 35 | endpoint=ENDPOINT) 36 | print("Connecting to: %s" % hide_password(address)) 37 | 38 | server = xmlrpc.client.ServerProxy(address, verbose=VERBOSE) 39 | 40 | try: 41 | with open(arguments[0], 'rb') as handle: 42 | plugin_id, version_id = server.plugin.upload( 43 | xmlrpc.client.Binary(handle.read())) 44 | print("Plugin ID: %s" % plugin_id) 45 | print("Version ID: %s" % version_id) 46 | except xmlrpc.client.ProtocolError as err: 47 | print("A protocol error occurred") 48 | print("URL: %s" % hide_password(err.url, 0)) 49 | print("HTTP/HTTPS headers: %s" % err.headers) 50 | print("Error code: %d" % err.errcode) 51 | print("Error message: %s" % err.errmsg) 52 | except xmlrpc.client.Fault as err: 53 | print("A fault occurred") 54 | print("Fault code: %d" % err.faultCode) 55 | print("Fault string: %s" % err.faultString) 56 | 57 | 58 | def hide_password(url, start=6): 59 | """Returns the http url with password part replaced with '*'. 60 | 61 | :param url: URL to upload the plugin to. 62 | :type url: str 63 | 64 | :param start: Position of start of password. 65 | :type start: int 66 | """ 67 | start_position = url.find(':', start) + 1 68 | end_position = url.find('@') 69 | return "%s%s%s" % ( 70 | url[:start_position], 71 | '*' * (end_position - start_position), 72 | url[end_position:]) 73 | 74 | 75 | if __name__ == "__main__": 76 | parser = OptionParser(usage="%prog [options] plugin.zip") 77 | parser.add_option( 78 | "-w", "--password", dest="password", 79 | help="Password for plugin site", metavar="******") 80 | parser.add_option( 81 | "-u", "--username", dest="username", 82 | help="Username of plugin site", metavar="user") 83 | parser.add_option( 84 | "-p", "--port", dest="port", 85 | help="Server port to connect to", metavar="80") 86 | parser.add_option( 87 | "-s", "--server", dest="server", 88 | help="Specify server name", metavar="plugins.qgis.org") 89 | options, args = parser.parse_args() 90 | if len(args) != 1: 91 | print("Please specify zip file.\n") 92 | parser.print_help() 93 | sys.exit(1) 94 | if not options.server: 95 | options.server = SERVER 96 | if not options.port: 97 | options.port = PORT 98 | if not options.username: 99 | # interactive mode 100 | username = getpass.getuser() 101 | print("Please enter user name [%s] :" % username, end=' ') 102 | 103 | res = input() 104 | if res != "": 105 | options.username = res 106 | else: 107 | options.username = username 108 | if not options.password: 109 | # interactive mode 110 | options.password = getpass.getpass() 111 | main(options, args) 112 | -------------------------------------------------------------------------------- /autotrace.py: -------------------------------------------------------------------------------- 1 | from math import atan2, cos, sin, radians 2 | 3 | from qgis.core import QgsTask, QgsMessageLog 4 | 5 | 6 | class AutotraceSubTask(QgsTask): 7 | 8 | def __init__(self, pointtool, vlayer, clicked_point=None): 9 | super().__init__( 10 | 'Task for switching mode to autotrace', 11 | QgsTask.CanCancel 12 | ) 13 | self.pointtool = pointtool 14 | self.vlayer = vlayer 15 | self.pseudo_anchors = [] 16 | self.path = [] 17 | self.clicked_point = clicked_point 18 | 19 | def run(self): 20 | 21 | if self.clicked_point: 22 | self.pseudo_anchors.append(self.pointtool.anchors[-1]) 23 | self.pseudo_anchors.append(self.clicked_point) 24 | self.clicked_point = None 25 | else: 26 | self.pseudo_anchors.append(self.pointtool.anchors[-2]) 27 | self.pseudo_anchors.append(self.pointtool.anchors[-1]) 28 | 29 | result_path = self.follow_next_segment(initial=True) 30 | self.path += result_path 31 | 32 | 33 | for _ in range(5): 34 | # check isCanceled() to handle cancellation 35 | if self.isCanceled(): 36 | return False 37 | 38 | result_path = self.follow_next_segment() 39 | self.path += result_path[1:] 40 | 41 | return True 42 | 43 | 44 | def follow_next_segment(self, initial=False): 45 | _, _, i0, j0 = self.pseudo_anchors[-2] 46 | _, _, i1, j1 = self.pseudo_anchors[-1] 47 | 48 | direction = atan2(j1 - j0, i1 - i0) 49 | distance = 5 50 | 51 | if initial: 52 | # self.pointtool.remove_last_anchor_point(undo_edit=False, redraw=False) 53 | self.pseudo_anchors.pop() 54 | i1, j1 = i0, j0 55 | 56 | points = self.search_near_points((i1, j1), direction, distance) 57 | 58 | costs = [] 59 | paths = [] 60 | 61 | for point in points: 62 | i2, j2 = point 63 | # x2, y2 = self.pointtool.to_coords(i2, j2) 64 | 65 | path, cost = self.pointtool.trace_over_image((i1, j1), (i2, j2)) 66 | costs.append(cost) 67 | paths.append(path) 68 | 69 | min_cost = min(costs) 70 | min_cost_index = costs.index(min_cost) 71 | 72 | best_point = points[min_cost_index] 73 | best_path = paths[min_cost_index] 74 | i, j = best_point 75 | x, y = self.pointtool.to_coords(i, j) 76 | self.pseudo_anchors.append((x, y, i, j)) 77 | 78 | return best_path 79 | 80 | 81 | def search_near_points(self, point, direction, distance): 82 | ''' 83 | Returns list of points near last point in the given direction, 84 | at a given distance with given space between points. 85 | ''' 86 | 87 | points = [] 88 | 89 | i1, j1 = point 90 | 91 | angles = [direction + radians(i) for i in range(-60, 60, 10)] 92 | 93 | for angle in angles: 94 | i2 = i1 + distance * cos(angle) 95 | j2 = j1 + distance * sin(angle) 96 | 97 | points.append((int(i2), int(j2))) 98 | 99 | return points 100 | 101 | def finished(self, result): 102 | ''' 103 | Call callback function if self.run was successful 104 | ''' 105 | 106 | if result: 107 | vlayer = self.vlayer 108 | self.pointtool.draw_path(self.path, vlayer, was_tracing=True) 109 | x, y, i, j = self.pseudo_anchors[-1] 110 | self.pointtool.add_anchor_points(x, y, i, j) 111 | self.pointtool.pan(x, y) 112 | self.pointtool.redraw() 113 | self.pointtool.update_rubber_band() 114 | 115 | 116 | def cancel(self): 117 | ''' 118 | Executed when run catches cancel signal. 119 | Terminates the QgsTask. 120 | ''' 121 | 122 | super().cancel() 123 | -------------------------------------------------------------------------------- /help/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\template_class.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\template_class.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /help/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/template_class.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/template_class.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/template_class" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/template_class" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /astar.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module performs searching of the best path on 2D grid between 3 | two given points by using famous A* method. 4 | Code is based on example from 5 | https://www.redblobgames.com/pathfinding/a-star/implementation.html 6 | ''' 7 | 8 | import heapq 9 | 10 | from qgis.core import QgsTask, QgsMessageLog 11 | 12 | class PriorityQueue: 13 | def __init__(self): 14 | self.elements = [] 15 | 16 | def empty(self): 17 | return len(self.elements) == 0 18 | 19 | def put(self, item, priority): 20 | heapq.heappush(self.elements, (priority, item)) 21 | 22 | def get(self): 23 | return heapq.heappop(self.elements)[1] 24 | 25 | def heuristic(a, b): 26 | (x1, y1) = a 27 | (x2, y2) = b 28 | return abs(x1 - x2) + abs(y1 - y2) 29 | 30 | def get_neighbors(size_i, size_j, ij): 31 | """ returns possible neighbors of a numpy cell """ 32 | i,j = ij 33 | neighbors = set() 34 | if i>0: 35 | neighbors.add((i-1, j)) 36 | if j>0: 37 | neighbors.add((i, j-1)) 38 | if i list of map layers that were added 66 | 67 | .. note:: The QgsInterface api does not include this method, 68 | it is added here as a helper to facilitate testing. 69 | """ 70 | #LOGGER.debug('addLayers called on qgis_interface') 71 | #LOGGER.debug('Number of layers being added: %s' % len(layers)) 72 | #LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers())) 73 | current_layers = self.canvas.layers() 74 | final_layers = [] 75 | for layer in current_layers: 76 | final_layers.append(QgsMapCanvasLayer(layer)) 77 | for layer in layers: 78 | final_layers.append(QgsMapCanvasLayer(layer)) 79 | 80 | self.canvas.setLayerSet(final_layers) 81 | #LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers())) 82 | 83 | @pyqtSlot('QgsMapLayer') 84 | def addLayer(self, layer): 85 | """Handle a layer being added to the registry so it shows up in canvas. 86 | 87 | :param layer: list list of map layers that were added 88 | 89 | .. note: The QgsInterface api does not include this method, it is added 90 | here as a helper to facilitate testing. 91 | 92 | .. note: The addLayer method was deprecated in QGIS 1.8 so you should 93 | not need this method much. 94 | """ 95 | pass 96 | 97 | @pyqtSlot() 98 | def removeAllLayers(self): 99 | """Remove layers from the canvas before they get deleted.""" 100 | self.canvas.setLayerSet([]) 101 | 102 | def newProject(self): 103 | """Create new project.""" 104 | # noinspection PyArgumentList 105 | QgsMapLayerRegistry.instance().removeAllMapLayers() 106 | 107 | # ---------------- API Mock for QgsInterface follows ------------------- 108 | 109 | def zoomFull(self): 110 | """Zoom to the map full extent.""" 111 | pass 112 | 113 | def zoomToPrevious(self): 114 | """Zoom to previous view extent.""" 115 | pass 116 | 117 | def zoomToNext(self): 118 | """Zoom to next view extent.""" 119 | pass 120 | 121 | def zoomToActiveLayer(self): 122 | """Zoom to extent of active layer.""" 123 | pass 124 | 125 | def addVectorLayer(self, path, base_name, provider_key): 126 | """Add a vector layer. 127 | 128 | :param path: Path to layer. 129 | :type path: str 130 | 131 | :param base_name: Base name for layer. 132 | :type base_name: str 133 | 134 | :param provider_key: Provider key e.g. 'ogr' 135 | :type provider_key: str 136 | """ 137 | pass 138 | 139 | def addRasterLayer(self, path, base_name): 140 | """Add a raster layer given a raster layer file name 141 | 142 | :param path: Path to layer. 143 | :type path: str 144 | 145 | :param base_name: Base name for layer. 146 | :type base_name: str 147 | """ 148 | pass 149 | 150 | def activeLayer(self): 151 | """Get pointer to the active layer (layer selected in the legend).""" 152 | # noinspection PyArgumentList 153 | layers = QgsMapLayerRegistry.instance().mapLayers() 154 | for item in layers: 155 | return layers[item] 156 | 157 | def addToolBarIcon(self, action): 158 | """Add an icon to the plugins toolbar. 159 | 160 | :param action: Action to add to the toolbar. 161 | :type action: QAction 162 | """ 163 | pass 164 | 165 | def removeToolBarIcon(self, action): 166 | """Remove an action (icon) from the plugin toolbar. 167 | 168 | :param action: Action to add to the toolbar. 169 | :type action: QAction 170 | """ 171 | pass 172 | 173 | def addToolBar(self, name): 174 | """Add toolbar with specified name. 175 | 176 | :param name: Name for the toolbar. 177 | :type name: str 178 | """ 179 | pass 180 | 181 | def mapCanvas(self): 182 | """Return a pointer to the map canvas.""" 183 | return self.canvas 184 | 185 | def mainWindow(self): 186 | """Return a pointer to the main window. 187 | 188 | In case of QGIS it returns an instance of QgisApp. 189 | """ 190 | pass 191 | 192 | def addDockWidget(self, area, dock_widget): 193 | """Add a dock widget to the main window. 194 | 195 | :param area: Where in the ui the dock should be placed. 196 | :type area: 197 | 198 | :param dock_widget: A dock widget to add to the UI. 199 | :type dock_widget: QDockWidget 200 | """ 201 | pass 202 | 203 | def legendInterface(self): 204 | """Get the legend.""" 205 | return self.canvas 206 | -------------------------------------------------------------------------------- /pointtool_states.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module contains States for pointtool. 3 | ''' 4 | 5 | from math import atan2, cos, sin, radians 6 | 7 | from qgis.core import QgsApplication 8 | 9 | from .autotrace import AutotraceSubTask 10 | 11 | 12 | class State: 13 | ''' 14 | Abstract class for the state 15 | ''' 16 | 17 | def __init__(self, pointtool): 18 | self.pointtool = pointtool 19 | 20 | def click_rmb(self, mouseEvent, vlayer): 21 | ''' 22 | Event when the user clicks on the map with the right button 23 | ''' 24 | 25 | # finish point path if it was last point 26 | self.pointtool.anchors = [] 27 | 28 | # hide all markers 29 | while self.pointtool.markers: 30 | marker = self.pointtool.markers.pop() 31 | self.pointtool.canvas().scene().removeItem(marker) 32 | 33 | # hide rubber_band 34 | self.pointtool.rubber_band.hide() 35 | 36 | # change state 37 | self.pointtool.change_state(WaitingFirstPointState) 38 | 39 | def click_lmb(self, mouseEvent, vlayer): 40 | ''' 41 | Event when the user clicks on the map with the left button 42 | ''' 43 | 44 | # self.pointtool.last_mouse_event_pos = mouseEvent.pos() 45 | # hide rubber_band 46 | self.pointtool.rubber_band.hide() 47 | 48 | # check if he haven't any new tasks yet 49 | if self.pointtool.tracking_is_active: 50 | self.pointtool.display_message( 51 | " ", 52 | "Please wait till the last segment is finished" + 53 | " or terminate tracing by hitting Esc", 54 | level='Critical', 55 | duration=1, 56 | ) 57 | return False 58 | 59 | # acquire point coordinates from mouseEvent 60 | qgsPoint = self.pointtool.toMapCoordinates(mouseEvent.pos()) 61 | x1, y1 = qgsPoint.x(), qgsPoint.y() 62 | 63 | if self.pointtool.to_indexes is None: 64 | self.pointtool.display_message( 65 | "Missing Layer", 66 | "Please select correct raster layer", 67 | level='Critical', 68 | duration=2, 69 | ) 70 | return False 71 | 72 | if self.pointtool.snap2_tolerance: 73 | x1, y1 = self.pointtool.snap_to_itself(x1, y1, self.pointtool.snap2_tolerance) 74 | i1, j1 = self.pointtool.to_indexes(x1, y1) 75 | self.pointtool.add_anchor_points(x1, y1, i1, j1) 76 | 77 | return True 78 | 79 | 80 | class WaitingFirstPointState(State): 81 | ''' 82 | State of waiting the user to click on the first point in the line. 83 | Is active when the user is about to begin tracing new line. 84 | After the user clicks on the left mouse button 85 | it changes the state to WaitingMiddlePointState. 86 | ''' 87 | 88 | def click_lmb(self, mouseEvent, vlayer): 89 | 90 | if super().click_lmb(mouseEvent, vlayer) is False: 91 | return 92 | 93 | # change state 94 | self.pointtool.change_state(WaitingMiddlePointState) 95 | # self.pointtool.change_state(AutoFollowingLineState) 96 | 97 | def click_rmb(self, mouseEvent, vlayer): 98 | pass 99 | 100 | 101 | class WaitingMiddlePointState(State): 102 | ''' 103 | State of waiting the user to click on the next point in the line. 104 | Is active when the user is already clicked on at least one point. 105 | After the user clicks on the left mouse button it keeps the state. 106 | After the user clicks on the right mouse button it finishes the line and 107 | switches the state to WaitingFirstPointState. 108 | 109 | ''' 110 | 111 | def click_lmb(self, mouseEvent, vlayer): 112 | if super().click_lmb(mouseEvent, vlayer) is False: 113 | return 114 | 115 | x1, y1, i1, j1 = self.pointtool.anchors[-1] 116 | 117 | if self.pointtool.tracing_mode.is_auto(): 118 | 119 | # perform autotrace 120 | self.autotrace_task = AutotraceSubTask( 121 | self.pointtool, 122 | vlayer, 123 | clicked_point=self.pointtool.anchors[-1], 124 | ) 125 | # self.pointtool.remove_last_anchor_point(undo_edit=False, redraw=False) 126 | 127 | QgsApplication.taskManager().addTask( 128 | self.autotrace_task, 129 | ) 130 | 131 | else: 132 | self.pointtool.trace(x1, y1, i1, j1, vlayer) 133 | 134 | def click_rmb(self, mouseEvent, vlayer): 135 | 136 | super().click_rmb(mouseEvent, vlayer) 137 | 138 | # # add last feature to spatial index to perform fast search of closet points 139 | # self.pointtool.add_last_feature_to_spindex(vlayer) 140 | 141 | 142 | class AutoFollowingLineState(State): 143 | ''' 144 | This state is active when raster_tracer is trying to 145 | perform auto-following of the line. 146 | ''' 147 | 148 | def click_lmb(self, mouseEvent, vlayer): 149 | if super().click_lmb(mouseEvent, vlayer) is False: 150 | return 151 | 152 | self.follow_next_segment(vlayer, initial=True) 153 | 154 | for _ in range(25): 155 | self.follow_next_segment(vlayer) 156 | # while True: 157 | # if self.pointtool.ready is True: 158 | # break 159 | self.pointtool.redraw() 160 | self.pointtool.update_rubber_band() 161 | # print('a') 162 | 163 | 164 | def click_rmb(self, mouseEvent, vlayer): 165 | super().click_rmb(mouseEvent, vlayer) 166 | 167 | def follow_next_segment(self, vlayer, initial=False): 168 | _, _, i0, j0 = self.pointtool.anchors[-2] 169 | _, _, i1, j1 = self.pointtool.anchors[-1] 170 | 171 | direction = atan2(j1 - j0, i1 - i0) 172 | distance = 5 173 | 174 | if initial: 175 | self.pointtool.remove_last_anchor_point(undo_edit=False) 176 | i1, j1 = i0, j0 177 | 178 | points = self.search_near_points((i1, j1), direction, distance) 179 | 180 | costs = [] 181 | paths = [] 182 | 183 | for point in points: 184 | i2, j2 = point 185 | x2, y2 = self.pointtool.to_coords(i2, j2) 186 | 187 | path, cost = self.pointtool.trace_over_image((i1, j1), (i2, j2)) 188 | costs.append(cost) 189 | paths.append(path) 190 | 191 | min_cost = min(costs) 192 | min_cost_index = costs.index(min_cost) 193 | 194 | best_point = points[min_cost_index] 195 | best_path = paths[min_cost_index] 196 | i, j = best_point 197 | x, y = self.pointtool.to_coords(i, j) 198 | 199 | if len(self.pointtool.anchors)>1: 200 | self.pointtool.draw_path(best_path, vlayer, was_tracing=True) 201 | self.pointtool.add_anchor_points(x, y, i, j) 202 | else: 203 | self.pointtool.add_anchor_points(x, y, i, j) 204 | self.pointtool.draw_path(best_path, vlayer, was_tracing=True) 205 | 206 | self.pointtool.pan(x, y) 207 | 208 | def search_near_points(self, point, direction, distance): 209 | ''' 210 | Returns list of points near last point in the given direction, 211 | at a given distance with given space between points. 212 | ''' 213 | 214 | points = [] 215 | 216 | i1, j1 = point 217 | 218 | angles = [direction + radians(i) for i in range(-60, 60, 10)] 219 | 220 | for angle in angles: 221 | i2 = i1 + distance * cos(angle) 222 | j2 = j1 + distance * sin(angle) 223 | 224 | points.append((int(i2), int(j2))) 225 | 226 | return points 227 | -------------------------------------------------------------------------------- /help/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # RasterTracer documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Feb 12 17:11:03 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.todo', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'RasterTracer' 44 | copyright = u'2013, Mikhail Kondratyev' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_TemplateModuleNames = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'TemplateClassdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'RasterTracer.tex', u'RasterTracer Documentation', 182 | u'Mikhail Kondratyev', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'TemplateClass', u'RasterTracer Documentation', 215 | [u'Mikhail Kondratyev'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #/*************************************************************************** 2 | # RasterTracer 3 | # 4 | # This plugin traces the underlying raster map 5 | # ------------------- 6 | # begin : 2019-11-09 7 | # git sha : $Format:%H$ 8 | # copyright : (C) 2019 by Mikhail Kondratyev 9 | # email : mkondratyev85@gmail.com 10 | # ***************************************************************************/ 11 | # 12 | #/*************************************************************************** 13 | # * * 14 | # * This program is free software; you can redistribute it and/or modify * 15 | # * it under the terms of the GNU General Public License as published by * 16 | # * the Free Software Foundation; either version 2 of the License, or * 17 | # * (at your option) any later version. * 18 | # * * 19 | # ***************************************************************************/ 20 | 21 | ################################################# 22 | # Edit the following to match your sources lists 23 | ################################################# 24 | 25 | 26 | #Add iso code for any locales you want to support here (space separated) 27 | # default is no locales 28 | # LOCALES = af 29 | LOCALES = 30 | 31 | # If locales are enabled, set the name of the lrelease binary on your system. If 32 | # you have trouble compiling the translations, you may have to specify the full path to 33 | # lrelease 34 | #LRELEASE = lrelease 35 | #LRELEASE = lrelease-qt4 36 | 37 | 38 | # translation 39 | SOURCES = \ 40 | __init__.py \ 41 | raster_tracer.py raster_tracer_dockwidget.py 42 | 43 | PLUGINNAME = raster_tracer 44 | 45 | PY_FILES = \ 46 | __init__.py \ 47 | raster_tracer.py raster_tracer_dockwidget.py 48 | 49 | UI_FILES = raster_tracer_dockwidget_base.ui 50 | 51 | EXTRAS = metadata.txt icon.png 52 | 53 | EXTRA_DIRS = 54 | 55 | COMPILED_RESOURCE_FILES = resources.py 56 | 57 | PEP8EXCLUDE=pydev,resources.py,conf.py,third_party,ui 58 | 59 | # QGISDIR points to the location where your plugin should be installed. 60 | # This varies by platform, relative to your HOME directory: 61 | # * Linux: 62 | # .local/share/QGIS/QGIS3/profiles/default/python/plugins/ 63 | # * Mac OS X: 64 | # Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins 65 | # * Windows: 66 | # AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins' 67 | 68 | QGISDIR=/home/fatune/.local/share/QGIS/QGIS3/profiles/default/python/plugins/ 69 | 70 | ################################################# 71 | # Normally you would not need to edit below here 72 | ################################################# 73 | 74 | HELP = help/build/html 75 | 76 | PLUGIN_UPLOAD = $(c)/plugin_upload.py 77 | 78 | RESOURCE_SRC=$(shell grep '^ *@@g;s/.*>//g' | tr '\n' ' ') 79 | 80 | .PHONY: default 81 | default: 82 | @echo While you can use make to build and deploy your plugin, pb_tool 83 | @echo is a much better solution. 84 | @echo A Python script, pb_tool provides platform independent management of 85 | @echo your plugins and runs anywhere. 86 | @echo You can install pb_tool using: pip install pb_tool 87 | @echo See https://g-sherman.github.io/plugin_build_tool/ for info. 88 | 89 | compile: $(COMPILED_RESOURCE_FILES) 90 | 91 | %.py : %.qrc $(RESOURCES_SRC) 92 | pyrcc5 -o $*.py $< 93 | 94 | %.qm : %.ts 95 | $(LRELEASE) $< 96 | 97 | test: compile transcompile 98 | @echo 99 | @echo "----------------------" 100 | @echo "Regression Test Suite" 101 | @echo "----------------------" 102 | 103 | @# Preceding dash means that make will continue in case of errors 104 | @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ 105 | export QGIS_DEBUG=0; \ 106 | export QGIS_LOG_FILE=/dev/null; \ 107 | nosetests -v --with-id --with-coverage --cover-package=. \ 108 | 3>&1 1>&2 2>&3 3>&- || true 109 | @echo "----------------------" 110 | @echo "If you get a 'no module named qgis.core error, try sourcing" 111 | @echo "the helper script we have provided first then run make test." 112 | @echo "e.g. source run-env-linux.sh ; make test" 113 | @echo "----------------------" 114 | 115 | deploy: compile doc transcompile 116 | @echo 117 | @echo "------------------------------------------" 118 | @echo "Deploying plugin to your .qgis2 directory." 119 | @echo "------------------------------------------" 120 | # The deploy target only works on unix like operating system where 121 | # the Python plugin directory is located at: 122 | # $HOME/$(QGISDIR)/python/plugins 123 | mkdir -p $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 124 | cp -vf $(PY_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 125 | cp -vf $(UI_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 126 | cp -vf $(COMPILED_RESOURCE_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 127 | cp -vf $(EXTRAS) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 128 | cp -vfr i18n $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 129 | cp -vfr $(HELP) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)/help 130 | # Copy extra directories if any 131 | (foreach EXTRA_DIR,(EXTRA_DIRS), cp -R (EXTRA_DIR) (HOME)/(QGISDIR)/python/plugins/(PLUGINNAME)/;) 132 | 133 | 134 | # The dclean target removes compiled python files from plugin directory 135 | # also deletes any .git entry 136 | dclean: 137 | @echo 138 | @echo "-----------------------------------" 139 | @echo "Removing any compiled python files." 140 | @echo "-----------------------------------" 141 | find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname "*.pyc" -delete 142 | find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname ".git" -prune -exec rm -Rf {} \; 143 | 144 | 145 | derase: 146 | @echo 147 | @echo "-------------------------" 148 | @echo "Removing deployed plugin." 149 | @echo "-------------------------" 150 | rm -Rf $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 151 | 152 | zip: deploy dclean 153 | @echo 154 | @echo "---------------------------" 155 | @echo "Creating plugin zip bundle." 156 | @echo "---------------------------" 157 | # The zip target deploys the plugin and creates a zip file with the deployed 158 | # content. You can then upload the zip file on http://plugins.qgis.org 159 | rm -f $(PLUGINNAME).zip 160 | cd $(HOME)/$(QGISDIR)/python/plugins; zip -9r $(CURDIR)/$(PLUGINNAME).zip $(PLUGINNAME) 161 | 162 | package: compile 163 | # Create a zip package of the plugin named $(PLUGINNAME).zip. 164 | # This requires use of git (your plugin development directory must be a 165 | # git repository). 166 | # To use, pass a valid commit or tag as follows: 167 | # make package VERSION=Version_0.3.2 168 | @echo 169 | @echo "------------------------------------" 170 | @echo "Exporting plugin to zip package. " 171 | @echo "------------------------------------" 172 | rm -f $(PLUGINNAME).zip 173 | git archive --prefix=$(PLUGINNAME)/ -o $(PLUGINNAME).zip $(VERSION) 174 | echo "Created package: $(PLUGINNAME).zip" 175 | 176 | upload: zip 177 | @echo 178 | @echo "-------------------------------------" 179 | @echo "Uploading plugin to QGIS Plugin repo." 180 | @echo "-------------------------------------" 181 | $(PLUGIN_UPLOAD) $(PLUGINNAME).zip 182 | 183 | transup: 184 | @echo 185 | @echo "------------------------------------------------" 186 | @echo "Updating translation files with any new strings." 187 | @echo "------------------------------------------------" 188 | @chmod +x scripts/update-strings.sh 189 | @scripts/update-strings.sh $(LOCALES) 190 | 191 | transcompile: 192 | @echo 193 | @echo "----------------------------------------" 194 | @echo "Compiled translation files to .qm files." 195 | @echo "----------------------------------------" 196 | @chmod +x scripts/compile-strings.sh 197 | @scripts/compile-strings.sh $(LRELEASE) $(LOCALES) 198 | 199 | transclean: 200 | @echo 201 | @echo "------------------------------------" 202 | @echo "Removing compiled translation files." 203 | @echo "------------------------------------" 204 | rm -f i18n/*.qm 205 | 206 | clean: 207 | @echo 208 | @echo "------------------------------------" 209 | @echo "Removing uic and rcc generated files" 210 | @echo "------------------------------------" 211 | rm $(COMPILED_UI_FILES) $(COMPILED_RESOURCE_FILES) 212 | 213 | doc: 214 | @echo 215 | @echo "------------------------------------" 216 | @echo "Building documentation using sphinx." 217 | @echo "------------------------------------" 218 | cd help; make html 219 | 220 | pylint: 221 | @echo 222 | @echo "-----------------" 223 | @echo "Pylint violations" 224 | @echo "-----------------" 225 | @pylint --reports=n --rcfile=pylintrc . || true 226 | @echo 227 | @echo "----------------------" 228 | @echo "If you get a 'no module named qgis.core' error, try sourcing" 229 | @echo "the helper script we have provided first then run make pylint." 230 | @echo "e.g. source run-env-linux.sh ; make pylint" 231 | @echo "----------------------" 232 | 233 | 234 | # Run pep8 style checking 235 | #http://pypi.python.org/pypi/pep8 236 | pep8: 237 | @echo 238 | @echo "-----------" 239 | @echo "PEP8 issues" 240 | @echo "-----------" 241 | @pep8 --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128 --exclude $(PEP8EXCLUDE) . || true 242 | @echo "-----------" 243 | @echo "Ignored in PEP8 check:" 244 | @echo $(PEP8EXCLUDE) 245 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. See also the "--disable" option for examples. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifiers separated by comma (,) or put this 34 | # option multiple times (only on the command line, not in the configuration 35 | # file where it should appear only once).You can also use "--disable=all" to 36 | # disable everything first and then reenable specific checks. For example, if 37 | # you want to run only the similarities checker, you can use "--disable=all 38 | # --enable=similarities". If you want to run only the classes checker, but have 39 | # no Warning level messages displayed, use"--disable=all --enable=classes 40 | # --disable=W" 41 | # see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them 42 | disable=locally-disabled,C0103 43 | 44 | 45 | [REPORTS] 46 | 47 | # Set the output format. Available formats are text, parseable, colorized, msvs 48 | # (visual studio) and html. You can also give a reporter class, eg 49 | # mypackage.mymodule.MyReporterClass. 50 | output-format=text 51 | 52 | # Put messages in a separate file for each module / package specified on the 53 | # command line instead of printing them on stdout. Reports (if any) will be 54 | # written in a file name "pylint_global.[txt|html]". 55 | files-output=no 56 | 57 | # Tells whether to display a full report or only the messages 58 | reports=yes 59 | 60 | # Python expression which should return a note less than 10 (10 is the highest 61 | # note). You have access to the variables errors warning, statement which 62 | # respectively contain the number of errors / warnings messages and the total 63 | # number of statements analyzed. This is used by the global evaluation report 64 | # (RP0004). 65 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 66 | 67 | # Add a comment according to your evaluation note. This is used by the global 68 | # evaluation report (RP0004). 69 | comment=no 70 | 71 | # Template used to display messages. This is a python new-style format string 72 | # used to format the message information. See doc for all details 73 | #msg-template= 74 | 75 | 76 | [BASIC] 77 | 78 | # Required attributes for module, separated by a comma 79 | required-attributes= 80 | 81 | # List of builtins function names that should not be used, separated by a comma 82 | bad-functions=map,filter,apply,input 83 | 84 | # Regular expression which should only match correct module names 85 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 86 | 87 | # Regular expression which should only match correct module level names 88 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 89 | 90 | # Regular expression which should only match correct class names 91 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 92 | 93 | # Regular expression which should only match correct function names 94 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 95 | 96 | # Regular expression which should only match correct method names 97 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 98 | 99 | # Regular expression which should only match correct instance attribute names 100 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 101 | 102 | # Regular expression which should only match correct argument names 103 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 104 | 105 | # Regular expression which should only match correct variable names 106 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 107 | 108 | # Regular expression which should only match correct attribute names in class 109 | # bodies 110 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 111 | 112 | # Regular expression which should only match correct list comprehension / 113 | # generator expression variable names 114 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 115 | 116 | # Good variable names which should always be accepted, separated by a comma 117 | good-names=i,j,k,ex,Run,_ 118 | 119 | # Bad variable names which should always be refused, separated by a comma 120 | bad-names=foo,bar,baz,toto,tutu,tata 121 | 122 | # Regular expression which should only match function or class names that do 123 | # not require a docstring. 124 | no-docstring-rgx=__.*__ 125 | 126 | # Minimum line length for functions/classes that require docstrings, shorter 127 | # ones are exempt. 128 | docstring-min-length=-1 129 | 130 | 131 | [MISCELLANEOUS] 132 | 133 | # List of note tags to take in consideration, separated by a comma. 134 | notes=FIXME,XXX,TODO 135 | 136 | 137 | [TYPECHECK] 138 | 139 | # Tells whether missing members accessed in mixin class should be ignored. A 140 | # mixin class is detected if its name ends with "mixin" (case insensitive). 141 | ignore-mixin-members=yes 142 | 143 | # List of classes names for which member attributes should not be checked 144 | # (useful for classes with attributes dynamically set). 145 | ignored-classes=SQLObject 146 | 147 | # When zope mode is activated, add a predefined set of Zope acquired attributes 148 | # to generated-members. 149 | zope=no 150 | 151 | # List of members which are set dynamically and missed by pylint inference 152 | # system, and so shouldn't trigger E0201 when accessed. Python regular 153 | # expressions are accepted. 154 | generated-members=REQUEST,acl_users,aq_parent 155 | 156 | 157 | [VARIABLES] 158 | 159 | # Tells whether we should check for unused import in __init__ files. 160 | init-import=no 161 | 162 | # A regular expression matching the beginning of the name of dummy variables 163 | # (i.e. not used). 164 | dummy-variables-rgx=_$|dummy 165 | 166 | # List of additional names supposed to be defined in builtins. Remember that 167 | # you should avoid to define new builtins when possible. 168 | additional-builtins= 169 | 170 | 171 | [FORMAT] 172 | 173 | # Maximum number of characters on a single line. 174 | max-line-length=80 175 | 176 | # Regexp for a line that is allowed to be longer than the limit. 177 | ignore-long-lines=^\s*(# )??$ 178 | 179 | # Allow the body of an if to be on the same line as the test if there is no 180 | # else. 181 | single-line-if-stmt=no 182 | 183 | # List of optional constructs for which whitespace checking is disabled 184 | no-space-check=trailing-comma,dict-separator 185 | 186 | # Maximum number of lines in a module 187 | max-module-lines=1000 188 | 189 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 190 | # tab). 191 | indent-string=' ' 192 | 193 | 194 | [SIMILARITIES] 195 | 196 | # Minimum lines number of a similarity. 197 | min-similarity-lines=4 198 | 199 | # Ignore comments when computing similarities. 200 | ignore-comments=yes 201 | 202 | # Ignore docstrings when computing similarities. 203 | ignore-docstrings=yes 204 | 205 | # Ignore imports when computing similarities. 206 | ignore-imports=no 207 | 208 | 209 | [IMPORTS] 210 | 211 | # Deprecated modules which should not be used, separated by a comma 212 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 213 | 214 | # Create a graph of every (i.e. internal and external) dependencies in the 215 | # given file (report RP0402 must not be disabled) 216 | import-graph= 217 | 218 | # Create a graph of external dependencies in the given file (report RP0402 must 219 | # not be disabled) 220 | ext-import-graph= 221 | 222 | # Create a graph of internal dependencies in the given file (report RP0402 must 223 | # not be disabled) 224 | int-import-graph= 225 | 226 | 227 | [DESIGN] 228 | 229 | # Maximum number of arguments for function / method 230 | max-args=5 231 | 232 | # Argument names that match this expression will be ignored. Default to name 233 | # with leading underscore 234 | ignored-argument-names=_.* 235 | 236 | # Maximum number of locals for function / method body 237 | max-locals=15 238 | 239 | # Maximum number of return / yield for function / method body 240 | max-returns=6 241 | 242 | # Maximum number of branch for function / method body 243 | max-branches=12 244 | 245 | # Maximum number of statements in function / method body 246 | max-statements=50 247 | 248 | # Maximum number of parents for a class (see R0901). 249 | max-parents=7 250 | 251 | # Maximum number of attributes for a class (see R0902). 252 | max-attributes=7 253 | 254 | # Minimum number of public methods for a class (see R0903). 255 | min-public-methods=2 256 | 257 | # Maximum number of public methods for a class (see R0904). 258 | max-public-methods=20 259 | 260 | 261 | [CLASSES] 262 | 263 | # List of interface methods to ignore, separated by a comma. This is used for 264 | # instance to not check methods defines in Zope's Interface base class. 265 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 266 | 267 | # List of method names used to declare (i.e. assign) instance attributes. 268 | defining-attr-methods=__init__,__new__,setUp 269 | 270 | # List of valid names for the first argument in a class method. 271 | valid-classmethod-first-arg=cls 272 | 273 | # List of valid names for the first argument in a metaclass class method. 274 | valid-metaclass-classmethod-first-arg=mcs 275 | 276 | 277 | [EXCEPTIONS] 278 | 279 | # Exceptions that will emit a warning when being caught. Defaults to 280 | # "Exception" 281 | overgeneral-exceptions=Exception 282 | -------------------------------------------------------------------------------- /raster_tracer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | RasterTracer 5 | A QGIS plugin 6 | This plugin traces the underlying raster map 7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 8 | ------------------- 9 | begin : 2019-11-09 10 | git sha : $Format:%H$ 11 | copyright : (C) 2019 by Mikhail Kondratyev 12 | email : mkondratyev85@gmail.com 13 | ***************************************************************************/ 14 | 15 | /*************************************************************************** 16 | * * 17 | * This program is free software; you can redistribute it and/or modify * 18 | * it under the terms of the GNU General Public License as published by * 19 | * the Free Software Foundation; either version 2 of the License, or * 20 | * (at your option) any later version. * 21 | * * 22 | ***************************************************************************/ 23 | """ 24 | from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt 25 | from qgis.PyQt.QtGui import QIcon 26 | from qgis.PyQt.QtWidgets import QAction, QApplication 27 | # Initialize Qt resources from file resources.py 28 | from .resources import * 29 | 30 | 31 | # Import the code for the DockWidget 32 | from .raster_tracer_dockwidget import RasterTracerDockWidget 33 | import os.path 34 | 35 | 36 | from qgis.core import QgsProject, QgsVectorLayer 37 | 38 | from .pointtool import PointTool 39 | 40 | 41 | class RasterTracer: 42 | """QGIS Plugin Implementation.""" 43 | 44 | def __init__(self, iface): 45 | """Constructor. 46 | 47 | :param iface: An interface instance that will be passed to this class 48 | which provides the hook by which you can manipulate the QGIS 49 | application at run time. 50 | :type iface: QgsInterface 51 | """ 52 | # Save reference to the QGIS interface 53 | self.iface = iface 54 | 55 | # initialize plugin directory 56 | self.plugin_dir = os.path.dirname(__file__) 57 | 58 | # initialize locale 59 | locale = QSettings().value('locale/userLocale')[0:2] 60 | locale_path = os.path.join( 61 | self.plugin_dir, 62 | 'i18n', 63 | 'RasterTracer_{}.qm'.format(locale)) 64 | 65 | if os.path.exists(locale_path): 66 | self.translator = QTranslator() 67 | self.translator.load(locale_path) 68 | QCoreApplication.installTranslator(self.translator) 69 | 70 | # Declare instance attributes 71 | self.actions = [] 72 | self.menu = self.tr(u'&Raster Tracer') 73 | # TODO: We are going to let the user set this up in a future iteration 74 | self.toolbar = self.iface.addToolBar(u'RasterTracer') 75 | self.toolbar.setObjectName(u'RasterTracer') 76 | 77 | # print "** INITIALIZING RasterTracer" 78 | 79 | self.pluginIsActive = False 80 | self.dockwidget = None 81 | 82 | # noinspection PyMethodMayBeStatic 83 | def tr(self, message): 84 | """Get the translation for a string using Qt translation API. 85 | 86 | We implement this ourselves since we do not inherit QObject. 87 | 88 | :param message: String for translation. 89 | :type message: str, QString 90 | 91 | :returns: Translated version of message. 92 | :rtype: QString 93 | """ 94 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 95 | return QCoreApplication.translate('RasterTracer', message) 96 | 97 | def add_action(self, 98 | icon_path, 99 | text, 100 | callback, 101 | enabled_flag=True, 102 | add_to_menu=True, 103 | add_to_toolbar=True, 104 | status_tip=None, 105 | whats_this=None, 106 | parent=None): 107 | """Add a toolbar icon to the toolbar. 108 | 109 | :param icon_path: Path to the icon for this action. Can be a resource 110 | path (e.g. ':/plugins/foo/bar.png') or a normal file system path. 111 | :type icon_path: str 112 | 113 | :param text: Text that should be shown in menu items for this action. 114 | :type text: str 115 | 116 | :param callback: Function to be called when the action is triggered. 117 | :type callback: function 118 | 119 | :param enabled_flag: A flag indicating if the action should be enabled 120 | by default. Defaults to True. 121 | :type enabled_flag: bool 122 | 123 | :param add_to_menu: Flag indicating whether the action should also 124 | be added to the menu. Defaults to True. 125 | :type add_to_menu: bool 126 | 127 | :param add_to_toolbar: Flag indicating whether the action should also 128 | be added to the toolbar. Defaults to True. 129 | :type add_to_toolbar: bool 130 | 131 | :param status_tip: Optional text to show in a popup when mouse pointer 132 | hovers over the action. 133 | :type status_tip: str 134 | 135 | :param parent: Parent widget for the new action. Defaults None. 136 | :type parent: QWidget 137 | 138 | :param whats_this: Optional text to show in the status bar when the 139 | mouse pointer hovers over the action. 140 | 141 | :returns: The action that was created. Note that the action is also 142 | added to self.actions list. 143 | :rtype: QAction 144 | """ 145 | 146 | icon = QIcon(icon_path) 147 | action = QAction(icon, text, parent) 148 | action.triggered.connect(callback) 149 | action.setEnabled(enabled_flag) 150 | 151 | if status_tip is not None: 152 | action.setStatusTip(status_tip) 153 | 154 | if whats_this is not None: 155 | action.setWhatsThis(whats_this) 156 | 157 | if add_to_toolbar: 158 | self.toolbar.addAction(action) 159 | 160 | if add_to_menu: 161 | self.iface.addPluginToMenu( 162 | self.menu, 163 | action) 164 | 165 | self.actions.append(action) 166 | 167 | return action 168 | 169 | def initGui(self): 170 | """Create the menu entries and toolbar icons inside the QGIS GUI.""" 171 | 172 | icon_path = ':/plugins/raster_tracer/icon.png' 173 | self.add_action( 174 | icon_path, 175 | text=self.tr(u'Trace Raster'), 176 | callback=self.run, 177 | parent=self.iface.mainWindow()) 178 | 179 | # ------------------------------------------------------------------------- 180 | 181 | def onClosePlugin(self): 182 | """Cleanup necessary items here when plugin dockwidget is closed""" 183 | 184 | # disconnects 185 | self.dockwidget.closingPlugin.disconnect(self.onClosePlugin) 186 | 187 | # remove this statement if dockwidget is to remain 188 | # for reuse if plugin is reopened 189 | # Commented next statement since it causes QGIS crashe 190 | # when closing the docked window: 191 | self.dockwidget = None 192 | 193 | self.pluginIsActive = False 194 | 195 | self.tool_identify.deactivate() 196 | QApplication.restoreOverrideCursor() 197 | self.map_canvas.setMapTool(self.last_maptool) 198 | 199 | def unload(self): 200 | """Removes the plugin menu item and icon from QGIS GUI.""" 201 | 202 | # print( "** UNLOAD RasterTracer") 203 | 204 | for action in self.actions: 205 | self.iface.removePluginMenu( 206 | self.tr(u'&Raster Tracer'), 207 | action) 208 | self.iface.removeToolBarIcon(action) 209 | # remove the toolbar 210 | del self.toolbar 211 | 212 | # ------------------------------------------------------------------------- 213 | 214 | def activate_map_tool(self): 215 | ''' Activates map tool''' 216 | self.last_maptool = self.iface.mapCanvas().mapTool() 217 | self.map_canvas.setMapTool(self.tool_identify) 218 | 219 | def run(self): 220 | """Run method that loads and starts the plugin""" 221 | 222 | if self.pluginIsActive: 223 | self.activate_map_tool() 224 | return 225 | 226 | self.pluginIsActive = True 227 | 228 | # print "** STARTING RasterTracer" 229 | 230 | # dockwidget may not exist if: 231 | # first run of plugin 232 | # removed on close (see self.onClosePlugin method) 233 | if self.dockwidget is None: 234 | # Create the dockwidget (after translation) and keep reference 235 | self.dockwidget = RasterTracerDockWidget() 236 | 237 | # connect to provide cleanup on closing of dockwidget 238 | self.dockwidget.closingPlugin.connect(self.onClosePlugin) 239 | 240 | # show the dockwidget 241 | self.iface.addDockWidget(Qt.LeftDockWidgetArea, self.dockwidget) 242 | self.dockwidget.show() 243 | 244 | self.map_canvas = self.iface.mapCanvas() 245 | # vlayer = self.iface.layerTreeView().selectedLayers()[0] 246 | self.tool_identify = PointTool(self.map_canvas, self.iface, self.turn_off_snap) 247 | # self.map_canvas.setMapTool(self.tool_identify) 248 | self.activate_map_tool() 249 | 250 | excluded_layers = [l for l in QgsProject().instance().mapLayers().values() 251 | if isinstance(l, QgsVectorLayer)] 252 | self.dockwidget.mMapLayerComboBox.setExceptedLayerList(excluded_layers) 253 | self.dockwidget.mMapLayerComboBox.currentIndexChanged.connect(self.raster_layer_changed) 254 | self.tool_identify.raster_layer_has_changed(self.dockwidget.mMapLayerComboBox.currentLayer()) 255 | 256 | self.dockwidget.checkBoxColor.stateChanged.connect(self.checkBoxColor_changed) 257 | self.dockwidget.mColorButton.colorChanged.connect(self.checkBoxColor_changed) 258 | 259 | self.dockwidget.checkBoxSnap.stateChanged.connect(self.checkBoxSnap_changed) 260 | self.dockwidget.mQgsSpinBox.valueChanged.connect(self.checkBoxSnap_changed) 261 | 262 | self.map_canvas.setMapTool(self.tool_identify) 263 | # self.last_maptool = self.iface.mapCanvas().mapTool() 264 | 265 | self.dockwidget.checkBoxSmooth.stateChanged.connect(self.checkBoxSmooth_changed) 266 | self.dockwidget.checkBoxSmooth.setChecked(True) 267 | 268 | self.dockwidget.checkBoxSnap2.stateChanged.connect(self.checkBoxSnap2_changed) 269 | self.dockwidget.SpinBoxSnap.valueChanged.connect(self.checkBoxSnap2_changed) 270 | 271 | 272 | def raster_layer_changed(self): 273 | self.tool_identify.raster_layer_has_changed(self.dockwidget.mMapLayerComboBox.currentLayer()) 274 | self.checkBoxColor_changed() 275 | 276 | def checkBoxSmooth_changed(self): 277 | self.tool_identify.smooth_line = (self.dockwidget.checkBoxSmooth.isChecked() is True) 278 | 279 | def checkBoxSnap_changed(self): 280 | if self.dockwidget.checkBoxSnap.isChecked(): 281 | self.dockwidget.mQgsSpinBox.setEnabled(True) 282 | snap_tolerance = self.dockwidget.mQgsSpinBox.value() 283 | self.tool_identify.snap_tolerance_changed(snap_tolerance) 284 | else: 285 | self.dockwidget.mQgsSpinBox.setEnabled(False) 286 | self.tool_identify.snap_tolerance_changed(None) 287 | 288 | def checkBoxSnap2_changed(self): 289 | if self.dockwidget.checkBoxSnap2.isChecked(): 290 | self.dockwidget.SpinBoxSnap.setEnabled(True) 291 | snap_tolerance = self.dockwidget.SpinBoxSnap.value() 292 | self.tool_identify.snap2_tolerance_changed(snap_tolerance) 293 | else: 294 | self.dockwidget.SpinBoxSnap.setEnabled(False) 295 | self.tool_identify.snap2_tolerance_changed(None) 296 | 297 | 298 | def turn_off_snap(self): 299 | self.dockwidget.checkBoxSnap.nextCheckState() 300 | 301 | def checkBoxColor_changed(self): 302 | if self.dockwidget.checkBoxColor.isChecked(): 303 | self.dockwidget.mColorButton.setEnabled(True) 304 | self.dockwidget.checkBoxSnap.setEnabled(True) 305 | color = self.dockwidget.mColorButton.color() 306 | self.tool_identify.trace_color_changed(color) 307 | else: 308 | self.dockwidget.mColorButton.setEnabled(False) 309 | self.dockwidget.checkBoxSnap.setEnabled(False) 310 | self.tool_identify.trace_color_changed(False) 311 | -------------------------------------------------------------------------------- /pointtool.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Main functionality of raster tracer. 3 | ''' 4 | 5 | from enum import Enum 6 | from collections import namedtuple 7 | import numpy as np 8 | 9 | from qgis.core import QgsPointXY, QgsPoint, QgsGeometry, QgsFeature, \ 10 | QgsVectorLayer, QgsProject, QgsWkbTypes, QgsApplication, \ 11 | QgsRectangle, QgsSpatialIndex 12 | from qgis.gui import QgsMapToolEmitPoint, QgsMapToolEdit, \ 13 | QgsRubberBand, QgsVertexMarker, QgsMapTool 14 | from qgis.PyQt.QtCore import Qt 15 | from qgis.PyQt.QtGui import QColor 16 | from qgis.core import Qgis 17 | from qgis.core import QgsCoordinateTransform 18 | 19 | 20 | from .astar import FindPathTask, FindPathFunction 21 | from .line_simplification import smooth, simplify 22 | from .utils import get_whole_raster, PossiblyIndexedImageError 23 | from .pointtool_states import WaitingFirstPointState 24 | from .exceptions import OutsideMapError 25 | 26 | # An point on the map where the user clicked along the line 27 | Anchor = namedtuple('Anchor', ['x', 'y', 'i', 'j']) 28 | 29 | # Flag for experimental Autofollowing mode 30 | ALLOW_AUTO_FOLLOWING = False 31 | 32 | 33 | class TracingModes(Enum): 34 | ''' 35 | Possible Tracing Modes for Pointtool. 36 | LINE - straight line from start to end. 37 | PATH - tracing along color from start to end. 38 | AUTO - auto tracing mode along color in the given direction. 39 | ''' 40 | 41 | LINE = 1 42 | PATH = 2 43 | AUTO = 3 44 | 45 | def next(self): 46 | ''' 47 | Switches between LINE and PATH 48 | ''' 49 | cls = self.__class__ 50 | members = list(cls) 51 | 52 | if not ALLOW_AUTO_FOLLOWING: 53 | return members[0] if self.value == 2 else members[1] 54 | 55 | index = members.index(self) + 1 56 | if index >= len(members): 57 | index = 0 58 | return members[index] 59 | 60 | def is_tracing(self): 61 | ''' 62 | Returns True if mode is PATH 63 | ''' 64 | return True if self.value == 2 else False 65 | 66 | def is_auto(self): 67 | ''' 68 | Returns True if mode is PATH 69 | ''' 70 | return True if self.value == 3 else False 71 | 72 | 73 | # Line styles for the rubber band 74 | RUBBERBAND_LINE_STYLES = { 75 | TracingModes.PATH: Qt.DotLine, 76 | TracingModes.LINE: Qt.SolidLine, 77 | TracingModes.AUTO: Qt.DashDotLine, 78 | } 79 | 80 | 81 | class PointTool(QgsMapToolEdit): 82 | ''' 83 | Implementation of interactions of the user with the main map. 84 | Will called every time the user clicks on the map 85 | or hovers the mouse over the map. 86 | ''' 87 | 88 | def deactivate(self): 89 | QgsMapTool.deactivate(self) 90 | self.deactivated.emit() 91 | 92 | def __init__(self, canvas, iface, turn_off_snap, smooth=False): 93 | ''' 94 | canvas - link to the QgsCanvas of the application 95 | iface - link to the Qgis Interface 96 | turn_off_snap - flag sets snapping to the nearest color 97 | smooth - flag sets smoothing of the traced path 98 | ''' 99 | 100 | self.iface = iface 101 | 102 | # list of Anchors for current line 103 | self.anchors = [] 104 | 105 | # for keeping track of mouse event for rubber band updating 106 | self.last_mouse_event_pos = None 107 | 108 | self.tracing_mode = TracingModes.PATH 109 | 110 | self.turn_off_snap = turn_off_snap 111 | self.smooth_line = smooth 112 | 113 | # possible variants: gray_diff, as_is, color_diff (using v from hsv) 114 | self.grid_conversion = "gray_diff" 115 | 116 | # QApplication.restoreOverrideCursor() 117 | # QApplication.setOverrideCursor(Qt.CrossCursor) 118 | QgsMapToolEmitPoint.__init__(self, canvas) 119 | 120 | self.rlayer = None 121 | self.grid_changed = None 122 | self.snap_tolerance = None # snap to color 123 | self.snap2_tolerance = None # snap to itself 124 | self.vlayer = None 125 | self.grid = None 126 | self.sample = None 127 | 128 | self.tracking_is_active = False 129 | 130 | # False = not a polygon 131 | self.rubber_band = QgsRubberBand(self.canvas(), QgsWkbTypes.LineGeometry) 132 | self.markers = [] 133 | self.marker_snap = QgsVertexMarker(self.canvas()) 134 | self.marker_snap.setColor(QColor(255, 0, 255)) 135 | 136 | self.find_path_task = None 137 | 138 | self.change_state(WaitingFirstPointState) 139 | 140 | self.last_vlayer = None 141 | 142 | def display_message(self, 143 | title, 144 | message, 145 | level='Info', 146 | duration=2, 147 | ): 148 | ''' 149 | Shows message bar to the user. 150 | `level` receives one of four possible string values: 151 | Info, Warning, Critical, Success 152 | ''' 153 | 154 | LEVELS = { 155 | 'Info': Qgis.Info, 156 | 'Warning': Qgis.Warning, 157 | 'Critical': Qgis.Critical, 158 | 'Success': Qgis.Success, 159 | } 160 | 161 | self.iface.messageBar().pushMessage( 162 | title, 163 | message, 164 | LEVELS[level], 165 | duration) 166 | 167 | def change_state(self, state): 168 | self.state = state(self) 169 | 170 | def snap_tolerance_changed(self, snap_tolerance): 171 | self.snap_tolerance = snap_tolerance 172 | if snap_tolerance is None: 173 | self.marker_snap.hide() 174 | else: 175 | self.marker_snap.show() 176 | 177 | def snap2_tolerance_changed(self, snap_tolerance): 178 | self.snap2_tolerance = snap_tolerance**2 179 | # if snap_tolerance is None: 180 | # self.marker_snap.hide() 181 | # else: 182 | # self.marker_snap.show() 183 | 184 | def trace_color_changed(self, color): 185 | r, g, b = self.sample 186 | 187 | if color is False: 188 | self.grid_changed = None 189 | else: 190 | r0, g0, b0, t = color.getRgb() 191 | self.grid_changed = np.abs((r0 - r) ** 2 + (g0 - g) ** 2 + 192 | (b0 - b) ** 2) 193 | 194 | def get_current_vector_layer(self): 195 | try: 196 | vlayer = self.iface.layerTreeView().selectedLayers()[0] 197 | if isinstance(vlayer, QgsVectorLayer): 198 | if vlayer.wkbType() == QgsWkbTypes.MultiLineString: 199 | # if self.last_vlayer: 200 | # if vlayer != self.last_vlayer: 201 | # self.create_spatial_index_for_vlayer(vlayer) 202 | # else: 203 | # self.create_spatial_index_for_vlayer(vlayer) 204 | # self.last_vlayer = vlayer 205 | return vlayer 206 | else: 207 | self.display_message( 208 | " ", 209 | "The active layer must be" + 210 | " a MultiLineString vector layer", 211 | level='Warning', 212 | duration=2, 213 | ) 214 | return None 215 | else: 216 | self.display_message( 217 | "Missing Layer", 218 | "Please select vector layer to draw", 219 | level='Warning', 220 | duration=2, 221 | ) 222 | return None 223 | except IndexError: 224 | self.display_message( 225 | "Missing Layer", 226 | "Please select vector layer to draw", 227 | level='Warning', 228 | duration=2, 229 | ) 230 | return None 231 | 232 | def raster_layer_has_changed(self, raster_layer): 233 | self.rlayer = raster_layer 234 | if self.rlayer is None: 235 | self.display_message( 236 | "Missing Layer", 237 | "Please select raster layer to trace", 238 | level='Warning', 239 | duration=2, 240 | ) 241 | return 242 | 243 | try: 244 | sample, to_indexes, to_coords, to_coords_provider, \ 245 | to_coords_provider2 = \ 246 | get_whole_raster(self.rlayer, 247 | QgsProject.instance(), 248 | ) 249 | except PossiblyIndexedImageError: 250 | self.display_message( 251 | "Missing Layer", 252 | "Can't trace indexed or gray image", 253 | level='Critical', 254 | duration=2, 255 | ) 256 | return 257 | 258 | r = sample[0].astype(float) 259 | g = sample[1].astype(float) 260 | b = sample[2].astype(float) 261 | where_are_NaNs = np.isnan(r) 262 | r[where_are_NaNs] = 0 263 | where_are_NaNs = np.isnan(g) 264 | g[where_are_NaNs] = 0 265 | where_are_NaNs = np.isnan(b) 266 | b[where_are_NaNs] = 0 267 | 268 | self.sample = (r, g, b) 269 | self.grid = r + g + b 270 | self.to_indexes = to_indexes 271 | self.to_coords = to_coords 272 | self.to_coords_provider = to_coords_provider 273 | self.to_coords_provider2 = to_coords_provider2 274 | 275 | def remove_last_anchor_point(self, undo_edit=True, redraw=True): 276 | ''' 277 | Removes last anchor point and last marker point 278 | ''' 279 | 280 | # check if we have at least one feature to delete 281 | vlayer = self.get_current_vector_layer() 282 | if vlayer is None: 283 | return 284 | if vlayer.featureCount() < 1: 285 | return 286 | 287 | # remove last marker 288 | if self.markers: 289 | last_marker = self.markers.pop() 290 | self.canvas().scene().removeItem(last_marker) 291 | 292 | # remove last anchor 293 | if self.anchors: 294 | self.anchors.pop() 295 | 296 | if undo_edit: 297 | # it's a very ugly way of triggering single undo event 298 | self.iface.editMenu().actions()[0].trigger() 299 | 300 | if redraw: 301 | self.update_rubber_band() 302 | self.redraw() 303 | 304 | def keyPressEvent(self, e): 305 | if e.key() == Qt.Key_Backspace or e.key() == Qt.Key_B: 306 | # delete last segment if backspace is pressed 307 | self.remove_last_anchor_point() 308 | elif e.key() == Qt.Key_A: 309 | # change tracing mode 310 | self.tracing_mode = self.tracing_mode.next() 311 | self.update_rubber_band() 312 | elif e.key() == Qt.Key_S: 313 | # toggle snap mode 314 | self.turn_off_snap() 315 | elif e.key() == Qt.Key_Escape: 316 | # Abort tracing process 317 | self.abort_tracing_process() 318 | 319 | def add_anchor_points(self, x1, y1, i1, j1): 320 | ''' 321 | Adds anchor points and markers to self. 322 | ''' 323 | 324 | anchor = Anchor(x1, y1, i1, j1) 325 | self.anchors.append(anchor) 326 | 327 | marker = QgsVertexMarker(self.canvas()) 328 | marker.setCenter(QgsPointXY(x1, y1)) 329 | self.markers.append(marker) 330 | 331 | def trace_over_image(self, 332 | start, 333 | goal, 334 | do_it_as_task=False, 335 | vlayer=None): 336 | ''' 337 | performs tracing 338 | ''' 339 | 340 | i0, j0 = start 341 | i1, j1 = goal 342 | 343 | r, g, b, = self.sample 344 | 345 | try: 346 | r0 = r[i1, j1] 347 | g0 = g[i1, j1] 348 | b0 = b[i1, j1] 349 | except IndexError: 350 | raise OutsideMapError 351 | 352 | if self.grid_changed is None: 353 | grid = np.abs((r0 - r) ** 2 + (g0 - g) ** 2 + (b0 - b) ** 2) 354 | else: 355 | grid = self.grid_changed 356 | 357 | if do_it_as_task: 358 | # dirty hack to avoid QGIS crashing 359 | self.find_path_task = FindPathTask( 360 | grid.astype(np.dtype('l')), 361 | start, 362 | goal, 363 | self.draw_path, 364 | vlayer, 365 | ) 366 | 367 | QgsApplication.taskManager().addTask( 368 | self.find_path_task, 369 | ) 370 | self.tracking_is_active = True 371 | else: 372 | path, cost = FindPathFunction( 373 | grid.astype(np.dtype('l')), 374 | (i0, j0), 375 | (i1, j1), 376 | ) 377 | return path, cost 378 | 379 | def trace(self, x1, y1, i1, j1, vlayer): 380 | ''' 381 | Traces path from last point to given point. 382 | In case tracing is inactive just creates 383 | straight line. 384 | ''' 385 | 386 | if self.tracing_mode.is_tracing(): 387 | if self.snap_tolerance is not None: 388 | try: 389 | i1, j1 = self.snap(i1, j1) 390 | except OutsideMapError: 391 | return 392 | 393 | _, _, i0, j0 = self.anchors[-2] 394 | start_point = i0, j0 395 | end_point = i1, j1 396 | try: 397 | self.trace_over_image(start_point, 398 | end_point, 399 | do_it_as_task=True, 400 | vlayer=vlayer) 401 | except OutsideMapError: 402 | pass 403 | else: 404 | self.draw_path( 405 | None, 406 | vlayer, 407 | was_tracing=False, 408 | x1=x1, 409 | y1=y1, 410 | ) 411 | 412 | def snap_to_itself(self, x, y, sq_tolerance=1): 413 | ''' 414 | finds a nearest segment line to the current vlayer 415 | ''' 416 | 417 | pt = QgsPointXY(x, y) 418 | # nearest_feature_id = self.spIndex.nearestNeighbor(pt, 1, tolerance)[0] 419 | vlayer = self.get_current_vector_layer() 420 | # feature = vlayer.getFeature(nearest_feature_id) 421 | for feature in vlayer.getFeatures(): 422 | closest_point, _, _, _, sq_distance = feature.geometry().closestVertex(pt) 423 | if sq_distance < sq_tolerance: 424 | return closest_point.x(), closest_point.y() 425 | return x, y 426 | 427 | def snap(self, i, j): 428 | if self.snap_tolerance is None: 429 | return i, j 430 | if not self.tracing_mode.is_tracing(): 431 | return i, j 432 | if self.grid_changed is None: 433 | return i, j 434 | 435 | size_i, size_j = self.grid.shape 436 | size = self.snap_tolerance 437 | 438 | if i < size or j < size or i + size > size_i or j + size > size_j: 439 | raise OutsideMapError 440 | 441 | grid_small = self.grid_changed 442 | grid_small = grid_small[i - size: i + size, j - size: j + size] 443 | 444 | smallest_cells = np.where(grid_small == np.amin(grid_small)) 445 | coordinates = list(zip(smallest_cells[0], smallest_cells[1])) 446 | 447 | if len(coordinates) == 1: 448 | delta_i, delta_j = coordinates[0] 449 | delta_i -= size 450 | delta_j -= size 451 | else: 452 | # find the closest to the center 453 | deltas = [(i - size, j - size) for i, j in coordinates] 454 | lengths = [(i ** 2 + j ** 2) for i, j in deltas] 455 | i = lengths.index(min(lengths)) 456 | delta_i, delta_j = deltas[i] 457 | 458 | return i+delta_i, j+delta_j 459 | 460 | def canvasReleaseEvent(self, mouseEvent): 461 | ''' 462 | Method where the actual tracing is performed 463 | after the user clicked on the map 464 | ''' 465 | 466 | vlayer = self.get_current_vector_layer() 467 | 468 | if vlayer is None: 469 | return 470 | 471 | if not vlayer.isEditable(): 472 | self.display_message( 473 | "Edit mode", 474 | "Please begin editing vector layer to trace", 475 | level='Warning', 476 | duration=2, 477 | ) 478 | return 479 | 480 | if self.rlayer is None: 481 | self.display_message( 482 | "Missing Layer", 483 | "Please select raster layer to trace", 484 | level='Warning', 485 | duration=2, 486 | ) 487 | return 488 | 489 | if mouseEvent.button() == Qt.RightButton: 490 | self.state.click_rmb(mouseEvent, vlayer) 491 | elif mouseEvent.button() == Qt.LeftButton: 492 | self.state.click_lmb(mouseEvent, vlayer) 493 | 494 | return 495 | 496 | def draw_path(self, path, vlayer, was_tracing=True,\ 497 | x1=None, y1=None): 498 | ''' 499 | Draws a path after tracer found it. 500 | ''' 501 | 502 | transform = QgsCoordinateTransform(QgsProject.instance().crs(), 503 | vlayer.crs(), 504 | QgsProject.instance()) 505 | if was_tracing: 506 | if self.smooth_line: 507 | path = smooth(path, size=5) 508 | path = simplify(path) 509 | vlayer = self.get_current_vector_layer() 510 | current_last_point = self.to_coords(*path[-1]) 511 | path_ref = [transform.transform(*self.to_coords_provider(i, j)) for i, j in path] 512 | x0, y0, _, _ = self.anchors[-2] 513 | last_point = transform.transform(*self.to_coords_provider2(x0, y0)) 514 | path_ref = [last_point] + path_ref[1:] 515 | else: 516 | x0, y0, _i, _j = self.anchors[-2] 517 | current_last_point = (x1, y1) 518 | path_ref = [transform.transform(*self.to_coords_provider2(x0, y0)), 519 | transform.transform(*self.to_coords_provider2(x1, y1))] 520 | 521 | 522 | self.ready = False 523 | if len(self.anchors) == 2: 524 | vlayer.beginEditCommand("Adding new line") 525 | add_feature_to_vlayer(vlayer, path_ref) 526 | vlayer.endEditCommand() 527 | else: 528 | vlayer.beginEditCommand("Adding new segment to the line") 529 | add_to_last_feature(vlayer, path_ref) 530 | vlayer.endEditCommand() 531 | _, _, current_last_point_i, current_last_point_j = self.anchors[-1] 532 | self.anchors[-1] = current_last_point[0], current_last_point[1], current_last_point_i, current_last_point_j 533 | self.redraw() 534 | self.tracking_is_active = False 535 | 536 | 537 | def update_rubber_band(self): 538 | # this is very ugly but I can't make another way 539 | if self.last_mouse_event_pos is None: 540 | return 541 | 542 | if not self.anchors: 543 | return 544 | 545 | x0, y0, _, _ = self.anchors[-1] 546 | qgsPoint = self.toMapCoordinates(self.last_mouse_event_pos) 547 | x1, y1 = qgsPoint.x(), qgsPoint.y() 548 | points = [QgsPoint(x0, y0), QgsPoint(x1, y1)] 549 | 550 | self.rubber_band.setColor(QColor(255, 0, 0)) 551 | self.rubber_band.setWidth(3) 552 | 553 | self.rubber_band.setLineStyle( 554 | RUBBERBAND_LINE_STYLES[self.tracing_mode], 555 | ) 556 | 557 | vlayer = self.get_current_vector_layer() 558 | if vlayer is None: 559 | return 560 | 561 | self.rubber_band.setToGeometry( 562 | QgsGeometry.fromPolyline(points), 563 | self.vlayer, 564 | ) 565 | 566 | def canvasMoveEvent(self, mouseEvent): 567 | ''' 568 | Store the mouse position for the correct 569 | updating of the rubber band 570 | ''' 571 | 572 | # we need at least one point to draw 573 | if not self.anchors: 574 | return 575 | 576 | if self.snap_tolerance is not None and self.tracing_mode.is_tracing(): 577 | qgsPoint = self.toMapCoordinates(mouseEvent.pos()) 578 | x1, y1 = qgsPoint.x(), qgsPoint.y() 579 | # i, j = get_indxs_from_raster_coords(self.geo_ref, x1, y1) 580 | i, j = self.to_indexes(x1, y1) 581 | try: 582 | i1, j1 = self.snap(i, j) 583 | except OutsideMapError: 584 | return 585 | # x1, y1 = get_coords_from_raster_indxs(self.geo_ref, i1, j1) 586 | x1, y1 = self.to_coords(i1, j1) 587 | self.marker_snap.setCenter(QgsPointXY(x1, y1)) 588 | 589 | self.last_mouse_event_pos = mouseEvent.pos() 590 | self.update_rubber_band() 591 | self.redraw() 592 | 593 | def abort_tracing_process(self): 594 | ''' 595 | Terminate background process of tracing raster 596 | after the user hits Esc. 597 | ''' 598 | 599 | # check if we have any tasks 600 | if self.find_path_task is None: 601 | return 602 | 603 | self.tracking_is_active = False 604 | 605 | try: 606 | # send terminate signal to the task 607 | self.find_path_task.cancel() 608 | self.find_path_task = None 609 | except RuntimeError: 610 | return 611 | else: 612 | self.remove_last_anchor_point( 613 | undo_edit=False, 614 | ) 615 | 616 | def redraw(self): 617 | # If caching is enabled, a simple canvas refresh might not be 618 | # sufficient to trigger a redraw and you must clear the cached image 619 | # for the layer 620 | if self.iface.mapCanvas().isCachingEnabled(): 621 | vlayer = self.get_current_vector_layer() 622 | if vlayer is None: 623 | return 624 | vlayer.triggerRepaint() 625 | 626 | self.iface.mapCanvas().refresh() 627 | QgsApplication.processEvents() 628 | 629 | def pan(self, x, y): 630 | ''' 631 | Move the canvas to the x, y position 632 | ''' 633 | currExt = self.iface.mapCanvas().extent() 634 | canvasCenter = currExt.center() 635 | dx = x - canvasCenter.x() 636 | dy = y - canvasCenter.y() 637 | xMin = currExt.xMinimum() + dx 638 | xMax = currExt.xMaximum() + dx 639 | yMin = currExt.yMinimum() + dy 640 | yMax = currExt.yMaximum() + dy 641 | newRect = QgsRectangle(xMin, yMin, xMax, yMax) 642 | self.iface.mapCanvas().setExtent(newRect) 643 | 644 | def add_last_feature_to_spindex(self, vlayer): 645 | ''' 646 | Adds last feature to spatial index 647 | ''' 648 | features = list(vlayer.getFeatures()) 649 | last_feature = features[-1] 650 | self.spIndex.insertFeature(last_feature) 651 | 652 | def create_spatial_index_for_vlayer(self, vlayer): 653 | ''' 654 | Creates spatial index for the vlayer 655 | ''' 656 | 657 | self.spIndex = QgsSpatialIndex() 658 | # features = [f for f in vlayer] 659 | self.spIndex.addFeatures(vlayer.getFeatures()) 660 | 661 | 662 | 663 | def add_to_last_feature(vlayer, points): 664 | ''' 665 | Adds points to the last line feature in the vlayer 666 | vlayer - QgsLayer of type MultiLine string 667 | points - list of points 668 | ''' 669 | features = list(vlayer.getFeatures()) 670 | last_feature = features[-1] 671 | fid = last_feature.id() 672 | geom = last_feature.geometry() 673 | points = [QgsPointXY(x, y) for x, y in points] 674 | geom.addPointsXY(points) 675 | vlayer.changeGeometry(fid, geom) 676 | 677 | 678 | def add_feature_to_vlayer(vlayer, points): 679 | ''' 680 | Adds new line feature to the vlayer 681 | ''' 682 | 683 | feat = QgsFeature(vlayer.fields()) 684 | polyline = [QgsPoint(x, y) for x, y in points] 685 | feat.setGeometry(QgsGeometry.fromPolyline(polyline)) 686 | vlayer.addFeature(feat) 687 | 688 | -------------------------------------------------------------------------------- /resources.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.9.5) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x1c\xa7\ 13 | \x89\ 14 | \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ 15 | \x00\x00\x17\x00\x00\x00\x18\x08\x06\x00\x00\x00\x11\x7c\x66\x75\ 16 | \x00\x00\x19\x00\x7a\x54\x58\x74\x52\x61\x77\x20\x70\x72\x6f\x66\ 17 | \x69\x6c\x65\x20\x74\x79\x70\x65\x20\x65\x78\x69\x66\x00\x00\x78\ 18 | \xda\xad\x9b\x69\x72\x1c\x39\x96\xad\xff\x63\x15\xbd\x04\xc7\x0c\ 19 | \x2c\x07\xa3\x59\xef\xe0\x2d\xbf\xbf\x03\x38\x29\x8a\x1a\xb2\xb2\ 20 | \xec\x49\x95\x0a\x32\xc2\x03\x0e\xdc\xe1\x0c\x80\x97\x59\xff\xef\ 21 | \x7f\xb7\xf9\x1f\xfe\xa4\x9c\x1e\x13\x62\x2e\xa9\xa6\xf4\xf0\x27\ 22 | \xd4\x50\x5d\xe3\x87\xf2\xdc\x3f\xf7\xd5\x3e\xe1\xfc\x7b\x7f\xc9\ 23 | \xef\x67\xf6\xe7\xf7\x8d\xfd\xf8\x92\xe3\x2d\xcf\xab\xbf\xbf\xa6\ 24 | \xf5\x5e\xdf\x78\x3f\xfe\xf8\x42\x0e\xef\xfb\xfd\xe7\xf7\x4d\x1e\ 25 | \xef\x38\xe5\x1d\xe8\xfd\xe0\x63\x40\xaf\x3b\x3b\x7e\x98\xef\x24\ 26 | \xdf\x81\xbc\xbb\xef\xdb\xf7\x77\x53\xdd\xfd\xa1\xa5\x2f\xcb\x79\ 27 | \xff\xf3\xf9\x0c\xf1\x79\xf1\xf7\xdf\x43\x26\x18\x33\xf2\xa6\x77\ 28 | \xc6\x2d\x6f\xfd\x73\xfe\x75\xf7\x4e\x9e\x59\xf8\xea\x9b\xde\x39\ 29 | \xff\xea\x42\xeb\x03\x3f\x47\x5f\xf8\xd7\xfb\xf4\x6b\xfc\x8c\x7e\ 30 | \xdb\xf1\xf7\x01\xfc\xfc\xe9\x5b\xfc\x9e\xf1\xbe\xef\x7f\x84\xc3\ 31 | \xdc\xc8\xbe\x17\xa4\x6f\x71\x7a\xdf\xb7\xf1\xf7\xf1\x3b\x51\xfa\ 32 | \x3a\x23\xeb\x3e\xef\xec\xbe\xce\x28\x6c\x5b\x9f\xaf\x7f\xbe\xc4\ 33 | \x6f\xef\x59\xf6\x5e\x77\x75\x2d\x24\x43\xb8\xd2\xbb\xa8\x8f\xa5\ 34 | \x9c\x9f\xb8\xb0\x33\x94\x3f\x5f\x4b\xfc\xcd\xfc\x17\xf9\x39\x9f\ 35 | \xbf\x95\xbf\xe5\x69\xcf\x20\xf0\x93\xa5\x76\xf3\x74\x7e\xa9\xd6\ 36 | \x11\xd3\x6d\x83\x9d\xb6\xd9\x6d\xd7\x79\x1d\x76\x30\xc5\xe0\x96\ 37 | \xcb\xbc\x3a\x37\x9c\x3f\xef\x15\x9f\x5d\x75\xc3\xdf\x14\xf0\xd7\ 38 | \x6e\x97\x0d\xf9\x99\xe4\xc2\xf9\x41\xe6\x3c\x6f\xbb\xcf\xb9\xd8\ 39 | \x73\xdf\xaa\xfb\x71\xb3\xc2\x9d\xa7\xe5\x4a\x67\x19\x4c\x19\xfd\ 40 | \xe9\xaf\xf9\xfe\xc6\x7f\xfb\xf7\xa7\x81\xf6\x56\x99\x5b\xfb\x94\ 41 | \x37\x4e\x51\x09\x76\xaa\x2f\xa6\xa1\xcc\xe9\x5f\xae\x22\x21\x76\ 42 | \xbf\x31\x8d\x27\xbe\xd6\xdc\x97\xe7\xfb\x1f\x25\xd6\x93\xc1\x78\ 43 | \xc2\x5c\x58\x60\x7b\xfa\x1d\xa2\x47\xfb\xa3\xb6\xfc\xc9\xb3\x7f\ 44 | \xa2\xe1\xd2\xf0\x84\xb7\xb1\xe7\x3b\x00\x21\xe2\xde\x91\xc9\x58\ 45 | \x4f\x06\x9e\x64\x7d\xb4\xc9\x3e\xd9\xb9\x6c\x2d\x71\x2c\xe4\xa7\ 46 | \x31\x73\xe7\x83\xeb\x64\xc0\x46\x13\xdd\x64\x96\x2e\x50\xf7\x24\ 47 | \xa7\x38\xdd\x9b\xef\x64\x7b\xae\x75\xd1\xdd\xb7\x81\x17\x12\x11\ 48 | \x7d\xf2\x99\xd4\xd0\x40\x24\x2b\x84\x18\x12\xfd\x56\x28\xa1\x66\ 49 | \xa2\x8f\x21\xc6\x98\x62\x8e\x25\xd6\xd8\x92\x4f\x21\xc5\x94\x00\ 50 | \x2b\xe1\x54\xcb\x3e\x87\x1c\x73\xca\x39\x97\x5c\x73\x2b\xbe\x84\ 51 | \x12\x4b\x2a\xb9\x94\x52\x4b\xab\xae\x7a\x60\x2c\x9a\x9a\x6a\xae\ 52 | \xa5\xd6\xda\x1a\x37\x6d\xa1\x31\x56\xe3\xfa\xc6\x1b\xdd\x75\xdf\ 53 | \x43\x8f\x3d\xf5\xdc\x4b\xaf\xbd\x0d\xca\x67\x84\x11\x47\x1a\x79\ 54 | \x94\x51\x47\x9b\x6e\xfa\x09\x04\x98\x99\x66\x9e\x65\xd6\xd9\x96\ 55 | \x5d\x94\xd2\x0a\x2b\xae\xb4\xf2\x2a\xab\xae\xb6\xa9\xb5\xed\x77\ 56 | \xd8\x71\xa7\x9d\x77\xd9\x75\xb7\xcf\xac\xd9\xb7\x6d\x7f\xca\xda\ 57 | \xf7\xcc\xfd\x3d\x6b\xf6\xcd\x9a\x3b\x89\xd2\x75\xf9\x47\xd6\x78\ 58 | \x3b\xe7\x8f\x21\xac\xe0\x24\x2a\x67\x64\xcc\x05\x4b\xc6\xb3\x32\ 59 | \x40\x41\x3b\xe5\xec\x29\x36\x04\xa7\xcc\x29\x67\x4f\x75\xde\x78\ 60 | \x1f\x1d\xb3\x8c\x4a\xce\xb4\xca\x18\x19\x0c\xcb\xba\xb8\xed\x67\ 61 | \xee\x7e\x64\xee\x8f\x79\x33\x44\xf7\xdf\xe6\xcd\xfd\x2e\x73\x46\ 62 | \xa9\xfb\xff\x91\x39\xa3\xd4\x7d\xc9\xdc\xaf\x79\xfb\x4d\xd6\x66\ 63 | \x3b\x70\xeb\x4f\x82\xd4\x85\xc4\x14\x84\xf4\xb4\x1f\x17\xb5\x56\ 64 | \x73\xdc\xab\x87\xbc\x3b\xff\xcc\x56\xf7\xb4\xa9\xd5\xc9\x9f\x51\ 65 | \x57\x2a\xcb\xe6\xb5\x76\x8b\xf9\x59\x4e\x94\xb0\xa8\xf4\x02\x82\ 66 | \x8d\xd0\xeb\xde\x0c\x14\x93\x5f\x9d\x18\xd5\x14\x3a\x41\x80\x4a\ 67 | \x83\xdb\x71\xcd\x68\xd7\xf6\xbc\x2c\x37\x63\xab\x96\x36\xd0\x1c\ 68 | \x4b\x4b\xf5\x99\xcd\x95\xe6\x7a\x1d\xfd\x39\x3f\x01\xbd\x86\xdf\ 69 | \x57\x06\x7d\x41\xd4\xc8\xf7\x1c\x3f\xd6\x0d\x82\xe9\x9d\xb5\xc7\ 70 | \x50\xf1\x64\xd7\x67\xf4\xcb\xb6\x5c\x7a\xe2\xf7\x14\x97\x1d\xbd\ 71 | \x4c\x72\x60\x4b\x62\xd4\xda\xbc\xe9\x3d\x95\xc7\xcd\x4e\x54\xdb\ 72 | \x9a\x4f\x21\x54\xbb\xd6\xb2\x7d\x4d\x3e\xce\x18\xfb\x93\x99\x6c\ 73 | \xc8\xb5\xc5\x6e\xeb\x48\xe0\x7c\x58\xa3\xac\xb1\x0a\x61\xdf\x36\ 74 | \xb2\x9a\xec\x9b\x35\x24\xcc\xf2\x85\xd6\x42\x58\x6b\xfa\xa7\x8c\ 75 | \x52\x83\x85\x25\x63\x2c\x3b\x39\x3f\x59\x01\x4c\x40\xb6\x6b\xd3\ 76 | \xec\x76\x9f\x67\xfe\x73\xf3\x2e\xaf\x83\xa0\x78\xe6\x6a\x28\xb3\ 77 | \xde\x68\xbb\xe1\x53\xac\xa5\x8c\x10\xfc\xa4\xb4\x42\x9b\x8b\x0a\ 78 | \x48\x91\x5b\xa6\xcc\x37\x3a\x90\x94\xb5\xec\xdc\xcf\xb2\xfb\xb2\ 79 | \x67\xa0\xd2\x9a\xdf\xcb\x26\xe3\x52\x3b\x6f\xf8\xb5\x93\xdf\x3b\ 80 | \x0f\xaf\xdf\x98\x72\x80\xb8\x06\x0b\xd8\x8b\xb2\x21\xfb\x71\xe7\ 81 | \xba\x6a\x45\x06\x55\x92\x44\x31\x51\x37\x10\x5b\x9e\x6b\xe4\xbd\ 82 | \xb6\x29\xf9\x84\xd5\xb5\x37\xe0\x77\x05\x4c\xf9\xae\xe1\xac\x80\ 83 | \x81\x59\x81\x5d\x23\x25\xcb\xf0\x84\x7c\x8d\x4e\x4a\x27\x6d\xd7\ 84 | \xb6\x3f\xb7\x36\x7f\xfd\xea\x3f\x2c\x49\x53\xf6\xbb\x33\xff\x3c\ 85 | \x4d\x65\x6e\xff\x26\x16\x7f\x0a\x85\x89\x81\x6f\x54\x16\x98\xc0\ 86 | \xb5\xdd\xf9\xdf\xd8\x61\x50\x5d\x63\xec\xbf\x7d\x91\xef\x7d\x06\ 87 | \x8a\xd0\x64\xf3\xc6\x4a\xa1\x3a\x81\x4a\x1e\xee\xfe\x25\xbe\x96\ 88 | \x31\x91\x14\x71\xee\xd4\x3b\xa5\xa2\x16\x66\x11\xbd\x89\xc8\x53\ 89 | \x78\x92\x33\x11\x04\x74\xd4\x7f\xce\x2e\x4f\xae\x7a\x1a\x75\x14\ 90 | \x47\x9c\xb5\xae\x0c\x0e\x3d\xd3\x33\xea\xa6\xa3\xc3\x1c\xa7\x1d\ 91 | \x5a\xa5\x5b\x47\x9d\xa9\x6e\x0a\x1b\x6c\x0a\x9d\x55\x18\xba\x8c\ 92 | \xee\xa1\x85\x1b\x98\xd3\x26\xc1\xa6\xd3\x96\xdf\x63\x75\x9a\x74\ 93 | \x32\x7c\xe3\xf7\xa8\xcf\xd6\xaa\x7b\x14\xda\x23\x77\x61\x41\x63\ 94 | \xf6\x4f\x27\x55\x79\xac\x3d\x0d\xcb\xab\xc3\x02\x22\xb4\x5f\x6b\ 95 | \x1f\x4d\x5f\x9a\x1a\x1d\x84\x1e\x5d\xed\xda\xd7\x74\x75\x15\x3a\ 96 | \x06\xa0\xa1\xee\x43\xa1\x03\x76\x58\x1b\x46\x1b\x99\x26\xd9\xe0\ 97 | \x51\x74\x6d\xb4\x68\xbb\xf7\x6d\x06\xf8\xd0\xa7\x33\xff\x95\xdd\ 98 | \xbb\x12\x29\xde\xfb\xba\x4b\x5d\x7d\x11\x4e\x94\x89\x42\x0c\x7e\ 99 | \xcf\x25\xc5\x85\xd2\x32\xa7\x6f\x00\xf7\x98\x07\xc1\xdc\x81\x45\ 100 | \x3d\x74\x78\xb5\x80\x08\x59\x18\x70\x2d\x2f\x05\xb8\xd4\x10\x08\ 101 | \x8d\xf5\xe4\x31\xed\x00\x7e\x81\xa5\xae\x0c\xac\xd2\xb3\x37\xac\ 102 | \x41\x6d\xdb\x66\xeb\x23\x94\xd5\xbd\x24\x11\xf8\x2b\xfc\x8b\x05\ 103 | \x6f\xa0\x14\xdc\xb6\xcd\xf9\xb6\xed\x88\x6f\xdb\xa6\xdb\xb6\x61\ 104 | \x52\x88\x46\x95\x68\x03\xe8\x40\x88\x46\xd9\xd9\x3f\xca\x17\xdd\ 105 | \x4f\x81\xa7\x1c\x18\x3f\x37\xbb\x9e\x1e\xa2\xa5\x0c\xc8\x14\xb4\ 106 | \x74\x7e\x96\xdc\xfe\xf2\x6a\xbe\xbc\x91\x28\x27\x28\xa5\xf5\x34\ 107 | \x61\xc1\x50\xd3\x6a\xab\xba\x98\x43\x2a\x61\xa4\x27\x84\x07\xfd\ 108 | \xed\x46\xcb\x24\x60\xd5\x38\x4f\x97\x9e\xa2\x74\x7e\x1b\xb5\xef\ 109 | \xee\x2a\xc9\x70\x4a\xb2\x8f\x03\x59\x2b\x86\xe5\x61\xcc\x94\x2b\ 110 | \x21\x1e\xac\x78\x17\x2a\x88\xec\x35\xde\xdd\x91\x28\xb7\x9b\x14\ 111 | \x07\x74\x92\x0e\x73\xf3\x11\x07\xd8\x56\x52\x8e\xa1\x54\xfe\xf3\ 112 | \xd5\x46\xb2\x19\xe2\xa6\x76\x7b\x04\x9f\xb2\xcd\x39\x84\x51\xe3\ 113 | \xe8\x74\x53\x0d\x9b\x68\x50\x2e\xa4\xa9\xef\x49\x1f\x91\x35\xe4\ 114 | \xaa\xbb\x58\x5e\xc6\x7c\xbe\x67\xfc\xf3\xf5\x20\x3b\x08\x7c\xd2\ 115 | \x4e\x03\xd5\x4c\xda\x7d\xf2\x37\xeb\xdd\x19\x18\x0d\xad\xb8\x3d\ 116 | \x64\xc7\x2a\x48\xfb\x72\x00\xfe\x1a\x56\xcd\x4f\x0d\x56\xda\x66\ 117 | \xbb\x79\x8a\x1d\x0a\x8d\x89\x22\xf2\x0b\x7f\x43\x91\x6f\xd5\xf8\ 118 | \x96\x41\x98\xd5\x30\x64\x48\x60\x0f\x15\x34\x21\x4e\x50\x03\x44\ 119 | \x4f\xd4\x1b\x6a\x3a\x72\xdf\x68\x27\xd0\xde\xe5\x0d\xab\x3b\x35\ 120 | \xd1\x0b\x7c\x1e\xc1\xe3\x1a\xbb\xdf\xae\xd3\x1b\x2c\x09\x16\xf1\ 121 | \xaa\x91\x97\x89\x9e\xe7\xe5\xa2\x97\x89\xc8\x0a\x5c\xd4\x04\x34\ 122 | \xbe\xf5\x08\xa4\xf8\x90\x01\x08\x1a\x13\x92\x64\xbe\x55\x13\x50\ 123 | \xda\x4d\xc4\x60\x4d\xa7\x1a\xb1\x75\x33\x9b\xa1\xb4\x2e\x91\xc4\ 124 | \x8a\x7d\xa1\x04\x72\xf0\x69\xba\x31\x1d\x93\x18\xb0\xe9\xb2\xb1\ 125 | \xd6\x2c\x86\x4a\xd4\x66\x1b\x1e\xf2\x07\x70\x8d\xeb\xff\x48\x8e\ 126 | \xad\x95\xf5\xac\x36\x29\xe8\x3b\x1f\x25\xf6\x00\xcd\x99\xce\x09\ 127 | \xf0\x36\x90\x1c\x33\x11\xdc\x6d\x74\xca\x41\xbf\xbc\x92\xf8\x01\ 128 | \x8d\x29\x10\xfc\xf9\x63\x3e\xbf\x1f\x73\x2b\x06\x04\x3d\x13\x0a\ 129 | \x04\xa5\x61\xfa\x80\xe6\xdb\xa3\xdc\x85\xd3\xb8\xc2\xed\x95\xc7\ 130 | \x41\xfe\x47\x3c\xed\x45\x08\xac\xe6\xcc\x7c\xdd\xf7\x3f\x2f\x79\ 131 | \x2f\x80\x45\xde\x6b\xce\x15\xb3\x82\x2c\x22\x1a\xda\x6c\x32\x48\ 132 | \xc9\xb4\xe0\x06\x29\x01\x5a\xee\x51\x04\xff\x80\x36\xf3\xa3\x17\ 133 | \xe4\x38\x18\xc0\x8e\x26\x4c\x7e\xf0\xb4\x7d\x66\x14\x47\xde\x4f\ 134 | \x9a\x25\x01\x8b\xa8\x42\xe2\x8f\x90\x82\x09\xfb\x44\xef\x07\xf7\ 135 | \xcc\x2e\xe9\x38\x10\xf6\xe0\x5b\xf3\xbc\xed\x51\x05\x35\xf2\x1e\ 136 | \xaa\x20\x21\x00\xcd\x2b\x0a\x5a\x46\x32\x72\x43\xba\xa2\xf0\xd2\ 137 | \xd7\x5d\x0a\x72\x55\x0c\xd8\x20\x16\x7a\x8d\x82\x75\xe4\xf5\x2c\ 138 | \x8e\xc2\x3b\x69\x6a\x0f\xf5\x57\xeb\x34\x91\x24\xd1\xaf\x9d\x62\ 139 | \x24\x16\xd3\x82\x39\x4f\xdb\x71\x6c\x2e\xa5\x78\x5b\x54\x17\xa4\ 140 | \x79\xe2\x10\x98\x07\x02\x0a\xf8\xa1\x5f\xdc\x09\x38\x98\x7a\xf9\ 141 | \xcf\x50\xb3\x7b\x63\xca\x5b\x85\x17\x81\x8b\x5e\x98\xd1\x9c\x0a\ 142 | \x07\xc2\xb6\x7a\x9f\xdf\x2e\x0f\x68\xc2\x6f\x2d\x08\xb4\x64\xd8\ 143 | \x75\xb7\xa3\x46\x7c\xa1\x14\xad\x64\x27\x12\xf9\xd9\x88\x36\xc0\ 144 | \x7c\xcc\xb0\x24\x4a\x51\xa2\x8e\x34\x04\xbc\xaa\x30\x0b\x62\x93\ 145 | \xfe\x3e\x00\xf6\xfc\x78\xcd\x35\x52\x47\x2c\x15\x15\xdc\x88\xc1\ 146 | \x00\x6c\x73\xda\x94\x5f\x9d\xd9\x27\xf9\x29\x8b\x1c\x3b\xe5\xec\ 147 | \x50\x67\x69\x42\x37\x53\xfd\x05\x30\x79\x20\x0d\x1d\xc7\xbc\x66\ 148 | \x2a\x70\x1c\x36\xab\x66\x2f\x7a\x14\xfd\x3b\xda\x31\x8f\x2e\xd9\ 149 | \xf8\x31\x3a\x22\xbb\x15\x5c\xc3\x0c\x69\x25\xa8\xa0\xa4\x50\xa1\ 150 | \x29\xfa\x7a\x71\x57\xc6\xa6\xb3\x19\xdb\x1b\x57\x18\xbc\x25\xc0\ 151 | \x48\x6b\xa5\x9e\xc7\x69\xd5\x52\xf6\xad\x5f\x1c\xc4\xad\x77\x44\ 152 | \xbe\xf8\x6b\x44\x6a\x97\x8e\xe9\x01\x58\x73\x0b\x54\xa0\xf9\x69\ 153 | \x19\xd3\x18\x81\x84\x35\x75\xfc\xa9\xba\x57\x14\x0c\x7f\xb9\x82\ 154 | \xca\xb3\x54\x02\x75\xe7\xcf\x05\xb9\x1c\x79\x75\x3f\x7e\x3f\x54\ 155 | \x59\x1a\x55\x2e\x0b\xa5\x41\x3b\xd4\x08\xb6\x4d\x20\x8c\x74\xfb\ 156 | \xa9\x25\x92\xe9\x99\x01\xef\x49\xa1\x95\xcc\xd7\xa9\x56\xe8\x29\ 157 | \x57\x00\x41\x6d\x92\x27\xc1\xa6\x83\x07\x05\x89\x8f\xa1\xac\x6c\ 158 | \xf6\x20\x13\xe5\x96\xe0\x0b\xbb\xa0\xb2\x75\x51\xe1\x99\x2f\x2a\ 159 | \x1c\x48\xbd\xb8\x70\x6a\x71\x24\x64\xe1\xa1\x5c\x21\xef\xba\x4c\ 160 | \x0b\xf8\xc2\x21\x40\xaf\x8d\x07\x79\x07\x4d\x83\xc2\x78\x2a\x93\ 161 | \x23\x34\xd4\x21\xb0\x81\xf5\x01\x95\xfc\xa7\x2a\xff\xf9\x16\xe6\ 162 | \xfb\x3d\xf6\xe7\x15\x42\xcb\x7d\xd1\x52\x55\x7d\xf1\x32\x1d\xf2\ 163 | \x6f\x2a\x67\x51\x13\x3a\x65\xc3\xd3\xa8\x4b\xf3\xbc\xd0\x01\x2e\ 164 | \x7f\x40\x87\x2a\x5d\xd0\xf1\xf6\x5b\x99\x77\x49\x9f\x97\x9c\x76\ 165 | \xb9\xe0\xf1\x5e\xc4\x25\xe6\x02\x11\x10\xd2\xe2\x81\x90\xf5\x48\ 166 | \xf6\x00\x97\x2a\x7d\x71\xc1\x19\x64\x54\xb7\xdf\x08\xcd\x2e\x51\ 167 | \x01\x05\xf7\x05\xf8\x7e\x5e\x63\xfe\x70\x11\x89\x0d\x40\x37\xed\ 168 | \xcb\x67\x15\xf5\x44\xf3\xb3\x9c\x3f\x23\x84\xf9\x4f\x20\x22\x57\ 169 | \x7c\xd7\xb2\xe8\x51\x32\x84\x20\x0d\x71\x35\x35\x48\xf6\x43\x65\ 170 | \xe4\x5c\x8d\xe5\x66\xad\x9f\x9c\xa1\xc8\x28\x7b\xf2\x55\xd7\x73\ 171 | \x06\xaa\x36\x34\x49\xd5\xd9\x34\x51\x0c\x0d\xab\x67\x3c\xb7\x68\ 172 | \x6e\x54\xc9\xcd\x5e\xcd\x2b\x8c\xb9\xcc\xeb\xbc\x9e\x5d\xb5\x85\ 173 | \x88\x07\x69\x28\x3e\xbc\x35\x4a\x13\xce\x3f\xf3\x03\x37\x21\x35\ 174 | \x47\x76\x98\x0b\xf6\x77\x12\x8b\x23\xdd\xd0\x76\x4d\x48\x8f\x68\ 175 | \x4f\x06\x16\x1e\xf2\xbb\x1a\x10\x1c\xfb\x30\x72\x02\x8f\xf8\xa0\ 176 | \x8e\xe7\xe2\x43\xdc\x42\x9f\x8f\x0c\x32\xa6\x0c\xfe\x73\x5d\x1a\ 177 | \x95\xeb\x56\x70\x6b\x16\xf7\xe4\x60\x1a\x3e\x9a\x1e\x88\xd2\x41\ 178 | \x74\x65\xba\x23\xb5\xf2\x67\x39\xf1\x5b\x61\xd9\x8c\x03\x16\x15\ 179 | \x30\xc2\x7b\xd8\x21\xcd\x5b\x25\xc0\xda\x08\x6f\x79\xde\xc6\x54\ 180 | \x79\x82\xbb\xb7\x3c\xe3\x47\x79\xd2\x75\x94\x4d\xae\x06\x49\x3c\ 181 | \x68\x4a\xe4\xec\x86\xfa\x64\x35\x53\xf6\x08\xea\x86\xc8\x06\xc1\ 182 | \x42\x07\x88\x40\x40\xf4\x57\xb4\xae\x1e\xf8\x09\xb2\x2f\x4e\x9b\ 183 | \x94\xd0\xfd\x86\x24\xe1\x1e\xb8\x1f\xac\x05\x46\x08\x27\x86\x00\ 184 | \xce\x6c\x98\x11\x2c\x75\x88\xd3\xe7\x2c\x89\x57\x7f\x53\x3c\x1f\ 185 | \xe4\xd2\xb8\x39\x10\x3b\x41\x43\x03\x19\xe5\xb0\xb8\x2d\x12\xc1\ 186 | \xb9\x82\xad\xa4\x2b\x01\x0a\xaa\x3e\xf6\x4c\x32\xf1\x21\x9e\x37\ 187 | \x81\xd6\xd4\x40\x35\x64\x9a\x77\x2d\x2e\x1c\x1b\x33\x5f\x1b\xfb\ 188 | \xe1\x2a\x02\xc9\xe0\x80\x27\xca\xf7\x37\x04\xf3\xc9\x2f\x1d\x2f\ 189 | \x0c\x3c\xe7\xa1\x21\x1d\xc8\x96\x46\x05\x6c\x8b\xed\x87\x6b\x51\ 190 | \x2b\x68\x12\x43\x7d\x61\x4d\x1c\x9f\xe1\x5c\xe2\x87\x16\x91\xbe\ 191 | \xa1\xa4\x31\x83\x3b\x4b\x69\xb7\x3e\x7d\x5d\x31\x65\x2e\x7d\xa2\ 192 | \xf7\x9e\xd2\x9d\x81\xb9\x68\x33\x60\xd7\x50\x9a\xe9\x4e\xce\x83\ 193 | \xda\x54\x1d\x8d\xdf\x6b\xe8\x2f\xaf\x7e\xf9\x0c\x1e\x54\x86\x85\ 194 | \xb0\xdf\xba\xe4\x36\x04\xbb\x7d\xaa\xf5\xb0\x5c\xc5\xed\x8f\x2d\ 195 | \x1d\x18\xe8\x16\xb2\xe2\xb8\x39\x42\x8d\x62\x67\x6a\xa4\x1f\x89\ 196 | \x8b\x58\x83\x7a\x50\x5b\x72\xb4\xa8\xe0\x5b\x50\x86\xc2\xfd\xe7\ 197 | \xda\x13\x62\xec\xa2\x2d\x10\x08\xe3\xc2\xfe\xd5\xb3\x0d\x13\x24\ 198 | \x28\x69\xdd\xfc\xfd\x92\xd0\x51\x74\xcb\x6b\xbf\x66\x12\xca\x29\ 199 | \x56\xc7\xc0\x5b\x2c\x9e\xcd\x56\xbc\x57\x16\x24\xb8\x00\x36\x18\ 200 | \x3e\xc7\x4e\xd1\x21\xba\x1d\xfe\x8c\xb6\xc1\xbd\x9d\x3a\xa3\xd7\ 201 | \xf9\x1e\x10\x1e\x84\xcc\xa5\xfb\x6b\x96\xe7\x15\x8b\xb5\x09\x58\ 202 | \x9e\x2c\x9d\xbc\x7a\x36\xcc\x25\x41\x63\x82\x11\xe9\x8c\xbe\x63\ 203 | \x42\x08\x24\xe7\x32\x0e\x4a\xa5\x8b\xe6\xc1\x31\xb8\x35\x06\x46\ 204 | \xcb\x02\xd8\x2e\x7a\x22\x45\xb9\x00\x54\x09\xc7\xa3\x9d\x17\xd2\ 205 | \x4f\xbe\x2c\x2e\x53\xd5\x9c\x16\x91\xc7\x20\x25\xc5\xbe\xda\x81\ 206 | \x73\xa7\xc6\xc4\x6c\x98\xc8\x08\x8b\xe0\x0f\x09\x87\x98\x84\xb7\ 207 | \x7c\x21\xee\xa5\x6a\xe0\x00\xb2\x1a\x3f\xf1\x15\x68\x78\x1f\xd1\ 208 | \xf8\x3e\xa7\x49\xb3\x61\xe6\x86\xc4\xac\xd5\x7a\xe3\x7b\x14\xc0\ 209 | \xb0\xc8\x94\x4e\x05\x01\x2e\xcf\x59\x66\xa5\x7c\x0f\x9d\x43\x23\ 210 | \x68\x48\xe8\xda\xc1\xa8\xac\xab\x7a\x1c\x10\x31\xa4\xf6\x23\xd8\ 211 | \xeb\x3c\xd5\x60\x97\x65\x52\x94\xe7\x43\x0f\xd9\xb0\x10\xe5\x00\ 212 | \xaf\x5e\xfa\xe3\x3f\xc0\x86\x3a\x37\xbf\x2b\xbc\x12\x26\x39\x47\ 213 | \xd0\xd7\xda\xbc\xd7\x1e\xa6\xcc\x6e\x92\xe9\xf9\xfa\xb6\xcd\x82\ 214 | \x48\x8c\x24\x2e\x17\xe3\xd7\x58\xf1\x03\x24\x77\x20\x14\x74\x76\ 215 | \x74\x32\x4a\xc0\x13\x94\xb4\x3b\x16\xb7\x69\x7b\xf9\x5c\xcb\xa5\ 216 | \xa0\x7b\xf0\xda\x26\xfc\x32\xc4\x1d\xc1\x68\x88\x20\x04\x25\x6a\ 217 | \x1d\x78\xa3\x92\xde\x4d\x37\xd4\x14\x98\x0a\xc8\x58\xed\xe7\x21\ 218 | \x5d\xb3\xff\x6c\xaf\x4e\x09\x3a\xc8\x0a\x90\x59\x0e\x0e\x1b\xd5\ 219 | \x80\x26\x57\xc8\x9c\x63\x8e\xc2\x65\xb0\xde\xe1\xf6\x05\x4b\xca\ 220 | \x6f\x6f\xe2\xa1\x5d\x0f\x46\xb9\x96\xa2\x90\x8d\x07\x99\x41\x28\ 221 | \x1d\xc6\xd8\x2d\xc4\x5c\x37\x54\x93\x55\x75\x2e\x9f\xd6\xd2\x76\ 222 | \x20\x30\x9b\x5a\xa1\x8c\x9f\x50\x65\x7c\xf2\x51\x9b\xa0\x8f\x1c\ 223 | \x16\x09\x8a\x63\xb5\x24\x69\xb3\xa4\xd1\x9a\xe6\xef\xe9\x62\x83\ 224 | \x34\xc7\xe9\x2f\x59\x3e\x32\xef\x65\xf9\x8e\x66\x03\x89\x1c\x97\ 225 | \xe5\x05\x0a\x22\xa0\xed\x91\x2f\x14\xdc\x31\x8e\xf5\x6a\xb8\x24\ 226 | \x16\xa4\x77\x16\xda\xd6\xa0\x2a\xb6\x28\x1b\x6f\x67\x3b\xf5\x31\ 227 | \xc2\xba\x72\x18\x69\x1d\x86\x6f\x55\xe3\x1c\x31\x98\x59\x20\x48\ 228 | \x9c\x58\xee\x7c\x28\x3a\x14\x20\xd2\xc7\xd3\x54\x47\x67\x3b\x3b\ 229 | \xce\xf8\x60\x60\xa5\x89\x82\xa3\xc9\x02\x53\xc1\xd7\x55\xcc\x1b\ 230 | \xe2\x5a\xc2\x7f\x67\xc8\xa0\x14\x57\x19\x3a\xfa\x74\x5e\xb2\xab\ 231 | \x2b\x94\xab\x91\x0b\x03\x7d\x87\x0c\x48\x17\x8a\x6d\x38\xb1\x54\ 232 | \x40\xca\x57\x99\xff\x13\xe0\xfd\x52\x90\x79\x80\x5d\x84\xad\xc8\ 233 | \x11\x45\x29\x40\x12\x23\x99\xc2\xcf\x88\x43\xfc\xad\xd7\x2e\xf2\ 234 | \xd4\x1b\x40\xc9\xf0\x2a\xf8\x08\x0a\x1a\xbc\x0f\xb4\x42\xc9\x44\ 235 | \x84\x6a\x9e\x10\xd5\x23\xc9\x10\xaa\x20\x47\x2e\xe4\x80\x46\xa0\ 236 | \x86\xf2\x94\xe5\x03\x3e\xea\xb1\x5f\xa0\x43\xee\x7c\xfa\x40\x7b\ 237 | \x33\x38\xc3\xd7\xa8\x3a\xaf\x4d\x39\xd2\xe7\x2b\x64\x3a\x1c\x9e\ 238 | \xdd\x6a\x2f\xd6\xc7\xa7\x57\x9c\x71\x72\x95\xfa\xd6\xc2\xb5\x97\ 239 | \x5c\x7b\xb6\x19\xfd\x8e\xe3\x2d\xc3\x55\x62\x4a\xf9\x1b\x7c\x2d\ 240 | \xec\x4f\xff\x92\x49\x54\x20\xaa\x58\xbb\x1b\x36\x60\x81\x19\x2e\ 241 | \xdf\xbd\x95\x9e\xb4\xbb\x9d\xa3\x3f\x6d\x7f\x9b\x5e\x3a\x65\x8d\ 242 | \x4b\xf0\x74\xbd\xb9\x6d\x0f\x4c\x3d\x97\xd5\x7c\x95\x7e\xcc\x20\ 243 | \x0a\x75\x8e\xaa\x00\xc9\xa0\x1e\x8a\x0e\x03\x89\x15\x4b\xda\xf3\ 244 | \x16\xf0\xda\x07\x1f\xe2\x83\x76\xf2\x4e\xf6\x4c\xaf\xcd\xd5\x93\ 245 | \xaf\xfc\x91\x49\x12\xe6\xd4\x53\x3b\x10\x8a\xff\x20\x61\xe7\xd5\ 246 | \xfc\xfc\x46\x5c\xd0\xa7\x45\x4d\xa0\xc9\x8a\x4e\x8b\xf0\x0e\x49\ 247 | \xe6\x3f\x55\xed\xbe\x55\x4a\x08\x16\x8f\x0b\x25\x8e\xd8\x28\x8a\ 248 | \x5f\x91\x8d\x77\x58\x88\xb0\xc1\x9a\xde\xf2\xd3\x4b\x24\xd9\x29\ 249 | \x2d\x3a\x52\x0a\x03\xc3\x06\xba\x4a\x7b\x01\xc4\xb5\x2f\x5f\x6e\ 250 | \x85\xd7\x78\xb6\x50\x1f\xf9\xb0\x2b\xd6\x41\xdb\x6c\x30\x01\x19\ 251 | \xa1\x28\xde\xa0\xea\x51\x4d\x20\x78\x90\x81\x04\xd2\x25\x1f\x6d\ 252 | \x7f\x4a\xc2\x51\x2a\x45\x3d\xb9\xe1\x01\x08\x54\x44\x49\x87\x96\ 253 | \xd1\x54\xc9\x96\xb1\x7c\x35\x3a\x0d\xc0\xf6\xef\xe1\xd4\x56\xd8\ 254 | \xd1\xf9\x1c\xa5\x9a\x5a\xd2\xc6\x14\x9d\x83\xfc\x28\x7c\x31\xa2\ 255 | \xeb\x93\x8d\x34\xc8\xa6\xdc\x74\x6e\x91\xc0\x94\xd1\x74\xc0\x01\ 256 | \x75\x1b\x97\x71\x1b\x04\x07\xa1\x8b\x47\xc3\x42\x82\x7c\xc8\x1a\ 257 | \x59\x48\x9d\x72\x44\xb4\x01\x24\xd1\xa3\xfc\x20\x65\x83\xdc\xcc\ 258 | \xd5\xe9\xfc\x2a\x93\xb3\x06\x7c\x01\xa7\xd4\x59\xc6\x66\xe1\x88\ 259 | \x9d\x76\x01\xa5\x0e\x28\x6e\xdc\x5d\xe4\xe5\xec\x26\x39\xf0\xfd\ 260 | \xa8\xab\x78\xa5\xb9\xd5\x76\x0d\x7a\x8c\x8c\xcc\x8c\x06\xc0\xf9\ 261 | \x4a\xf4\x6e\xb0\xcd\x8c\x59\xa4\xa8\xca\x71\x46\x75\x58\x68\xf6\ 262 | \xd1\x48\x15\x98\xd1\x49\x59\x55\x1e\x24\x68\x60\xc3\x28\x7f\x0a\ 263 | \x5f\x4b\xe0\xd1\x54\x2b\x5c\xbf\xac\x2d\x63\x6f\xe8\x8a\xa3\xc3\ 264 | \x8b\x96\xed\x59\x50\xef\x8b\x6f\x4d\x5a\x43\xe1\x04\x30\xac\x68\ 265 | \x07\xcb\x02\x45\xc6\xd6\x2d\x2c\x89\x01\x85\xfd\xdd\xb6\x59\x9b\ 266 | \x7e\x70\x14\xc5\xc1\xd2\x12\x7a\xaa\xff\x15\xbe\x3c\x8a\x2c\x38\ 267 | \x44\x65\x18\x94\x93\x60\x20\x49\xa7\xa6\x38\x91\x42\x5c\x8a\xfa\ 268 | \x81\x45\xb4\xfd\x8c\x21\x2f\x2f\x3f\x42\x33\x01\x72\xb0\xa2\xc7\ 269 | \x92\x1c\xc9\xad\x00\xec\xa4\x89\x73\xbd\x28\x06\x00\x0c\xf7\x29\ 270 | \xbc\xf0\xc4\x00\x7b\x21\xd8\x98\xd3\xc7\xa5\x23\x97\x6a\xf9\x75\ 271 | \x73\xe0\xf7\xaf\xb0\xef\xc2\xf6\x59\x1d\x9e\x14\x54\x59\xa7\xd7\ 272 | \x50\xe4\xc1\xc1\x31\x07\x2b\x12\xfe\xd6\xa6\x45\x4e\xa7\xa8\x0b\ 273 | \x85\xec\x2e\x54\xe0\x98\x65\xb2\xf9\x9a\xca\x94\x9c\x42\x9f\xc7\ 274 | \x83\x5f\xb3\xb8\xad\xc1\x86\xff\x43\xf1\xab\xf6\x01\xf4\x8c\xeb\ 275 | \x0c\x4e\x31\x03\x2c\xe4\xf5\x13\x8b\x29\xe1\x6c\xf0\xae\x63\xfc\ 276 | \x0e\x15\xda\x7a\xa9\x30\xf6\x43\x85\xf9\x52\x21\xc2\x16\xcd\x0f\ 277 | \x35\xc1\x03\x01\xa0\x5b\x50\xa3\x8c\xaf\x9f\x2d\xe8\x54\x58\x9b\ 278 | \x3c\xad\x2c\xbe\x57\x4d\x22\x9b\xb1\x58\x8b\x3b\xd0\x8a\x33\x02\ 279 | \x10\x92\xf6\x23\xb9\x3e\xa4\x59\x6b\xd1\x4e\xf0\xd9\x0f\x0d\xd3\ 280 | \x87\xbb\xa9\x20\xc3\xd2\x28\xa5\x1e\x75\x7a\x52\x96\x76\xb4\xd0\ 281 | \x0a\x28\x2c\x81\xf9\xcc\xe1\xe0\x5d\x20\xf4\x3a\x1b\xa0\x29\xfe\ 282 | \x05\x04\x9b\x3f\x61\x30\x10\xfc\xa0\x9f\xb3\x8c\x28\x2c\xb8\xb3\ 283 | \xa7\xce\x9f\x48\xa1\x84\x7c\x77\xd7\x9e\x4a\x21\xe6\xf7\xcc\xe4\ 284 | \x68\xc8\x3c\xdc\x7b\x68\x12\x47\xa2\xb2\x33\x81\x68\x60\x0b\x56\ 285 | \x08\x63\xd7\x56\x87\x43\x45\xfd\x03\x14\xc5\x76\x1d\xed\x86\xb4\ 286 | \x99\xa0\x2c\x5d\xbb\x1f\x49\x95\x65\x10\x7d\x6b\xf5\x87\xac\x4c\ 287 | \x58\x8a\x5a\x5c\x74\x51\xbf\xf5\x12\x3b\x85\x9c\x6f\x84\x9e\x62\ 288 | \x57\xab\x77\x5f\xc0\x2a\xb9\x59\x96\x5c\x92\x71\xd6\x84\x3d\x31\ 289 | \xd6\x2d\x09\xb0\x48\x82\xc7\x18\x65\x3a\xc4\x52\x77\x51\x87\xc1\ 290 | \x42\x87\xce\xf7\xd1\xb0\x5e\x0f\x05\xd5\xe8\x71\x5c\xaf\x1c\xd5\ 291 | \x79\x66\x95\xbf\x41\x20\x54\xd7\x92\xa1\xf0\x7e\x2d\xfc\x1f\x0d\ 292 | \xf0\x85\xc0\x71\xcf\xf5\xbc\xef\x9f\xcf\x23\xcc\x76\xd4\x92\x4e\ 293 | \x28\x8c\x7b\x66\xf4\xc3\x67\x9d\xbf\xa4\x8e\xb7\xd1\xee\xc7\x90\ 294 | \xf1\xc8\xb4\x3a\x2c\x24\xd6\xd3\x86\x23\x94\x03\x41\x8b\x96\xc9\ 295 | \x83\x7b\xae\x18\xa3\x0d\x3d\x3a\xad\xe1\xd7\x44\xcf\x4d\x47\x54\ 296 | \x43\xb1\xb3\x60\x71\xc7\x2d\x3b\x57\x11\x90\xe0\x1c\x92\xa9\x4b\ 297 | \x3b\x81\xcb\x7d\xa0\xec\x07\xf8\x8c\x7e\xdc\x4d\xee\x56\xc7\x82\ 298 | \x45\x1e\xa3\x3c\xfd\x6c\x20\x54\x0f\x3c\x12\xb8\x1c\xfe\x86\xdd\ 299 | \x78\x90\xd9\xb9\x25\x91\x46\x62\x54\xfc\x1d\xf2\xad\x6e\xbe\x34\ 300 | \x86\x54\xed\x11\x0e\x64\x10\x1e\xb5\xf1\xee\x75\x68\x2b\x13\xad\ 301 | \x70\x3d\x47\x6f\xdf\xee\xfd\xfd\xd6\x09\x1b\xbe\xbb\xf1\x94\xed\ 302 | \x1e\xc3\x2f\x7a\x7d\xc9\x0d\x6a\x33\xf6\xd7\x99\xb3\xbc\xfb\x5d\ 303 | \x3d\xcc\xf1\xeb\xc4\xa5\xd8\xce\xdc\x75\x9c\xb9\xbe\xcd\xbd\xe4\ 304 | \x29\xa8\x9e\x72\xb2\xda\xfe\x0e\xe3\xcf\x0b\x2f\xdb\xd0\x92\x12\ 305 | \x19\xed\xee\xce\x50\xe2\x32\xab\xf3\x1e\xe5\x32\x06\x55\x07\x86\ 306 | \x1d\x79\x4a\xa2\x3c\x40\x00\x8b\xb1\xda\x7e\x8e\x40\xd6\xa3\x3d\ 307 | \x50\x6a\x6a\x46\x83\x43\xd2\xfe\x0c\xa9\x87\xb7\x67\xa5\xda\xa1\ 308 | \x55\xf8\xa7\xd5\x01\xe1\x01\x03\xa8\x1b\x60\x1a\x00\xaa\x32\xa0\ 309 | \xb1\x42\x8a\x41\x7b\x55\xb5\x04\x18\xbe\x7d\x30\xbc\x81\xe2\x79\ 310 | \xd5\xc9\xc9\x0a\x7a\xb2\x08\xc2\x1a\x4d\xd2\xdd\x49\xf6\x66\x8a\ 311 | \x3a\xef\x46\x3f\x6b\xe4\x87\x56\x42\x3f\x6c\x1a\xbf\x81\xff\x8f\ 312 | \x4f\xc0\xad\x2f\xce\x77\x34\xa4\x6e\xe7\x69\x73\x94\x9f\x68\x02\ 313 | \xd0\x60\xee\xc8\x06\x78\x45\xa7\x8f\x20\x69\x42\xf6\xe1\x12\xcf\ 314 | \xf6\x50\xd7\xe9\x04\x8b\x85\x64\xe1\xc8\x27\xa1\x09\xd2\x82\xc7\ 315 | \xf2\x30\xac\xb8\xc3\x67\x95\xb2\xf5\xb0\x06\x93\x22\x0e\x14\x83\ 316 | \x36\x93\xb9\xdc\x6b\xc7\x30\x00\x03\x0e\x83\x10\xa5\x8f\xa7\xd7\ 317 | \x46\x67\xb3\x60\xef\x57\xd7\x66\xae\x6d\xfb\xe2\xda\xd0\x64\x85\ 318 | \x76\x66\xf1\x18\xf2\x1e\xf8\x9d\x42\xd1\xf9\x45\xef\x4f\xc5\x6a\ 319 | \xce\x91\x82\x5d\x7c\x97\xdb\x01\x6a\x5b\x8f\xed\xd0\xaa\x06\xa7\ 320 | \x82\x57\xc0\xe4\xfb\x47\x8d\x6c\x85\x03\xf4\x28\xad\x71\x3b\x93\ 321 | \x7e\x2b\xe1\x4f\x3e\x1e\xfa\x87\x27\xe4\xe6\x1e\x03\x74\xb6\x1f\ 322 | \x32\xef\x3d\x39\x47\xc1\x3c\x77\x17\x79\x97\xd5\xe6\x3d\x0d\xb9\ 323 | \x3b\x8f\x9f\xc7\x25\xf7\x92\x8f\x0b\xb2\x39\x9f\xeb\xac\x84\x15\ 324 | \xd3\xb6\xac\x84\xdc\x22\x00\x31\xe7\x72\x1a\x48\x11\x4f\x3d\xea\ 325 | \x14\x00\x43\x35\xfb\x72\x3b\x56\x47\xb8\xd5\x10\xda\x00\x26\xd0\ 326 | \xf0\x37\x4d\xcb\x0f\x22\x7e\xab\x85\x40\x53\xcc\x3a\x22\x84\xbf\ 327 | \x6e\xff\x3e\xea\x16\xdc\x16\x8a\x11\x6a\x04\x14\xea\xa5\xc6\x2e\ 328 | \x95\x60\x29\xce\x63\xfe\xcd\x71\xff\xf5\x9e\x92\xad\x7c\x69\xf1\ 329 | \xdd\x00\x00\xf8\x41\x3b\x0a\x7d\x40\xc6\xcc\x53\x7b\x91\x13\xc9\ 330 | \x93\xd0\xe1\x28\x13\xf2\x39\x31\xe3\x16\x11\x06\xe2\x1a\xfc\x29\ 331 | \xfa\x81\xba\xde\x8a\x3e\x35\xd0\x55\x02\x09\xc2\x09\x49\x78\x7b\ 332 | \x0f\x2e\x5f\x97\xae\xed\x77\x65\xdb\xbe\x72\xfd\x6e\x7b\xea\x60\ 333 | \xbf\x1a\x96\x2b\x87\x5e\x71\xe8\x7c\x65\xac\xd8\x4b\xa6\x9e\x9c\ 334 | \x36\xca\x41\x92\x58\x64\x46\x4a\xd8\x5e\x9b\xa5\x7a\x0a\x60\xbc\ 335 | \x4f\x01\xe4\x2f\x4f\x01\x50\x6d\xc6\xdd\xe7\x1a\x14\xd6\x8f\x6d\ 336 | \x7a\x1d\x8d\x2e\x81\x49\x00\x5c\x72\x38\xfb\xf8\x6b\x44\x32\x45\ 337 | \x26\x98\x97\xb6\x63\x3f\x48\x4d\x94\x76\x02\x63\x0e\xa9\x09\x65\ 338 | \xee\x93\x00\x7a\x3e\xe4\x3e\xd4\x21\xd3\xb9\x2f\x8c\xc1\xcb\xc4\ 339 | \x4b\xfe\x16\xfa\xaf\xf4\x63\x72\x82\xf8\x93\xd7\x70\xe4\x46\x09\ 340 | \xe6\xce\x59\xbb\x8c\x67\xd6\x77\xce\x3f\x66\x7c\xf7\xdd\xbe\x4e\ 341 | \xf9\x73\xc2\x77\xba\x8c\x86\x02\xec\xd3\xe8\xb4\x20\x37\xf7\xd7\ 342 | \xd9\x58\xf4\x11\x7d\xab\x9d\x80\xae\x13\x5b\xc0\x06\x71\x9d\x5d\ 343 | \x9a\x43\xbd\x69\xa9\x8e\xc7\x2c\xd0\xa5\x81\x84\x3a\x5f\xff\xa0\ 344 | \x5f\xb2\xd9\xfc\x3f\x92\xef\x4b\xbd\x2f\xf1\xd2\xb4\x97\x7b\x19\ 345 | \xf8\x83\x7d\xff\x2b\xf2\x35\xbf\x61\xdf\xbf\x92\xef\xaf\x64\xac\ 346 | \xce\x1e\xd9\x28\x78\x9b\x62\x81\x1f\x8f\xc5\x0a\xb1\x63\x92\xd0\ 347 | \xd1\x60\x3f\x7a\x16\xdc\xa8\x09\x29\x03\xce\x7b\x04\x93\xcd\xf9\ 348 | \x09\x54\xdc\x39\xf8\x73\xe8\x35\x9f\x10\xde\x3a\x2e\x27\x46\xfa\ 349 | \x3e\x7a\x18\xe1\x0e\xf7\x74\x6d\xf5\xfb\xaa\x8d\xb3\x3f\x29\x4a\ 350 | \x62\x69\xb5\x33\x76\x6f\xee\x82\xb6\x66\xfc\x34\x38\x8f\x87\x46\ 351 | \x9c\xbd\x85\xe2\x62\x3c\xb7\x66\x89\xe2\x49\x20\x21\x3e\x59\x9b\ 352 | \xac\x2b\x23\x08\xba\x47\x15\x27\x6d\x4d\x7d\x5a\x88\x8c\x1d\x80\ 353 | \x49\x47\x5b\xc5\xd0\xc4\xf7\x48\x6f\x03\xb6\x68\x6e\x26\x8c\xf9\ 354 | \x8f\xbd\xe1\xed\x15\x73\xac\x13\xc6\x64\x74\x81\xe4\x14\xc6\xe9\ 355 | \xa1\x34\xf8\xbf\x5b\x9d\x7c\xc8\x15\x7a\x95\x9c\x33\x0b\x1c\x39\ 356 | \xe8\x00\xdd\x59\xf7\x4a\x27\x50\x0f\xe8\x6f\x34\x5f\x0a\x93\xa6\ 357 | \x45\xc0\x3a\xc8\x4a\xcf\x88\x60\x02\x1e\x49\xdd\x25\x06\x4c\xef\ 358 | \x39\x20\x81\x34\x58\x09\x7e\xa6\xf7\xb1\xd4\x61\xb1\xc6\x20\x69\ 359 | \x90\x6c\x1f\xa5\xcb\x39\xa5\xc7\xad\x27\x2d\xa7\x6a\x8e\x58\x27\ 360 | \xed\x25\x52\x26\xc2\x51\x24\x8e\x3d\x47\xca\xa7\xd7\x56\x88\x1f\ 361 | \x4f\x20\x61\xb8\x11\x34\x8c\x0b\x77\xb5\xe9\xfd\xa0\x53\x93\x4e\ 362 | \xbf\x25\xa1\xe6\xe1\x0f\x94\x52\x47\x29\x60\xd0\x08\x81\xe4\xf8\ 363 | \xb2\x18\x33\x7a\x60\xd1\xb4\xe7\x88\xa4\x7f\x7d\x86\xe7\xcb\xad\ 364 | \xee\x7d\x96\xb6\xc3\x51\x36\x81\x28\x7b\x80\xb2\x53\x05\xcc\x49\ 365 | \xcf\x06\x55\x29\x9c\x15\xa7\x9e\x18\xd7\xc3\x50\xdf\x26\xfa\x65\ 366 | \x9e\xf7\x18\x25\x84\x1c\xdc\x6d\x6e\x7b\xce\x6f\x7f\x3c\xec\x84\ 367 | \x41\xa4\xe4\xf4\x68\x15\x0a\x9b\x0f\xab\x9e\x12\xe2\x5a\x96\xd4\ 368 | \xa5\xee\xe4\x56\x3e\xbe\x7f\xbe\x2d\xb7\xd2\x3e\x9e\x14\xbb\xe7\ 369 | \x5b\xa5\xac\x8b\x61\x7e\xe9\x51\x86\x7a\x05\xd6\x01\x8f\x77\xfe\ 370 | \x0f\x0a\x67\x4a\x10\x3a\x1d\x70\x61\x30\x53\xd6\xc3\x30\xbf\xec\ 371 | \x87\xfe\x20\x56\xf3\x6f\x99\x75\xc1\xa3\x3a\xf7\x71\x08\xbe\x02\ 372 | \xd4\x7c\xf0\xaa\xf9\x37\xc4\x7a\x77\xc6\x6c\xb1\xa2\xd6\x1f\x12\ 373 | \x79\xb3\xf8\x62\x14\xcb\xa5\x1d\x71\xab\xc7\x24\xb1\x3d\x18\x41\ 374 | \x34\x20\xa4\xe6\x74\x1e\x1f\x62\x41\x46\x96\xfc\x79\x6e\xdc\xdd\ 375 | \xc7\xa9\xb1\xee\x37\xbb\x0e\xde\x2a\xee\x17\xa8\x45\x64\xbb\x44\ 376 | \xd7\xf8\x73\x6c\x7c\x56\x7f\x0e\x0a\xe9\x76\x49\x37\xbf\x99\x72\ 377 | \x3e\x27\x89\xfe\x1c\x12\x1e\xe9\xfa\x7e\x28\x43\x71\x3f\x36\xf7\ 378 | \xf3\x73\xd2\xa8\x27\x86\x49\x3b\x2d\xa3\xf9\xf4\xde\x76\x27\x52\ 379 | \x50\xfb\x63\x45\x4b\x28\x22\x7a\x64\xf4\x80\xef\x89\xda\x68\x43\ 380 | \xfa\x61\xf3\xaa\x36\x19\x6c\x33\xe8\xad\x27\x40\xad\x44\x2e\x20\ 381 | \xfd\x9a\x2c\x95\x5d\x82\x91\x2c\x79\x7f\x9e\x53\x90\x71\x6f\x9d\ 382 | \xd0\xee\x40\x14\x08\x73\x47\x35\x26\x6d\x96\x6e\x85\x5e\x3b\x40\ 383 | \xd1\xd0\xc7\xd0\x9d\x4b\x1f\x5b\xe3\x2d\x8d\xfc\x63\x6f\x7c\x0c\ 384 | \x2c\x56\x3d\xb6\x5a\x4f\x56\x08\xdf\xd1\x12\xfd\x34\xaa\xb6\x00\ 385 | \xb5\xf9\x6d\xcf\xe6\xb7\xc1\xd8\x83\x73\x43\x0f\x2e\xe1\x36\x12\ 386 | \xa2\x9d\xc1\x02\x64\x4b\x1f\xc7\xaa\x75\x94\x73\xe2\x3c\xb5\x11\ 387 | \xd3\xac\x05\x97\x1f\xe6\xfa\xf8\x7b\xc2\x30\x10\xa0\x50\xdb\x73\ 388 | \x0f\x7c\x47\x39\xfa\x41\xdb\x76\x5a\x86\xb6\x1c\x55\x2c\x20\xbb\ 389 | \xd4\x62\xc0\x9d\x03\xb2\xb0\x9c\x97\xc5\x2e\x52\x09\x5d\x6e\xfa\ 390 | \x19\x13\x7e\x9b\x98\xbf\x9a\xad\xc9\x14\x26\x39\xf5\x01\x9a\x20\ 391 | \xe2\xa5\x82\xff\x2a\x40\xa0\x31\xe6\x78\x76\x84\x96\x76\xb6\x60\ 392 | \x85\xaa\x53\x79\x4b\xb7\x9c\xc7\x26\xcf\x69\x59\xaa\xda\xe4\x39\ 393 | \x27\x35\xae\x3d\xe3\x3f\x3e\x26\xfc\x55\xfa\xbd\xfb\x7b\x9f\x4f\ 394 | \x44\xfd\x72\xb0\x68\xed\x01\x9c\x83\x00\xa8\xb0\x03\x39\x07\x71\ 395 | \xf4\x18\x93\xff\xaa\x16\x9c\x3d\xa8\x61\x3e\x60\x23\x7d\xc2\xdb\ 396 | \x8c\x44\x0b\x5b\x31\x4e\xd4\xea\x96\xf3\xe8\x5d\x9b\xb7\xc8\xfe\ 397 | \x39\x2e\xb0\x8d\xf7\x3e\x9f\x77\x79\x81\xcd\xff\xf4\x70\xe2\x7b\ 398 | \x93\x0b\x6c\xe9\xe3\xb9\xc4\x8f\xd1\xd5\x5d\xf5\x13\x7d\x40\x83\ 399 | \x83\x3f\x27\x6b\xde\xeb\x54\x21\xa7\x47\x0d\x19\xb9\xf1\xd6\xc3\ 400 | \xec\x00\x38\xd9\xbf\xa7\x1e\x41\x07\x36\x6e\x5a\x2d\x3b\xc1\x0d\ 401 | \x6b\xd0\x25\x8f\xb6\x77\xa6\x36\xbc\x6a\x48\xe6\x79\x83\xf5\xf3\ 402 | \x6e\xb6\xd7\x16\x8f\x9e\xbc\xa5\x5a\x90\x80\xf8\x61\x7b\x4f\x55\ 403 | \x0a\x23\x70\xd1\xaf\xdb\x41\xe6\xdd\x0f\x7a\x9f\x29\xc2\x36\xe1\ 404 | \xd8\xbc\x94\x98\xbf\x4f\x92\x4e\x96\x71\x61\x6f\xc5\xac\x73\x9b\ 405 | \xf3\x7c\x65\x11\x0b\x06\x00\xfb\x60\x85\xeb\xf5\x64\x6d\x61\x9a\ 406 | \x2c\x6a\x09\x03\x82\x7e\x20\xdc\x7a\xa0\x52\x96\x9a\x58\x68\xa3\ 407 | \x47\xff\x6f\x8e\xf4\xb1\x31\xf4\x2a\x60\x6b\x5f\x05\x9c\x8f\x02\ 408 | \x66\xa0\x14\x05\x2c\x51\x25\x1b\xcf\xb6\x90\x25\x18\x90\xb0\x0c\ 409 | \xef\xfb\xac\xf2\x40\x72\x8c\x7f\x2a\x29\xf3\xdf\xd6\xe2\x8f\x63\ 410 | \xc5\x73\xde\x82\x60\x8f\x91\x3a\x00\xdc\x1a\xb4\x88\x9c\xa1\x41\ 411 | \x91\x25\x83\xee\x49\xa2\xaf\x1e\xc0\x1a\x1b\x91\xdb\x01\x5c\x44\ 412 | \x82\xe9\x38\xc4\x82\x4d\xb0\x29\x45\x06\x6e\x5c\x81\xf8\xbb\x23\ 413 | \x9f\x1d\x70\x9b\x58\x3e\x35\x6e\x24\x72\x88\xed\x7b\x08\x24\x5d\ 414 | \xb2\xcf\xe9\xea\x3c\x47\xd3\x3f\x25\xc7\xfc\x2e\x3b\xd4\xd2\xc1\ 415 | \xca\x5e\xf5\x7f\x9c\x21\x76\x03\x45\x8b\x68\x3a\x06\x87\xf6\x00\ 416 | \x15\x1f\xf3\x7f\x0f\xdf\x71\x6d\x51\xed\x3d\xc8\x00\x00\x01\x84\ 417 | \x69\x43\x43\x50\x49\x43\x43\x20\x70\x72\x6f\x66\x69\x6c\x65\x00\ 418 | \x00\x28\x91\x7d\x91\x3d\x48\xc3\x40\x1c\xc5\x5f\x53\x45\x29\x15\ 419 | \x07\x3b\x88\x38\x64\xa8\x4e\x16\x44\x45\xd4\x49\xab\x50\x84\x0a\ 420 | \xa1\x56\x68\xd5\xc1\xe4\xd2\x2f\x68\xd2\x90\xa4\xb8\x38\x0a\xae\ 421 | \x05\x07\x3f\x16\xab\x0e\x2e\xce\xba\x3a\xb8\x0a\x82\xe0\x07\x88\ 422 | \x8b\xab\x93\xa2\x8b\x94\xf8\xbf\xa4\xd0\x22\xc6\x83\xe3\x7e\xbc\ 423 | \xbb\xf7\xb8\x7b\x07\x08\xf5\x32\xd3\xac\x8e\x51\x40\xd3\x6d\x33\ 424 | \x95\x88\x8b\x99\xec\xaa\xd8\xf5\x8a\x30\x42\x08\x62\x1a\x33\x32\ 425 | \xb3\x8c\x39\x49\x4a\xc2\x77\x7c\xdd\x23\xc0\xd7\xbb\x18\xcf\xf2\ 426 | \x3f\xf7\xe7\xe8\x51\x73\x16\x03\x02\x22\xf1\x2c\x33\x4c\x9b\x78\ 427 | \x83\x78\x72\xd3\x36\x38\xef\x13\x47\x58\x51\x56\x89\xcf\x89\x47\ 428 | \x4c\xba\x20\xf1\x23\xd7\x15\x8f\xdf\x38\x17\x5c\x16\x78\x66\xc4\ 429 | \x4c\xa7\xe6\x89\x23\xc4\x62\xa1\x8d\x95\x36\x66\x45\x53\x23\x9e\ 430 | \x20\x8e\xaa\x9a\x4e\xf9\x42\xc6\x63\x95\xf3\x16\x67\xad\x5c\x65\ 431 | \xcd\x7b\xf2\x17\x86\x73\xfa\xca\x32\xd7\x69\x0e\x22\x81\x45\x2c\ 432 | \x41\x82\x08\x05\x55\x94\x50\x86\x8d\x18\xad\x3a\x29\x16\x52\xb4\ 433 | \x1f\xf7\xf1\x0f\xb8\x7e\x89\x5c\x0a\xb9\x4a\x60\xe4\x58\x40\x05\ 434 | \x1a\x64\xd7\x0f\xfe\x07\xbf\xbb\xb5\xf2\xe3\x63\x5e\x52\x38\x0e\ 435 | \x74\xbe\x38\xce\xc7\x10\xd0\xb5\x0b\x34\x6a\x8e\xf3\x7d\xec\x38\ 436 | \x8d\x13\x20\xf8\x0c\x5c\xe9\x2d\x7f\xa5\x0e\x4c\x7d\x92\x5e\x6b\ 437 | \x69\xd1\x23\xa0\x77\x1b\xb8\xb8\x6e\x69\xca\x1e\x70\xb9\x03\xf4\ 438 | \x3f\x19\xb2\x29\xbb\x52\x90\xa6\x90\xcf\x03\xef\x67\xf4\x4d\x59\ 439 | \xa0\xef\x16\x08\xad\x79\xbd\x35\xf7\x71\xfa\x00\xa4\xa9\xab\xe4\ 440 | \x0d\x70\x70\x08\x0c\x17\x28\x7b\xdd\xe7\xdd\xdd\xed\xbd\xfd\x7b\ 441 | \xa6\xd9\xdf\x0f\xc5\x0e\x72\xc8\xe0\x28\xf0\x84\x00\x00\x00\x06\ 442 | \x62\x4b\x47\x44\x00\xee\x00\xd0\x00\x4a\x75\x27\x5f\x7f\x00\x00\ 443 | \x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\ 444 | \x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xe3\x0c\x0a\x03\ 445 | \x3a\x24\xed\xa5\x4a\x18\x00\x00\x01\x98\x49\x44\x41\x54\x48\xc7\ 446 | \xad\x93\x31\x4f\xc2\x40\x18\x86\xdf\x3b\xaa\x83\x02\x56\xfe\x42\ 447 | \x9d\x4c\x44\x56\x13\x13\x31\xea\x22\x8b\xa3\x1b\x8b\x7f\xc0\x8d\ 448 | \x9f\x80\x93\xbf\xc1\x4d\x37\x16\x58\x8c\x06\x13\x13\x56\x81\xc4\ 449 | \x8d\xbf\x20\x95\x8a\x89\xd2\xde\xe7\x80\x57\xdb\x72\xbd\x36\xd4\ 450 | \x77\x6a\xbf\x6b\x9f\xef\xfd\xde\xbb\x63\x44\x74\xf8\x6d\x3f\x59\ 451 | \xee\xa4\x57\x17\xee\xfb\x3e\x32\x8a\x1b\x1b\xcf\x46\x71\xef\x66\ 452 | \xd5\x3c\x18\xb1\xaf\x71\xf7\x62\x36\xbe\xbf\x26\xf2\xf2\xc1\x8f\ 453 | \x0a\x56\x13\xce\xa8\x01\xef\xe8\x36\x11\x98\x7b\x38\x0f\xbd\x33\ 454 | \x96\xfb\x58\xd9\x3c\xb9\xe4\xee\xa4\x57\x8f\x82\x75\x3f\xaa\x14\ 455 | \x35\x40\xe4\xe5\xdd\x49\xaf\xce\x9c\x51\x83\xe2\x7e\x92\xee\x75\ 456 | \x22\x22\x88\xe3\x3b\x85\x11\xfe\x69\x24\xb9\x92\x0d\x88\x08\xc5\ 457 | \xad\xab\x85\xf5\xf8\xe6\x62\xcd\xf0\xa6\x43\xe4\xd6\x77\x94\xcb\ 458 | \xce\xa8\x81\x82\xd5\x44\xc1\x6a\x2a\x41\xb2\xae\x8c\x6a\x3a\x84\ 459 | \x21\x1f\x00\x28\x9b\x24\xc5\xa2\x82\xfa\xc1\x44\x17\x82\x8b\x49\ 460 | \x92\x93\x49\xcd\x9c\x01\x84\x20\x08\x41\x8b\xf0\x65\x9b\x88\xd6\ 461 | \xf6\x1c\x76\xf6\x1a\xaa\x1b\x69\x46\x54\xc5\x35\x73\x06\x89\x4d\ 462 | \x17\xe0\x72\xa4\x50\x2d\x06\x54\xaa\x74\xf4\xb7\x35\xeb\x75\x7f\ 463 | \x7b\x39\x55\x47\x25\xe8\xcf\xb9\xca\xb1\x4e\xa5\x4a\xc7\x07\x47\ 464 | \xb3\xce\xe4\x3c\x0e\x2c\x37\x36\x94\x79\x5a\xd7\x32\xe3\x34\x60\ 465 | \xed\x69\x89\xdb\xac\xb4\xe0\x58\x78\x70\x6c\x65\x96\x29\xc0\x3e\ 466 | \x9c\x73\x06\x21\x48\x3b\xb6\xee\xf2\x24\x9e\xf3\xa8\xdb\xac\x60\ 467 | \xce\xd9\x1c\x6e\x96\xdb\x4b\x8d\xad\x03\xfb\xce\xed\x41\xcd\x3f\ 468 | \xeb\x59\xc1\x01\x4d\x38\x80\x96\xce\x31\xe7\xcc\x77\x12\x74\xa6\ 469 | \xab\xff\xea\x91\xd9\x83\xda\x2e\x80\x2e\x00\x13\xff\x27\x1b\x40\ 470 | \x95\x9b\xe5\x76\x1f\x40\x35\x38\x41\x46\xb5\x00\x54\xcd\x72\xbb\ 471 | \xff\x03\xcd\x08\xd3\x25\xa2\x07\xc5\x7b\x00\x00\x00\x00\x49\x45\ 472 | \x4e\x44\xae\x42\x60\x82\ 473 | " 474 | 475 | qt_resource_name = b"\ 476 | \x00\x07\ 477 | \x07\x3b\xe0\xb3\ 478 | \x00\x70\ 479 | \x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ 480 | \x00\x0d\ 481 | \x07\xad\x20\xa2\ 482 | \x00\x72\ 483 | \x00\x61\x00\x73\x00\x74\x00\x65\x00\x72\x00\x5f\x00\x74\x00\x72\x00\x61\x00\x63\x00\x65\x00\x72\ 484 | \x00\x08\ 485 | \x0a\x61\x5a\xa7\ 486 | \x00\x69\ 487 | \x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ 488 | " 489 | 490 | qt_resource_struct_v1 = b"\ 491 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 492 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 493 | \x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ 494 | \x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 495 | " 496 | 497 | qt_resource_struct_v2 = b"\ 498 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 499 | \x00\x00\x00\x00\x00\x00\x00\x00\ 500 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 501 | \x00\x00\x00\x00\x00\x00\x00\x00\ 502 | \x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ 503 | \x00\x00\x00\x00\x00\x00\x00\x00\ 504 | \x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 505 | \x00\x00\x01\x6e\xed\xf5\x73\x4c\ 506 | " 507 | 508 | qt_version = QtCore.qVersion().split('.') 509 | if qt_version < ['5', '8', '0']: 510 | rcc_version = 1 511 | qt_resource_struct = qt_resource_struct_v1 512 | else: 513 | rcc_version = 2 514 | qt_resource_struct = qt_resource_struct_v2 515 | 516 | def qInitResources(): 517 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 518 | 519 | def qCleanupResources(): 520 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 521 | 522 | qInitResources() 523 | --------------------------------------------------------------------------------