├── .gitignore ├── test ├── tenbytenraster.keywords ├── __init__.py ├── tenbytenraster.prj ├── tenbytenraster.asc ├── tenbytenraster.asc.aux.xml ├── tenbytenraster.lic ├── test_resources.py ├── tenbytenraster.qml ├── test_feature_space_dialog.py ├── test_translations.py ├── utilities.py ├── test_init.py ├── test_qgis_environment.py └── qgis_interface.py ├── icon.png ├── resources.qrc ├── scripts ├── compile-strings.sh ├── run-env-linux.sh └── update-strings.sh ├── i18n └── af.ts ├── README.md ├── README.txt ├── __init__.py ├── metadata.txt ├── feature_space_dialog.py ├── pb_tool.cfg ├── feature_space_dialog_base.ui ├── plugin_upload.py ├── resources.py ├── Makefile ├── plot.py ├── pylintrc └── feature_space.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | ideas 3 | -------------------------------------------------------------------------------- /test/tenbytenraster.keywords: -------------------------------------------------------------------------------- 1 | title: Tenbytenraster 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farhang74/feature-space/HEAD/icon.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## feature space QGIS plugin 2 | 3 | This plugin creates a scatter plot of two bands of a raster (or two raster files with the exact same shape and location) 4 | then you can select areas on the scatter plot and export it as raster or vector. 5 | 6 | This tool is present in ERDAS and ENVI but there was no such tool in QGIS. 7 | To install: Go to Plugins tab -> manage and install plugins then search for feature space. 8 | 9 | After the installation is complete a new tool will be added under Raster tab 10 | 11 | ![Screenshot from 2023-06-30 21-16-30](https://github.com/farhang74/feature-space/assets/76580757/0791428f-af82-4639-b902-907df0928ceb) 12 | 13 | 14 | ## simple tutorial 15 | 16 | https://github.com/farhang74/feature-space/assets/76580757/2aa2b26d-43f3-4e0f-999f-50ee136d8788 17 | 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Plugin Builder Results 2 | 3 | Your plugin FeatureSpace was created in: 4 | /home/farhang/myfolder/linkedin_posts/test/feature_space 5 | 6 | Your QGIS plugin directory is located at: 7 | /home/farhang/.local/share/QGIS/QGIS3/profiles/default/python/plugins 8 | 9 | What's Next: 10 | 11 | * Copy the entire directory containing your new plugin to the QGIS plugin 12 | directory 13 | 14 | * Compile the resources file using pyrcc5 15 | 16 | * Run the tests (``make test``) 17 | 18 | * Test the plugin by enabling it in the QGIS plugin manager 19 | 20 | * Customize it by editing the implementation file: ``feature_space.py`` 21 | 22 | * Create your own custom icon, replacing the default icon.png 23 | 24 | * Modify your user interface by opening FeatureSpace_dialog_base.ui in Qt Designer 25 | 26 | * You can use the Makefile to compile your Ui and resource files when 27 | you make changes. This requires GNU make (gmake) 28 | 29 | For more information, see the PyQGIS Developer Cookbook at: 30 | http://www.qgis.org/pyqgis-cookbook/index.html 31 | 32 | (C) 2011-2018 GeoApt LLC - geoapt.com 33 | -------------------------------------------------------------------------------- /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__ = 'fz' 12 | __date__ = '2023-06-05' 13 | __copyright__ = 'Copyright 2023, fz' 14 | 15 | import unittest 16 | 17 | from qgis.PyQt.QtGui import QIcon 18 | 19 | 20 | 21 | class FeatureSpaceDialogTest(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/FeatureSpace/icon.png' 35 | icon = QIcon(path) 36 | self.assertFalse(icon.isNull()) 37 | 38 | if __name__ == "__main__": 39 | suite = unittest.makeSuite(FeatureSpaceResourcesTest) 40 | runner = unittest.TextTestRunner(verbosity=2) 41 | runner.run(suite) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /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 | FeatureSpace 5 | A QGIS plugin 6 | test 7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 8 | ------------------- 9 | begin : 2023-06-05 10 | copyright : (C) 2023 by fz 11 | email : fz 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 FeatureSpace class from file FeatureSpace. 30 | 31 | :param iface: A QGIS interface instance. 32 | :type iface: QgsInterface 33 | """ 34 | # 35 | from .feature_space import FeatureSpace 36 | return FeatureSpace(iface) 37 | -------------------------------------------------------------------------------- /test/test_feature_space_dialog.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Dialog 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__ = 'fz' 12 | __date__ = '2023-06-05' 13 | __copyright__ = 'Copyright 2023, fz' 14 | 15 | import unittest 16 | 17 | from qgis.PyQt.QtGui import QDialogButtonBox, QDialog 18 | 19 | from feature_space_dialog import FeatureSpaceDialog 20 | 21 | from utilities import get_qgis_app 22 | QGIS_APP = get_qgis_app() 23 | 24 | 25 | class FeatureSpaceDialogTest(unittest.TestCase): 26 | """Test dialog works.""" 27 | 28 | def setUp(self): 29 | """Runs before each test.""" 30 | self.dialog = FeatureSpaceDialog(None) 31 | 32 | def tearDown(self): 33 | """Runs after each test.""" 34 | self.dialog = None 35 | 36 | def test_dialog_ok(self): 37 | """Test we can click OK.""" 38 | 39 | button = self.dialog.button_box.button(QDialogButtonBox.Ok) 40 | button.click() 41 | result = self.dialog.result() 42 | self.assertEqual(result, QDialog.Accepted) 43 | 44 | def test_dialog_cancel(self): 45 | """Test we can click cancel.""" 46 | button = self.dialog.button_box.button(QDialogButtonBox.Cancel) 47 | button.click() 48 | result = self.dialog.result() 49 | self.assertEqual(result, QDialog.Rejected) 50 | 51 | if __name__ == "__main__": 52 | suite = unittest.makeSuite(FeatureSpaceDialogTest) 53 | runner = unittest.TextTestRunner(verbosity=2) 54 | runner.run(suite) 55 | 56 | -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | ; the next section is mandatory 2 | 3 | [general] 4 | name=feature_space 5 | email=zarefarhang@gmail.com 6 | author=Farhang Zare 7 | qgisMinimumVersion=3.0 8 | description=A plugin to plot feature space and export areas as raster or vector 9 | about=A plugin to plot feature space and export areas as raster or vector 10 | version=version 0.21 11 | tracker=https://github.com/farhang74/feature-space/issues 12 | repository=https://github.com/farhang74/feature-space 13 | ; end of mandatory metadata 14 | 15 | ; start of optional metadata 16 | category=Raster 17 | changelog=The changelog lists the plugin versions 18 | and their changes as in the example below: 19 | 0.21 - Remove nan values from raster 20 | 0.2 - Remove dependencies 21 | 0.1 - First stable release 22 | 23 | ; Tags are in comma separated value format, spaces are allowed within the 24 | ; tag name. 25 | ; Tags should be in English language. Please also check for existing tags and 26 | ; synonyms before creating a new one. 27 | tags=feature space,feature, space, plot, density 28 | 29 | ; these metadata can be empty, they will eventually become mandatory. 30 | homepage=https://github.com/farhang74/feature-space 31 | icon=icon.png 32 | 33 | ; experimental flag (applies to the single version) 34 | experimental=False 35 | 36 | ; deprecated flag (applies to the whole plugin and not only to the uploaded version) 37 | deprecated=False 38 | 39 | ; if empty, it will be automatically set to major version + .99 40 | qgisMaximumVersion=3.99 41 | 42 | ; Since QGIS 3.8, a comma separated list of plugins to be installed 43 | ; (or upgraded) can be specified. 44 | ; The example below will try to install (or upgrade) "MyOtherPlugin" version 1.12 45 | ; and any version of "YetAnotherPlugin". 46 | ; Both "MyOtherPlugin" and "YetAnotherPlugin" names come from their own metadata's 47 | ; name field -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /feature_space_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | FeatureSpaceDialog 5 | A QGIS plugin 6 | test 7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 8 | ------------------- 9 | begin : 2023-06-05 10 | git sha : $Format:%H$ 11 | copyright : (C) 2023 by fz 12 | email : fz 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 uic 28 | from qgis.PyQt import QtWidgets 29 | 30 | # This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer 31 | FORM_CLASS, _ = uic.loadUiType(os.path.join( 32 | os.path.dirname(__file__), 'feature_space_dialog_base.ui')) 33 | 34 | 35 | class FeatureSpaceDialog(QtWidgets.QDialog, FORM_CLASS): 36 | def __init__(self, parent=None): 37 | """Constructor.""" 38 | super(FeatureSpaceDialog, self).__init__(parent) 39 | # Set up the user interface from Designer through FORM_CLASS. 40 | # After self.setupUi() you can access any designer object by doing 41 | # self., and you can use autoconnect slots - see 42 | # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html 43 | # #widgets-and-dialogs-with-auto-connect 44 | self.setupUi(self) 45 | -------------------------------------------------------------------------------- /pb_tool.cfg: -------------------------------------------------------------------------------- 1 | #/*************************************************************************** 2 | # FeatureSpace 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 : 2023-06-05 8 | # copyright : (C) 2023 by fz 9 | # email : fz 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: feature_space 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 feature_space.py feature_space_dialog.py 52 | 53 | # The main dialog file that is loaded (not compiled) 54 | main_dialog: feature_space_dialog_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 | -------------------------------------------------------------------------------- /feature_space_dialog_base.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FeatureSpaceDialogBase 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1373 10 | 898 11 | 12 | 13 | 14 | feature space 15 | 16 | 17 | 18 | 19 | 50 20 | 270 21 | 651 22 | 501 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 1160 31 | 120 32 | 111 33 | 41 34 | 35 | 36 | 37 | Plot 38 | 39 | 40 | 41 | 42 | 43 | 730 44 | 270 45 | 621 46 | 501 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 100 55 | 30 56 | 211 57 | 20 58 | 59 | 60 | 61 | Select bands for scatter plot: 62 | 63 | 64 | 65 | 66 | 67 | 710 68 | 30 69 | 211 70 | 20 71 | 72 | 73 | 74 | Select bands for rgb image: 75 | 76 | 77 | 78 | 79 | 80 | 510 81 | 830 82 | 141 83 | 25 84 | 85 | 86 | 87 | Add as raster 88 | 89 | 90 | 91 | 92 | 93 | 710 94 | 830 95 | 141 96 | 25 97 | 98 | 99 | 100 | Add as vector 101 | 102 | 103 | 104 | 105 | 106 | 80 107 | 820 108 | 91 109 | 20 110 | 111 | 112 | 113 | Layer name: 114 | 115 | 116 | 117 | 118 | 119 | 200 120 | 820 121 | 201 122 | 31 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.15.3) 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\x04\x0a\ 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\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ 17 | \x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ 18 | \x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\ 19 | \x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xd9\x02\x15\ 20 | \x16\x11\x2c\x9d\x48\x83\xbb\x00\x00\x03\x8a\x49\x44\x41\x54\x48\ 21 | \xc7\xad\x95\x4b\x68\x5c\x55\x18\xc7\x7f\xe7\xdc\x7b\x67\xe6\xce\ 22 | \x4c\x66\x26\x49\xd3\x24\x26\xa6\xc6\xf8\x40\x21\xa5\x04\xb3\x28\ 23 | \xda\x98\x20\xa5\x0b\xad\x55\xa8\x2b\xc5\x50\x1f\xa0\x6e\x34\x2b\ 24 | \x45\x30\x14\x02\xba\x52\x69\x15\x17\x66\x63\x45\x97\x95\xa0\xad\ 25 | \x0b\xfb\xc0\x06\x25\xb6\x71\x61\x12\x41\x50\xdb\x2a\x21\xd1\xe2\ 26 | \x24\xf3\x9e\xc9\xcc\xbd\xe7\x1c\x17\x35\x43\x1e\x33\x21\xb6\xfd\ 27 | \x56\x87\xf3\x9d\xfb\xfb\x1e\xf7\xff\x9d\x23\x8c\x31\x43\x95\xf4\ 28 | \x85\x1e\x3f\x3b\x35\xac\xfd\xcc\x43\xdc\xa4\x49\x3b\xfe\x9d\x1d\ 29 | \xdb\x7b\x22\x90\x78\xf8\xb2\x28\xa7\xbe\x7d\xc1\x4b\x9d\x79\xdf\ 30 | \x18\x15\xe5\x16\x99\x10\x56\xde\x69\xdc\x3f\x22\xfd\xec\xd4\xf0\ 31 | \xad\x04\x03\x18\xa3\xa2\x7e\x76\x6a\x58\xde\x68\x2b\xb4\x36\xf8\ 32 | \xbe\xc6\x18\x53\xdb\xef\xe7\xfa\xec\xed\x67\x63\x10\x42\x00\xf0\ 33 | \xfb\xd5\x65\x2a\x15\x45\xc7\x6d\x0d\x00\xc4\xa2\xc1\xaa\x6f\x0d\ 34 | \x3e\x6c\xab\xc2\x1c\x56\xa4\x77\x4b\xb0\xf2\x35\x15\x5f\x21\x85\ 35 | \xe0\xc8\x6b\x5f\x92\x2d\x37\x33\x39\xf9\x03\x27\x8e\x1f\xa2\xf7\ 36 | \xbe\x9d\x04\x1c\x0b\x37\xe4\xac\xff\xa6\x30\x87\xbd\xba\x00\x6a\ 37 | \x06\x79\xe5\xf5\xaf\x89\xd9\x92\xc5\xcc\x0a\xd9\x7c\x19\xcf\xe9\ 38 | \xe2\xe4\xa9\x2f\x78\x7c\xff\x01\x72\x85\x0a\x2b\x65\x1f\xa5\x4c\ 39 | \xb5\xb2\x55\x16\x80\xbd\x31\xda\xda\x20\x1f\x7d\x3e\xcd\xc2\xfd\ 40 | \x59\xa6\x93\x39\x92\xd1\x22\xea\x9b\x16\xce\x9d\x3f\xce\xe0\x83\ 41 | \x03\x24\x82\x59\x3a\xdb\x7b\x88\xc7\x82\x68\x63\x58\xc9\xcc\x62\ 42 | \x8c\x21\x18\xb0\x6a\xc3\x37\x06\x49\x16\xff\x24\x6b\xa5\x49\xbb\ 43 | \x25\xbc\xa2\xa6\x21\xbb\x40\x7f\xdf\x00\x83\xbd\x01\x8e\x3c\xd5\ 44 | \x45\xd7\x8e\x6b\x9c\x9c\x98\x25\x1a\xb6\xe8\xbe\x3d\xc2\xdd\x77\ 45 | \x44\x48\xc4\x1c\x22\xe1\xeb\x58\x59\xaf\xcf\xd3\x33\x29\x2e\x34\ 46 | \x2d\x91\x93\x3e\xbe\x34\x78\x01\xc5\xe2\x61\xc5\xae\x72\x8e\x70\ 47 | \xc8\xc2\x0d\x5a\xbc\xf5\xee\x2f\x9c\xfa\x3e\x86\x69\x7a\x8e\xcf\ 48 | \x26\xe6\xf9\x63\xa1\x44\xa1\xa4\xd0\xda\x6c\x0d\x2f\x15\x7c\xb4\ 49 | \x67\x28\x59\x0a\xcf\xd6\x54\xe2\x06\x13\x87\x2b\x6f\x68\xa6\x27\ 50 | \xaf\x31\x32\x36\xc7\xb2\x7f\x17\xef\x7d\x7c\x8c\x33\x67\xcf\x12\ 51 | \x70\x24\x4a\x69\xd6\x6a\x46\xd6\xd3\x70\x72\xa9\x82\x67\x34\x45\ 52 | \xad\x28\xdb\x1a\x15\x34\x98\xff\x46\xed\xef\x37\x0d\x99\xbf\x4a\ 53 | \x3c\x30\x38\xc0\xc8\x4b\xaf\x92\x5a\x9c\xe2\xe0\x23\x6d\x74\xb4\ 54 | \xba\x84\x5d\x0b\x29\x45\x7d\xb8\x94\x82\x96\xb6\x10\xf3\xc5\x12\ 55 | \x2a\xef\x53\x11\x1a\x63\xad\x3f\x93\x19\x85\xf1\xb1\x77\x58\x5a\ 56 | \xf8\x99\x97\x9f\xe9\xa6\x75\x47\x90\xc6\xb8\x43\xd8\xb5\xb6\xce\ 57 | \xfc\xfa\xfd\x00\xfb\x3e\xf4\xc8\x05\x35\xba\x5e\xeb\x46\x21\xf9\ 58 | \xcf\x0a\xa9\x8c\x87\xe3\x48\xdc\x90\xb5\x6e\x98\x6a\xaa\x65\xf2\ 59 | \x52\x92\x43\x2f\x5e\xc2\x8c\x02\x1a\x10\xf5\x07\xac\xc3\x75\x70\ 60 | \x83\x92\x80\xb3\xf9\xd0\x26\xf8\x8f\xb3\x29\xc6\x3e\xb8\x8c\x19\ 61 | \x35\x75\x6b\x7b\x7e\x3c\xca\x45\x0c\x7e\x49\x31\xf4\x58\x3b\xf7\ 62 | \xf6\x34\x90\x88\x39\x04\x1c\x59\x1f\xfe\xdb\xd5\x3c\x5f\x9d\x4b\ 63 | \x32\xfd\x44\xb2\xba\xd7\xfa\xb6\x60\xcf\xde\x16\xdc\x90\x45\x4c\ 64 | \x4a\x2a\x9e\x62\xfe\x4e\xc5\xc8\xc1\x4e\xda\x76\x86\xe8\xe9\x0a\ 65 | \xe3\xd8\x92\x58\xd4\xc6\xb2\x44\x6d\x78\x2a\x53\xe1\xca\x7c\x99\ 66 | \x63\x5d\xbf\x56\x9d\xbd\x9f\x44\x18\x7a\xba\x95\x27\x0f\xb4\xd3\ 67 | \xdc\x18\xc0\xf3\x0d\x52\x40\xd8\xb5\xb0\xa4\x20\x14\xb2\x70\x6c\ 68 | \x81\x63\xcb\xaa\x42\xd6\xfd\xb7\xf4\xec\xa3\x06\xa0\x50\x52\xd8\ 69 | \x4e\x1b\x7e\x4a\xd3\x31\xf9\x29\xcf\xfe\xd4\x49\x7f\x5f\x13\xfb\ 70 | \xfa\x9b\x71\x43\x92\x58\xd4\x21\x18\x90\xac\xde\xb0\x42\x50\x13\ 71 | \x58\x33\xf3\x88\x6b\xa1\xfd\x65\x96\xf2\x79\xc6\x43\x7b\xd8\x75\ 72 | \x38\xcc\x3d\xdd\xd1\xaa\xcf\x71\xe4\xff\x7f\x91\x56\x33\xaf\xea\ 73 | \x37\xe7\xa1\x94\x21\x16\xb5\xd1\x06\x2c\x29\x36\xf5\x72\x9b\x96\ 74 | \x95\xc0\xc4\xda\x9d\x78\x83\x43\x53\x22\x80\x65\x09\x1c\xfb\x86\ 75 | \xc1\x00\xe7\x25\x70\x14\x48\x6f\x1e\x22\x51\xe3\x75\xd9\xb6\xa5\ 76 | \x81\xa3\x32\xb1\xfb\xf4\x0c\x30\xb8\xb1\x82\x9b\xb0\x09\x60\x30\ 77 | \xb1\xfb\xf4\xcc\xbf\xa0\xe9\x6e\xae\x5a\xdf\x4b\x81\x00\x00\x00\ 78 | \x00\x49\x45\x4e\x44\xae\x42\x60\x82\ 79 | " 80 | 81 | qt_resource_name = b"\ 82 | \x00\x07\ 83 | \x07\x3b\xe0\xb3\ 84 | \x00\x70\ 85 | \x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ 86 | \x00\x0d\ 87 | \x02\x1b\x1f\x35\ 88 | \x00\x66\ 89 | \x00\x65\x00\x61\x00\x74\x00\x75\x00\x72\x00\x65\x00\x5f\x00\x73\x00\x70\x00\x61\x00\x63\x00\x65\ 90 | \x00\x08\ 91 | \x0a\x61\x5a\xa7\ 92 | \x00\x69\ 93 | \x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ 94 | " 95 | 96 | qt_resource_struct_v1 = b"\ 97 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 98 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 99 | \x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ 100 | \x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 101 | " 102 | 103 | qt_resource_struct_v2 = b"\ 104 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 105 | \x00\x00\x00\x00\x00\x00\x00\x00\ 106 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 107 | \x00\x00\x00\x00\x00\x00\x00\x00\ 108 | \x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ 109 | \x00\x00\x00\x00\x00\x00\x00\x00\ 110 | \x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 111 | \x00\x00\x01\x88\x8c\x30\x1a\xe0\ 112 | " 113 | 114 | qt_version = [int(v) for v in QtCore.qVersion().split('.')] 115 | if qt_version < [5, 8, 0]: 116 | rcc_version = 1 117 | qt_resource_struct = qt_resource_struct_v1 118 | else: 119 | rcc_version = 2 120 | qt_resource_struct = qt_resource_struct_v2 121 | 122 | def qInitResources(): 123 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 124 | 125 | def qCleanupResources(): 126 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 127 | 128 | qInitResources() 129 | -------------------------------------------------------------------------------- /test/qgis_interface.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """QGIS plugin implementation. 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 | .. note:: This source code was copied from the 'postgis viewer' application 10 | with original authors: 11 | Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk 12 | Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org 13 | Copyright (c) 2014 Tim Sutton, tim@linfiniti.com 14 | 15 | """ 16 | 17 | __author__ = 'tim@linfiniti.com' 18 | __revision__ = '$Format:%H$' 19 | __date__ = '10/01/2011' 20 | __copyright__ = ( 21 | 'Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and ' 22 | 'Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org' 23 | 'Copyright (c) 2014 Tim Sutton, tim@linfiniti.com' 24 | ) 25 | 26 | import logging 27 | from qgis.PyQt.QtCore import QObject, pyqtSlot, pyqtSignal 28 | from qgis.core import QgsMapLayerRegistry 29 | from qgis.gui import QgsMapCanvasLayer 30 | LOGGER = logging.getLogger('QGIS') 31 | 32 | 33 | #noinspection PyMethodMayBeStatic,PyPep8Naming 34 | class QgisInterface(QObject): 35 | """Class to expose QGIS objects and functions to plugins. 36 | 37 | This class is here for enabling us to run unit tests only, 38 | so most methods are simply stubs. 39 | """ 40 | currentLayerChanged = pyqtSignal(QgsMapCanvasLayer) 41 | 42 | def __init__(self, canvas): 43 | """Constructor 44 | :param canvas: 45 | """ 46 | QObject.__init__(self) 47 | self.canvas = canvas 48 | # Set up slots so we can mimic the behaviour of QGIS when layers 49 | # are added. 50 | LOGGER.debug('Initialising canvas...') 51 | # noinspection PyArgumentList 52 | QgsMapLayerRegistry.instance().layersAdded.connect(self.addLayers) 53 | # noinspection PyArgumentList 54 | QgsMapLayerRegistry.instance().layerWasAdded.connect(self.addLayer) 55 | # noinspection PyArgumentList 56 | QgsMapLayerRegistry.instance().removeAll.connect(self.removeAllLayers) 57 | 58 | # For processing module 59 | self.destCrs = None 60 | 61 | @pyqtSlot('QStringList') 62 | def addLayers(self, layers): 63 | """Handle layers being added to the registry so they show up in canvas. 64 | 65 | :param layers: list 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #/*************************************************************************** 2 | # FeatureSpace 3 | # 4 | # test 5 | # ------------------- 6 | # begin : 2023-06-05 7 | # git sha : $Format:%H$ 8 | # copyright : (C) 2023 by fz 9 | # email : fz 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 | feature_space.py feature_space_dialog.py 42 | 43 | PLUGINNAME = feature_space 44 | 45 | PY_FILES = \ 46 | __init__.py \ 47 | feature_space.py feature_space_dialog.py 48 | 49 | UI_FILES = feature_space_dialog_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/farhang/.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 | -------------------------------------------------------------------------------- /plot.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | matplotlib.use("Qt5Agg") 3 | from matplotlib.figure import Figure 4 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar 5 | from matplotlib.widgets import RectangleSelector 6 | from osgeo import gdal 7 | from matplotlib.widgets import RectangleSelector 8 | import numpy as np 9 | from matplotlib import colors 10 | from .feature_space_dialog import FeatureSpaceDialog 11 | from uuid import uuid4 12 | from qgis.core import QgsRasterLayer, QgsVectorLayer, QgsProject, QgsProcessingParameterRasterDestination 13 | import processing 14 | from matplotlib.colors import LogNorm 15 | 16 | cmap = colors.ListedColormap(['red']) 17 | 18 | class GuiProgram(FeatureSpaceDialog): 19 | def __init__(self, dialog, wcb, wcb2, wcb_red, wcb_green, wcb_blue, sb, sb2, sb_red, sb_green, sb_blue): 20 | self.dialog = dialog 21 | 22 | self.wcb = wcb 23 | self.wcb2= wcb2 24 | self.wcb_red= wcb_red 25 | self.wcb_green= wcb_green 26 | self.wcb_blue= wcb_blue 27 | 28 | self.sb = sb 29 | self.sb2 = sb2 30 | self.sb_red = sb_red 31 | self.sb_green = sb_green 32 | self.sb_blue = sb_blue 33 | 34 | FeatureSpaceDialog.__init__(self) 35 | self.setupUi(dialog) 36 | 37 | figure = Figure() 38 | axis = figure.add_subplot(111) 39 | figure2 = Figure() 40 | axis2 = figure2.add_subplot(111) 41 | self.initialize_figure(figure, axis, figure2, axis2) 42 | 43 | self.pushButton.clicked.connect(self.change_plot) 44 | self.add_raster.clicked.connect(self.save_as_raster) 45 | self.add_vector.clicked.connect(self.save_as_vector) 46 | 47 | def write_tiff(self, data, filename, proj, geo, dtype=gdal.GDT_Float32): 48 | rows, cols = data.shape 49 | driver = gdal.GetDriverByName("GTiff") 50 | DataSet = driver.Create(filename, cols, rows, 1, dtype) 51 | DataSet.SetGeoTransform(geo) 52 | DataSet.SetProjection(proj) 53 | DataSet.GetRasterBand(1).WriteArray(data) 54 | DataSet.FlushCache() 55 | DataSet = None 56 | 57 | def create_scatter(self, ax, x, y): 58 | bins = [1000, 1000] #TODO calculate dynamically 59 | hh, locx, locy = np.histogram2d(x, y, bins=bins) 60 | z = np.array([hh[np.argmax(a<=locx[1:]),np.argmax(b<=locy[1:])] for a,b in zip(x,y)]) 61 | idx = z.argsort() 62 | x2, y2, z2 = x[idx], y[idx], z[idx] 63 | s = ax.scatter(x2, y2, c=z2, cmap='jet', marker='.', s=0.05, norm = LogNorm()) 64 | 65 | def on_click(self, event): 66 | if event.button == 1 or event.button == 3 and not self.rs.active: 67 | self.rs.set_active(True) 68 | else: 69 | self.rs.set_active(False) 70 | 71 | def line_select_callback(self, eclick, erelease): 72 | x1, y1 = eclick.xdata, eclick.ydata 73 | x2, y2 = erelease.xdata, erelease.ydata 74 | 75 | b1_condition = np.logical_and(self.band1 > x1, self.band1 < x2) 76 | b2_condition = np.logical_and(self.band2 > y1, self.band2 < y2) 77 | self.conds = np.logical_and(b1_condition, b2_condition).astype(float) 78 | self.conds[self.conds == 0]= np.nan 79 | 80 | self.ax2.clear() 81 | self.ax2.imshow(self.rgb) 82 | self.ax2.imshow(self.conds, cmap=cmap, alpha=0.6) 83 | self.ax2.axis('off') 84 | self.fig2.tight_layout() 85 | self.canvas2.draw() 86 | 87 | 88 | def save_as_raster(self): 89 | dest = QgsProcessingParameterRasterDestination(name=str(uuid4())) 90 | filename = dest.generateTemporaryDestination() 91 | 92 | proj = self.image1.GetProjection() 93 | geo = self.image1.GetGeoTransform() 94 | data = self.conds 95 | self.write_tiff(data, filename, proj, geo) 96 | 97 | input_name = self.layer_name_input.toPlainText() 98 | if input_name == '': 99 | layer_name = "feature_space_selected_raster" 100 | else: 101 | layer_name = input_name 102 | 103 | layer = QgsRasterLayer(filename, layer_name) 104 | if not layer.isValid(): 105 | print("Layer failed to load!") 106 | QgsProject.instance().addMapLayer(layer) 107 | 108 | def save_as_vector(self): 109 | dest = QgsProcessingParameterRasterDestination(name=str(uuid4())) 110 | filename = dest.generateTemporaryDestination() 111 | 112 | proj = self.image1.GetProjection() 113 | geo = self.image1.GetGeoTransform() 114 | data = self.conds 115 | self.write_tiff(data, filename, proj, geo) 116 | 117 | input_name = self.layer_name_input.toPlainText() 118 | if input_name == '': 119 | layer_name = "feature_space_selected_vector" 120 | else: 121 | layer_name = input_name 122 | 123 | poly_opts = { 'BAND' : 1, 'EXTRA' : f'-mask {filename}', 'INPUT' : filename, 'OUTPUT' : 'TEMPORARY_OUTPUT' } 124 | vlayer = processing.run("gdal:polygonize", poly_opts) 125 | vlayer = QgsVectorLayer(vlayer["OUTPUT"], layer_name) 126 | QgsProject.instance().addMapLayer(vlayer) 127 | 128 | def get_band_as_array(self, filepath, band_number, flat=False): 129 | image = gdal.Open(filepath) 130 | band = image.GetRasterBand(band_number) 131 | nodata = band.GetNoDataValue() 132 | band = band.ReadAsArray() 133 | 134 | if flat: 135 | return image, band, nodata, band.flatten() 136 | else: 137 | return image, band, nodata 138 | 139 | def create_rgb(self, red, green, blue): 140 | rgb = np.zeros((red.shape[0],red.shape[1], 3)) 141 | rgb[:,:,0] = red 142 | rgb[:,:,1] = green 143 | rgb[:,:,2] = blue 144 | return rgb 145 | 146 | def change_plot(self): 147 | self.ax.clear() 148 | self.ax2.clear() 149 | 150 | try: 151 | xfile = self.wcb.currentLayer().source() 152 | yfile = self.wcb2.currentLayer().source() 153 | xband = int(self.sb.currentText()) 154 | yband = int(self.sb2.currentText()) 155 | 156 | self.image1, self.band1, self.nodatax, self.x = self.get_band_as_array(xfile, xband, True) 157 | self.image2, self.band2, self.nodatay, self.y = self.get_band_as_array(yfile, yband, True) 158 | 159 | self.x = self.x[np.isfinite(self.x)] 160 | self.y = self.y[np.isfinite(self.y)] 161 | 162 | self.create_scatter(self.ax, self.x, self.y) 163 | 164 | self.x = self.x[self.x!=self.nodatax] 165 | self.y = self.y[self.y!=self.nodatay] 166 | 167 | amountx = max(self.x)*0.1 168 | amounty = max(self.y)*0.1 169 | self.ax.set_xlim(min(self.x) - amountx, max(self.x) + amountx) 170 | self.ax.set_ylim(min(self.y) - amounty, max(self.y) + amounty) 171 | 172 | self.ax.set_xlabel(xfile.split('/')[-1] + '\nBand: ' + str(xband)) 173 | self.ax.set_ylabel(yfile.split('/')[-1] + '\nBand: ' + str(yband)) 174 | # Make sure everything fits inside the canvas 175 | self.fig.tight_layout() 176 | self.canvas.draw() 177 | except Exception as e: 178 | print(e) 179 | pass 180 | 181 | try: 182 | self.image_red, self.band_red, _ = self.get_band_as_array(self.wcb_red.currentLayer().source(), int(self.sb_red.currentText())) 183 | self.image_green, self.band_green, _ = self.get_band_as_array(self.wcb_green.currentLayer().source(), int(self.sb_green.currentText())) 184 | self.image_blue, self.band_blue, _ = self.get_band_as_array(self.wcb_blue.currentLayer().source(), int(self.sb_blue.currentText())) 185 | 186 | self.rgb = self.create_rgb(self.band_red, self.band_green, self.band_blue) 187 | self.rgb = self.rgb/self.rgb.max() 188 | self.ax2.imshow(self.rgb) 189 | self.ax2.axis('off') 190 | self.fig2.tight_layout() 191 | self.canvas2.draw() 192 | except Exception as e: 193 | print(e) 194 | pass 195 | 196 | def initialize_figure(self, fig, ax, fig2, ax2): 197 | self.fig = fig 198 | self.ax = ax 199 | 200 | self.fig2 = fig2 201 | self.ax2 = ax2 202 | 203 | self.canvas = FigureCanvas(self.fig) 204 | self.verticalLayout.addWidget(self.canvas) 205 | self.canvas.draw() 206 | 207 | self.canvas2 = FigureCanvas(self.fig2) 208 | self.verticalLayout_2.addWidget(self.canvas2) 209 | self.canvas2.draw() 210 | 211 | # Toolbar creation 212 | # self.toolbar = NavigationToolbar(self.canvas, self.plotWindow, 213 | # coordinates=True) 214 | # self.verticalLayout.addWidget(self.toolbar) 215 | self.rs = RectangleSelector(self.ax, self.line_select_callback, 216 | useblit=True, 217 | button=[1, 3], # don't use middle button 218 | minspanx=5, minspany=5, 219 | spancoords='pixels', 220 | interactive=True) 221 | 222 | 223 | # set Qlayout properties and show window 224 | # self.gridLayout = QtW.QGridLayout(self._main) 225 | # self.setLayout(self.verticalLayout) 226 | # self.show() 227 | 228 | # connect mouse events to canvas 229 | self.fig.canvas.mpl_connect('button_press_event', self.on_click) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /feature_space.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | FeatureSpace 5 | A QGIS plugin 6 | test 7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 8 | ------------------- 9 | begin : 2023-06-05 10 | git sha : $Format:%H$ 11 | copyright : (C) 2023 by fz 12 | email : fz 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 | from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication 26 | from qgis.PyQt.QtGui import QIcon 27 | from qgis.PyQt.QtWidgets import QAction 28 | from qgis.core import QgsMapLayerProxyModel 29 | from qgis.gui import QgsMapLayerComboBox 30 | from PyQt5.QtWidgets import QComboBox, QLabel 31 | 32 | # Initialize Qt resources from file resources.py 33 | from .resources import * 34 | # Import the code for the dialog 35 | from .feature_space_dialog import FeatureSpaceDialog 36 | import os.path 37 | from .plot import GuiProgram 38 | 39 | 40 | class FeatureSpace: 41 | """QGIS Plugin Implementation.""" 42 | 43 | def __init__(self, iface): 44 | """Constructor. 45 | 46 | :param iface: An interface instance that will be passed to this class 47 | which provides the hook by which you can manipulate the QGIS 48 | application at run time. 49 | :type iface: QgsInterface 50 | """ 51 | # Save reference to the QGIS interface 52 | self.iface = iface 53 | # initialize plugin directory 54 | self.plugin_dir = os.path.dirname(__file__) 55 | # initialize locale 56 | locale = QSettings().value('locale/userLocale')[0:2] 57 | locale_path = os.path.join( 58 | self.plugin_dir, 59 | 'i18n', 60 | 'FeatureSpace_{}.qm'.format(locale)) 61 | 62 | if os.path.exists(locale_path): 63 | self.translator = QTranslator() 64 | self.translator.load(locale_path) 65 | QCoreApplication.installTranslator(self.translator) 66 | 67 | # Declare instance attributes 68 | self.actions = [] 69 | self.menu = self.tr(u'&feature space') 70 | 71 | # Check if plugin was started the first time in current QGIS session 72 | # Must be set in initGui() to survive plugin reloads 73 | self.first_start = None 74 | 75 | # noinspection PyMethodMayBeStatic 76 | def tr(self, message): 77 | """Get the translation for a string using Qt translation API. 78 | 79 | We implement this ourselves since we do not inherit QObject. 80 | 81 | :param message: String for translation. 82 | :type message: str, QString 83 | 84 | :returns: Translated version of message. 85 | :rtype: QString 86 | """ 87 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 88 | return QCoreApplication.translate('FeatureSpace', message) 89 | 90 | 91 | def add_action( 92 | self, 93 | icon_path, 94 | text, 95 | callback, 96 | enabled_flag=True, 97 | add_to_menu=True, 98 | add_to_toolbar=True, 99 | status_tip=None, 100 | whats_this=None, 101 | parent=None): 102 | """Add a toolbar icon to the toolbar. 103 | 104 | :param icon_path: Path to the icon for this action. Can be a resource 105 | path (e.g. ':/plugins/foo/bar.png') or a normal file system path. 106 | :type icon_path: str 107 | 108 | :param text: Text that should be shown in menu items for this action. 109 | :type text: str 110 | 111 | :param callback: Function to be called when the action is triggered. 112 | :type callback: function 113 | 114 | :param enabled_flag: A flag indicating if the action should be enabled 115 | by default. Defaults to True. 116 | :type enabled_flag: bool 117 | 118 | :param add_to_menu: Flag indicating whether the action should also 119 | be added to the menu. Defaults to True. 120 | :type add_to_menu: bool 121 | 122 | :param add_to_toolbar: Flag indicating whether the action should also 123 | be added to the toolbar. Defaults to True. 124 | :type add_to_toolbar: bool 125 | 126 | :param status_tip: Optional text to show in a popup when mouse pointer 127 | hovers over the action. 128 | :type status_tip: str 129 | 130 | :param parent: Parent widget for the new action. Defaults None. 131 | :type parent: QWidget 132 | 133 | :param whats_this: Optional text to show in the status bar when the 134 | mouse pointer hovers over the action. 135 | 136 | :returns: The action that was created. Note that the action is also 137 | added to self.actions list. 138 | :rtype: QAction 139 | """ 140 | 141 | icon = QIcon(icon_path) 142 | action = QAction(icon, text, parent) 143 | action.triggered.connect(callback) 144 | action.setEnabled(enabled_flag) 145 | 146 | if status_tip is not None: 147 | action.setStatusTip(status_tip) 148 | 149 | if whats_this is not None: 150 | action.setWhatsThis(whats_this) 151 | 152 | if add_to_toolbar: 153 | # Adds plugin icon to Plugins toolbar 154 | self.iface.addToolBarIcon(action) 155 | 156 | if add_to_menu: 157 | self.iface.addPluginToRasterMenu( 158 | self.menu, 159 | action) 160 | 161 | self.actions.append(action) 162 | 163 | return action 164 | 165 | def initGui(self): 166 | """Create the menu entries and toolbar icons inside the QGIS GUI.""" 167 | 168 | icon_path = ':/plugins/feature_space/icon.png' 169 | self.add_action( 170 | icon_path, 171 | text=self.tr(u'feature space'), 172 | callback=self.run, 173 | parent=self.iface.mainWindow()) 174 | 175 | # will be set False in run() 176 | self.first_start = True 177 | 178 | 179 | def unload(self): 180 | """Removes the plugin menu item and icon from QGIS GUI.""" 181 | for action in self.actions: 182 | self.iface.removePluginRasterMenu( 183 | self.tr(u'&feature space'), 184 | action) 185 | self.iface.removeToolBarIcon(action) 186 | 187 | 188 | 189 | def load_fields(self, wcb, sb): 190 | lyr_nm=wcb.currentLayer() 191 | 192 | try: 193 | sb.clear() 194 | num_of_bands = lyr_nm.bandCount() 195 | if num_of_bands ==1: 196 | sb.addItems([str(1)]) 197 | else: 198 | sb.addItems([ str(i+1) for i in range(num_of_bands)]) 199 | except: 200 | pass 201 | 202 | def create_map_layer_drpdwn(self, dlg, width, loc, type, sb, label): 203 | wcb = QgsMapLayerComboBox(dlg) 204 | wcb.setFixedWidth(width) 205 | wcb.move(*loc) 206 | wcb.setFilters( type ) 207 | wcb.setAllowEmptyLayer(True) 208 | wcb.setCurrentIndex(0) 209 | wcb.layerChanged.connect(lambda: self.load_fields(wcb, sb)) 210 | Label = QLabel(dlg) 211 | Label.setText(label) 212 | Label.move(loc[0]-50, loc[1]) 213 | return wcb 214 | 215 | def create_select_band_drpdwn(self, dlg, width, loc): 216 | sb = QComboBox(dlg) 217 | sb.setFixedWidth(width) 218 | sb.move(*loc) 219 | return sb 220 | 221 | def run(self): 222 | # Only create GUI ONCE in callback, so that it will only load when the plugin is started 223 | if self.first_start == True: 224 | self.first_start = False 225 | self.dlg = FeatureSpaceDialog() 226 | 227 | 228 | self.sb = self.create_select_band_drpdwn(self.dlg, 50, [380,100]) 229 | self.sb2 = self.create_select_band_drpdwn(self.dlg, 50, [380,140]) 230 | 231 | self.sb_red = self.create_select_band_drpdwn(self.dlg, 50, [920,80]) 232 | self.sb_green = self.create_select_band_drpdwn(self.dlg, 50, [920,120]) 233 | self.sb_blue = self.create_select_band_drpdwn(self.dlg, 50, [920,160]) 234 | 235 | self.wcb = self.create_map_layer_drpdwn(self.dlg, 150, [220,100], QgsMapLayerProxyModel.RasterLayer, self.sb, 'X Axis') 236 | self.wcb2 = self.create_map_layer_drpdwn(self.dlg, 150, [220,140], QgsMapLayerProxyModel.RasterLayer, self.sb2, 'Y Axis') 237 | 238 | self.wcb_red = self.create_map_layer_drpdwn(self.dlg, 150, [750,80], QgsMapLayerProxyModel.RasterLayer, self.sb_red, 'Red') 239 | self.wcb_green = self.create_map_layer_drpdwn(self.dlg, 150, [750,120], QgsMapLayerProxyModel.RasterLayer, self.sb_green, "Green") 240 | self.wcb_blue = self.create_map_layer_drpdwn(self.dlg, 150, [750,160], QgsMapLayerProxyModel.RasterLayer, self.sb_blue, 'Blue') 241 | 242 | 243 | program = GuiProgram(self.dlg, 244 | self.wcb, self.wcb2, self.wcb_red, self.wcb_green, self.wcb_blue, 245 | self.sb, self.sb2, self.sb_red, self.sb_green, self.sb_blue) 246 | self.dlg.show() 247 | 248 | # Run the dialog event loop 249 | result = self.dlg.exec_() 250 | if result: 251 | pass 252 | 253 | --------------------------------------------------------------------------------