├── data_test ├── world.cpg ├── world.dbf ├── world.shp ├── world.shx ├── world.prj └── world.qpj ├── requirements.txt ├── test ├── tenbytenraster.keywords ├── __init__.py ├── tenbytenraster.prj ├── tenbytenraster.asc ├── tenbytenraster.asc.aux.xml ├── tenbytenraster.lic ├── test_resources.py ├── tenbytenraster.qml ├── test_neatmap_dialog.py ├── test_translations.py ├── utilities.py ├── test_init.py ├── test_qgis_environment.py └── qgis_interface.py ├── icon.png ├── doc └── doc ├── 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 ├── .travis.yml ├── .gitignore ├── metadata.txt ├── __init__.py ├── neatmap_about_dialog.py ├── neatmap_dialog.py ├── pb_tool.cfg ├── plugin_upload.py ├── classification.py ├── neatmap_about_dialog.ui ├── indicatorCalculation.py ├── app.py ├── morpho.py ├── Makefile ├── pylintrc ├── README.md ├── neatmap_dialog_base.ui ├── square_packing.py └── neatmap.py /data_test/world.cpg: -------------------------------------------------------------------------------- 1 | UTF-8 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scikit-learn===0.22.2.post1 2 | -------------------------------------------------------------------------------- /test/tenbytenraster.keywords: -------------------------------------------------------------------------------- 1 | title: Tenbytenraster 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IGNF/NeatMap/HEAD/icon.png -------------------------------------------------------------------------------- /data_test/world.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IGNF/NeatMap/HEAD/data_test/world.dbf -------------------------------------------------------------------------------- /data_test/world.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IGNF/NeatMap/HEAD/data_test/world.shp -------------------------------------------------------------------------------- /data_test/world.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IGNF/NeatMap/HEAD/data_test/world.shx -------------------------------------------------------------------------------- /doc/doc: -------------------------------------------------------------------------------- 1 | See doc on : https://github.com/IGNF/NeatMap 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /data_test/world.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /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]] -------------------------------------------------------------------------------- /data_test/world.qpj: -------------------------------------------------------------------------------- 1 | GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] 2 | -------------------------------------------------------------------------------- /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 | .. NeatMap 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 NeatMap'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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | before_install: 4 | - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key 51F523511C7028C3 5 | - echo "deb https://qgis.org/ubuntu bionic main" | sudo tee -a /etc/apt/sources.list 6 | - sudo apt update 7 | - sudo apt install qgis 8 | - pip install --upgrade pip 9 | - sudo apt install python3-tk 10 | - sudo apt install xvfb 11 | - pip install scikit-learn 12 | - pip install pyqt5 13 | 14 | language: python 15 | 16 | python: 17 | - "3.7" 18 | 19 | 20 | before_script: 21 | - which qgis 22 | - export QGIS_PREFIX_PATH=/usr 23 | - export PYTHONPATH=/home/travis/.local/lib/python3.7/site-packages:/usr/lib/python3/dist-packages:${QGIS_PREFIX_PATH}/share/qgis/python/:${QGIS_PREFIX_PATH}/share/qgis/python/plugins:`pwd` 24 | - echo "PYTHONPATH:" $PYTHONPATH 25 | - echo "PYTHONPATH:" $PYTHONPATH 26 | - export LD_LIBRARY_PATH=${QGIS_PREFIX_PATH}/lib 27 | 28 | script: 29 | - which python3 30 | - xvfb-run -a python3 app.py 31 | -------------------------------------------------------------------------------- /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__ = 'julien.perret@gmail.com' 12 | __date__ = '2016-11-30' 13 | __copyright__ = 'Copyright 2016, IGN' 14 | 15 | import unittest 16 | 17 | from PyQt4.QtGui import QIcon 18 | 19 | 20 | 21 | class TidyCityDialogTest(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/TidyCity/icon.png' 35 | icon = QIcon(path) 36 | self.assertFalse(icon.isNull()) 37 | 38 | if __name__ == "__main__": 39 | suite = unittest.makeSuite(TidyCityResourcesTest) 40 | runner = unittest.TextTestRunner(verbosity=2) 41 | runner.run(suite) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .project 92 | .pydevproject 93 | .settings/ 94 | .idea 95 | 96 | #Result data 97 | data_test/* 98 | -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | # This file contains metadata for your plugin. Since 2 | # version 2.0 of QGIS this is the proper way to supply 3 | # information about a plugin. The old method of 4 | # embedding metadata in __init__.py will 5 | # is no longer supported since version 2.0. 6 | 7 | # This file should be included when you package your plugin.# Mandatory items: 8 | 9 | [general] 10 | name=NeatMap 11 | qgisMinimumVersion=3.0 12 | description=A simple QGIS python plugin for building neat maps. 13 | version=1.3 14 | author=IGN-ENSG 15 | email=julien.perret@gmail.com; mickael.brasebin@gmail.com 16 | 17 | about=A QGIS python plugin for building organized maps (i.e for the classification and producing layout of polygonal features). Initially developped by Mickaël Brasebin Julien Perret, Rose Mathelier and Bruce Thomas at ENSG/IGNF. The project is developed as an Open-Source library based on QGIS API V3.0, for morphological operators and layout generation and Scikit learn 0.19.1 : for the classification. 18 | 19 | 20 | tracker=https://github.com/IGNF/NeatMap/issues 21 | repository=https://github.com/IGNF/NeatMap 22 | # End of mandatory metadata 23 | 24 | # Recommended items: 25 | 26 | # Uncomment the following line and add your changelog: 27 | # changelog= 28 | 29 | # Tags are comma separated with spaces allowed 30 | tags=classification;layouts 31 | 32 | homepage=https://github.com/IGNF/NeatMap 33 | category=Vector 34 | icon=icon.png 35 | # experimental flag 36 | experimental=True 37 | 38 | # deprecated flag (applies to the whole plugin, not just a single version) 39 | deprecated=False 40 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | NeatMap 5 | A QGIS plugin 6 | A simple QGIS python plugin for building tidy cities. 7 | ------------------- 8 | begin : 2016-11-30 9 | copyright : (C) 2016 by IGN 10 | email : julien.perret@gmail.com 11 | git sha : $Format:%H$ 12 | ***************************************************************************/ 13 | 14 | /*************************************************************************** 15 | * * 16 | * This program is free software; you can redistribute it and/or modify * 17 | * it under the terms of the GNU General Public License as published by * 18 | * the Free Software Foundation; either version 2 of the License, or * 19 | * (at your option) any later version. * 20 | * * 21 | ***************************************************************************/ 22 | This script initializes the plugin, making it known to QGIS. 23 | """ 24 | 25 | 26 | # noinspection PyPep8Naming 27 | def classFactory(iface): # pylint: disable=invalid-name 28 | """Load NeatMap class from file NeatMap. 29 | 30 | :param iface: A QGIS interface instance. 31 | :type iface: QgsInterface 32 | """ 33 | # 34 | from .neatmap import NeatMap 35 | return NeatMap(iface) 36 | -------------------------------------------------------------------------------- /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 | print ${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 | -------------------------------------------------------------------------------- /test/test_neatmap_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__ = 'julien.perret@gmail.com' 12 | __date__ = '2016-11-30' 13 | __copyright__ = 'Copyright 2016, IGN' 14 | 15 | import unittest 16 | 17 | from PyQt4.QtGui import QDialogButtonBox, QDialog 18 | 19 | from neatmap_dialog import NeatMapDialog 20 | 21 | from utilities import get_qgis_app 22 | QGIS_APP = get_qgis_app() 23 | 24 | 25 | class TidyCityDialogTest(unittest.TestCase): 26 | """Test dialog works.""" 27 | 28 | def setUp(self): 29 | """Runs before each test.""" 30 | self.dialog = NeatMapDialog(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(TidyCityDialogTest) 53 | runner = unittest.TextTestRunner(verbosity=2) 54 | runner.run(suite) 55 | 56 | -------------------------------------------------------------------------------- /neatmap_about_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | NeatMapAboutDialog 5 | 6 | ------------------- 7 | begin : 2016-11-30 8 | git sha : $Format:%H$ 9 | copyright : (C) 2016 by IGN 10 | email : mickael.brasebin@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | 23 | import os 24 | 25 | from PyQt5 import QtGui, uic 26 | from PyQt5.QtWidgets import QDialog 27 | 28 | FORM_CLASS, _ = uic.loadUiType(os.path.join( 29 | os.path.dirname(__file__), 'neatmap_about_dialog.ui')) 30 | 31 | 32 | class NeatMapAboutDialog(QDialog, FORM_CLASS): 33 | def __init__(self, parent=None): 34 | """Constructor.""" 35 | super(NeatMapAboutDialog, self).__init__(parent) 36 | # Set up the user interface from Designer. 37 | # After setupUI you can access any designer object by doing 38 | # self., and you can use autoconnect slots - see 39 | # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html 40 | # #widgets-and-dialogs-with-auto-connect 41 | self.setupUi(self) 42 | 43 | -------------------------------------------------------------------------------- /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 PyQt4.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 os.environ.iterkeys(): 30 | os.environ.__delitem__('LANG') 31 | 32 | def tearDown(self): 33 | """Runs after each test.""" 34 | if 'LANG' in os.environ.iterkeys(): 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 | -------------------------------------------------------------------------------- /neatmap_dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | NeatMapDialog 5 | A QGIS plugin 6 | A simple QGIS python plugin for building neat maps. 7 | ------------------- 8 | begin : 2016-11-30 9 | git sha : $Format:%H$ 10 | copyright : (C) 2016 by IGN 11 | email : julien.perret@gmail.com 12 | ***************************************************************************/ 13 | 14 | /*************************************************************************** 15 | * * 16 | * This program is free software; you can redistribute it and/or modify * 17 | * it under the terms of the GNU General Public License as published by * 18 | * the Free Software Foundation; either version 2 of the License, or * 19 | * (at your option) any later version. * 20 | * * 21 | ***************************************************************************/ 22 | """ 23 | 24 | import os 25 | 26 | from PyQt5 import QtGui, uic 27 | from PyQt5.QtWidgets import QDialog 28 | 29 | FORM_CLASS, _ = uic.loadUiType(os.path.join( 30 | os.path.dirname(__file__), 'neatmap_dialog_base.ui')) 31 | 32 | 33 | class NeatMapDialog(QDialog, FORM_CLASS): 34 | def __init__(self, parent=None): 35 | """Constructor.""" 36 | super(NeatMapDialog, self).__init__(parent) 37 | # Set up the user interface from Designer. 38 | # After setupUI you can access any designer object by doing 39 | # self., and you can use autoconnect slots - see 40 | # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html 41 | # #widgets-and-dialogs-with-auto-connect 42 | self.setupUi(self) 43 | -------------------------------------------------------------------------------- /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 PyQt4 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 | -------------------------------------------------------------------------------- /pb_tool.cfg: -------------------------------------------------------------------------------- 1 | #/*************************************************************************** 2 | # TidyCity 3 | # 4 | # Configuration file for plugin builder tool (pb_tool) 5 | # ------------------- 6 | # begin : 2016-11-30 7 | # copyright : (C) 2016 by IGN 8 | # email : julien.perret@gmail.com 9 | # ***************************************************************************/ 10 | # 11 | #/*************************************************************************** 12 | # * * 13 | # * This program is free software; you can redistribute it and/or modify * 14 | # * it under the terms of the GNU General Public License as published by * 15 | # * the Free Software Foundation; either version 2 of the License, or * 16 | # * (at your option) any later version. * 17 | # * * 18 | # ***************************************************************************/ 19 | # 20 | # 21 | # You can install pb_tool using: 22 | # pip install http://geoapt.net/files/pb_tool.zip 23 | # 24 | # Consider doing your development (and install of pb_tool) in a virtualenv. 25 | # 26 | # For details on setting up and using pb_tool, see: 27 | # http://spatialgalaxy.net/qgis-plugin-development-with-pb_tool 28 | # 29 | # Issues and pull requests here: 30 | # https://github.com/g-sherman/plugin_build_tool: 31 | # 32 | # Sane defaults for your plugin generated by the Plugin Builder are 33 | # already set below. 34 | # 35 | # As you add Python source files and UI files to your plugin, add 36 | # them to the appropriate [files] section below. 37 | 38 | [plugin] 39 | # Name of the plugin. This is the name of the directory that will 40 | # be created in .qgis2/python/plugins 41 | name: TidyCity 42 | 43 | [files] 44 | # Python files that should be deployed with the plugin 45 | python_files: __init__.py tidy_city.py tidy_city_dialog.py 46 | 47 | # The main dialog file that is loaded (not compiled) 48 | main_dialog: tidy_city_dialog_base.ui 49 | 50 | # Other ui files for dialogs you create (these will be compiled) 51 | compiled_ui_files: 52 | 53 | # Resource file(s) that will be compiled 54 | resource_files: resources.qrc 55 | 56 | # Other files required for the plugin 57 | extras: metadata.txt icon.png 58 | 59 | # Other directories to be deployed with the plugin. 60 | # These must be subdirectories under the plugin directory 61 | extra_dirs: 62 | 63 | # ISO code(s) for any locales (translations), separated by spaces. 64 | # Corresponding .ts files must exist in the i18n directory 65 | locales: 66 | 67 | [help] 68 | # the built help directory that should be deployed with the plugin 69 | dir: help/build/html 70 | # the name of the directory to target in the deployed plugin 71 | target: help 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /plugin_upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """This script uploads a plugin package on the server. 4 | Authors: A. Pasotti, V. Picavet 5 | git sha : $TemplateVCSFormat 6 | """ 7 | 8 | import sys 9 | import getpass 10 | import xmlrpclib 11 | from optparse import OptionParser 12 | 13 | # Configuration 14 | PROTOCOL = 'http' 15 | SERVER = 'plugins.qgis.org' 16 | PORT = '80' 17 | ENDPOINT = '/plugins/RPC2/' 18 | VERBOSE = False 19 | 20 | 21 | def main(parameters, arguments): 22 | """Main entry point. 23 | 24 | :param parameters: Command line parameters. 25 | :param arguments: Command line arguments. 26 | """ 27 | address = "%s://%s:%s@%s:%s%s" % ( 28 | PROTOCOL, 29 | parameters.username, 30 | parameters.password, 31 | parameters.server, 32 | parameters.port, 33 | ENDPOINT) 34 | print "Connecting to: %s" % hide_password(address) 35 | 36 | server = xmlrpclib.ServerProxy(address, verbose=VERBOSE) 37 | 38 | try: 39 | plugin_id, version_id = server.plugin.upload( 40 | xmlrpclib.Binary(open(arguments[0]).read())) 41 | print "Plugin ID: %s" % plugin_id 42 | print "Version ID: %s" % version_id 43 | except xmlrpclib.ProtocolError, err: 44 | print "A protocol error occurred" 45 | print "URL: %s" % hide_password(err.url, 0) 46 | print "HTTP/HTTPS headers: %s" % err.headers 47 | print "Error code: %d" % err.errcode 48 | print "Error message: %s" % err.errmsg 49 | except xmlrpclib.Fault, err: 50 | print "A fault occurred" 51 | print "Fault code: %d" % err.faultCode 52 | print "Fault string: %s" % err.faultString 53 | 54 | 55 | def hide_password(url, start=6): 56 | """Returns the http url with password part replaced with '*'. 57 | 58 | :param url: URL to upload the plugin to. 59 | :type url: str 60 | 61 | :param start: Position of start of password. 62 | :type start: int 63 | """ 64 | start_position = url.find(':', start) + 1 65 | end_position = url.find('@') 66 | return "%s%s%s" % ( 67 | url[:start_position], 68 | '*' * (end_position - start_position), 69 | url[end_position:]) 70 | 71 | 72 | if __name__ == "__main__": 73 | parser = OptionParser(usage="%prog [options] plugin.zip") 74 | parser.add_option( 75 | "-w", "--password", dest="password", 76 | help="Password for plugin site", metavar="******") 77 | parser.add_option( 78 | "-u", "--username", dest="username", 79 | help="Username of plugin site", metavar="user") 80 | parser.add_option( 81 | "-p", "--port", dest="port", 82 | help="Server port to connect to", metavar="80") 83 | parser.add_option( 84 | "-s", "--server", dest="server", 85 | help="Specify server name", metavar="plugins.qgis.org") 86 | options, args = parser.parse_args() 87 | if len(args) != 1: 88 | print "Please specify zip file.\n" 89 | parser.print_help() 90 | sys.exit(1) 91 | if not options.server: 92 | options.server = SERVER 93 | if not options.port: 94 | options.port = PORT 95 | if not options.username: 96 | # interactive mode 97 | username = getpass.getuser() 98 | print "Please enter user name [%s] :" % username, 99 | res = raw_input() 100 | if res != "": 101 | options.username = res 102 | else: 103 | options.username = username 104 | if not options.password: 105 | # interactive mode 106 | options.password = getpass.getpass() 107 | main(options, args) 108 | -------------------------------------------------------------------------------- /classification.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QVariant 2 | from qgis.core import * 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import os 7 | 8 | from sklearn import datasets 9 | from sklearn import preprocessing 10 | from sklearn.cluster import KMeans 11 | from sklearn.datasets import make_blobs 12 | from sklearn.metrics import pairwise_distances_argmin 13 | 14 | from sklearn.utils import Bunch 15 | 16 | """ 17 | Classification code only k-means for the moment 18 | """ 19 | 20 | 21 | # Transforing classification to vector layer (layer : input layer, attributes : attribute used for classification, layerName ; name of the layer, attributeClasse : attribute of the classification, attributeID : name of attribute ID) , copyAtt indicate if other attributes are copied in output 22 | def kmeans(layer, attributes, nbClasses, layerName, attributeClass, attributeID, copyAtt): 23 | # Load in the `digits` data 24 | dataset = prepareDataset(layer, attributes) 25 | # Defining k-means 26 | k_means = KMeans(init='k-means++', n_clusters=nbClasses, n_init=10) 27 | # Applying k-means 28 | k_means.fit(dataset.data) 29 | # Determining centers 30 | k_means_cluster_centers = np.sort(k_means.cluster_centers_, axis=0) 31 | # Association between points and nearest centers 32 | k_means_labels = pairwise_distances_argmin(dataset.data, k_means_cluster_centers) 33 | # Transforming classification to VectorLayer 34 | return export(layer, attributes, layerName, attributeClass, k_means_labels, attributeID, copyAtt) 35 | 36 | """ 37 | DataManagement 38 | """ 39 | 40 | """ 41 | Preparing dataset for sklearn from a layer and a set of selected numeric attributes 42 | """ 43 | def prepareDataset(layer, attributes): 44 | n_samples = layer.featureCount() 45 | n_features = len(attributes) 46 | 47 | valueArray = [] 48 | attributeArray = [] 49 | 50 | for a in attributes: 51 | attributeArray.append(a) 52 | columns = np.array(attributeArray) 53 | 54 | for f in layer.getFeatures(): 55 | data_temp = [] 56 | for a in attributes: 57 | data_temp.append(f.attribute(a)) 58 | valueArray.append(data_temp) 59 | 60 | data = np.array(valueArray) 61 | # print(data) 62 | data = preprocessing.scale(data) 63 | # print(data) 64 | dataset = Bunch(data=data, columns=columns) 65 | return dataset 66 | 67 | 68 | 69 | """ 70 | Exporting a vector layer from input dataset and classification 71 | """ 72 | # Transforing classification to vector layer (layer : input layer, attributes : attribute used for classification, layerName ; nae of the layer, attributeClasse : attribute of the classification, vectorClass : the vector with corresponding class, attributeID : name of attribute ID) 73 | def export(layer, attributes, layerName, attributeClass, vectorClass, attributeID, copyAtt): 74 | vl = QgsVectorLayer("Polygon", layerName, "memory") 75 | pr = vl.dataProvider() 76 | vl.startEditing() 77 | 78 | 79 | fields = [QgsField(attributeClass, QVariant.Int)] 80 | 81 | 82 | 83 | 84 | if copyAtt : 85 | #If copy is activated we copy all attributes 86 | for fieldTemp in layer.fields(): 87 | fields.append(fieldTemp) 88 | else : 89 | #if not we only keeps the fild id and necessary to classification 90 | fields.append(layer.fields().field(attributeID)) 91 | for a in attributes: 92 | fields.append(QgsField(a, QVariant.Double, "Real", 10, 3)) 93 | 94 | 95 | 96 | pr.addAttributes(fields) 97 | vl.updateFields() 98 | 99 | featureList = [] 100 | 101 | count = 0; 102 | 103 | for f in layer.getFeatures(): 104 | geom = f.geometry() 105 | 106 | feat = QgsFeature() 107 | feat.setGeometry(geom) 108 | feat.initAttributes(len(fields)) 109 | 110 | #print(vectorClass[count]) 111 | feat.setAttribute(0, vectorClass.item(count)) 112 | 113 | 114 | countAtt = 1 115 | 116 | if copyAtt: 117 | countTemp = 0; 118 | for field in layer.fields() : 119 | feat.setAttribute( countAtt, f.attribute(countTemp)) 120 | countTemp+=1 121 | countAtt+=1 122 | 123 | else: 124 | feat.setAttribute(1, f.attribute(attributeID)) 125 | countAtt+=1 126 | for a in attributes: 127 | feat.setAttribute(countAtt, f.attribute(a)) 128 | countAtt = countAtt + 1 129 | 130 | count = count + 1 131 | featureList.append(feat) 132 | pr.addFeatures(featureList) 133 | vl.commitChanges() 134 | return vl 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /neatmap_about_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | NeatMapAboutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 378 10 | 364 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 11 22 | 23 | 24 | 25 | NeatMap 26 | 27 | 28 | true 29 | 30 | 31 | 32 | 9 33 | 34 | 35 | 9 36 | 37 | 38 | 1 39 | 40 | 41 | 0 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 20 50 | 51 | 52 | 53 | NeatMap : About 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0 64 | 0 65 | 66 | 67 | 68 | Qt::ScrollBarAlwaysOff 69 | 70 | 71 | true 72 | 73 | 74 | 75 | 76 | 0 77 | 0 78 | 358 79 | 312 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | A QGIS plugin to organize and tidy surfacic features according to their shapes. 89 | This code can also be launched with no GUI. 90 | 91 | 92 | Qt::RichText 93 | 94 | 95 | true 96 | 97 | 98 | 99 | 100 | 101 | 102 | <html><head/><body><p><span style=" font-weight:600;">Website</span> (Documentation, tutorial, test data, code execution, issues) :</p><p><a href="https://github.com/IGNF/NeatMap/"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/IGNF/NeatMap</span></a></p></body></html> 103 | 104 | 105 | Qt::RichText 106 | 107 | 108 | true 109 | 110 | 111 | 112 | 113 | 114 | 115 | <html><head/><body><p><span style=" font-weight:600;">Contributors : </span></p><p><span style=" font-weight:600;">- Mickaël Brasebin et Julien Perret </span>(LASTIG / IGN)</p><p><span style=" font-weight:600;">- Rose Mathelier et Bruce Thomas</span>(ENSG)<span style=" font-weight:600;"><br/></span></p></body></html> 116 | 117 | 118 | Qt::RichText 119 | 120 | 121 | true 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /indicatorCalculation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | NeatMAp 5 | A QGIS plugin 6 | A simple QGIS python plugin for building neat maps. 7 | ------------------- 8 | begin : 2016-11-30 9 | git sha : $Format:%H$ 10 | copyright : (C) 2016 - 2018 by IGN 11 | email : julien.perret@gmail.com; mickael.brasebin@ign.fr 12 | ***************************************************************************/ 13 | 14 | /*************************************************************************** 15 | * * 16 | * This program is free software; you can redistribute it and/or modify * 17 | * it under the terms of the GNU General Public License as published by * 18 | * the Free Software Foundation; either version 2 of the License, or * 19 | * (at your option) any later version. * 20 | * * 21 | ***************************************************************************/ 22 | 23 | """ 24 | 25 | import os 26 | from qgis.core import * 27 | from PyQt5.QtCore import QVariant 28 | from .morpho import * 29 | import sys 30 | 31 | 32 | 33 | 34 | 35 | #Calculate a new layer (layerName) from a layer with polygonal features (layerPolygon) with id (idLayerPolygon) and indicates if attribute from initial layer are copied 36 | #Indicator are stored in morpho.py 37 | def calculate(layerName, layerPolygon, idLayerPolygon, copyAttribute): 38 | # A new layer is created 39 | vl = QgsVectorLayer("Polygon", layerName, "memory") 40 | pr = vl.dataProvider() 41 | vl.startEditing() 42 | fields = [ QgsField(idLayerPolygon, QVariant.String), 43 | QgsField("area", QVariant.Double, "Real", 10, 3), 44 | QgsField("SMBR_area", QVariant.Double, "Real",10, 3), 45 | QgsField("SMBR_angle", QVariant.Double, "Real",10, 3), 46 | QgsField("SMBR_w.", QVariant.Double, "Real",10, 3), 47 | QgsField("SMBR_h.", QVariant.Double, "Real",10, 3), 48 | QgsField("convexity1", QVariant.Double, "Real",10, 3), 49 | QgsField("convexity2", QVariant.Double, "Real",10, 3), 50 | QgsField("elongation", QVariant.Double, "Real",10, 3), 51 | QgsField("compact.", QVariant.Double, "Real",10, 3), 52 | QgsField("area/perim", QVariant.Double, "Real",10, 3), 53 | QgsField("complexity", QVariant.Double, "Real",10, 3)] 54 | 55 | # print("fields :" + str(len(fields))) 56 | 57 | 58 | #We copy the intial fields except the fid 59 | fieldsTemp = [] 60 | if copyAttribute : 61 | fieldsTemp = layerPolygon.fields() 62 | for f in fieldsTemp : 63 | if f.name() != idLayerPolygon: 64 | fields.append(f) 65 | #print(f.name()) 66 | 67 | # print("fields :" + str(len(fields))) 68 | 69 | # (const QString &name=QString(), QVariant::Type type=QVariant::Invalid, const QString &typeName=QString(), int len=0, int prec=0, const QString &comment=QString(), QVariant::Type subType=QVariant::Invalid) 70 | pr.addAttributes( fields ) 71 | vl.updateFields() 72 | 73 | featureList = [] 74 | 75 | for f in layerPolygon.getFeatures(): 76 | geom = f.geometry() 77 | 78 | if geom is None: 79 | continue 80 | 81 | area = geom.area() 82 | perimeter = geom.length() 83 | 84 | ident = f.attribute(idLayerPolygon) 85 | 86 | 87 | SMBR_geom, SMBR_area, SMBR_angle, SMBR_width, SMBR_height = geom.orientedMinimumBoundingBox() 88 | convexity1 = compute_convexity1(geom, area) 89 | convexity2 = compute_convexity2(area, SMBR_area) 90 | elongation = compute_elongation(SMBR_height, SMBR_width) 91 | compactness = compute_compactness(area, perimeter) 92 | complexity = compute_complexity(geom) 93 | 94 | 95 | 96 | feat = QgsFeature() 97 | feat.setGeometry( geom ) 98 | feat.initAttributes(len(fields)) 99 | 100 | count = 0; 101 | feat.setAttribute( count, ident) 102 | count += 1 103 | feat.setAttribute( count, area ) 104 | count += 1 105 | feat.setAttribute( count, SMBR_area ) 106 | count += 1 107 | feat.setAttribute( count, SMBR_angle ) 108 | count += 1 109 | feat.setAttribute( count, SMBR_width ) 110 | count += 1 111 | feat.setAttribute( count, SMBR_height ) 112 | count += 1 113 | feat.setAttribute( count, convexity1 ) 114 | count += 1 115 | feat.setAttribute( count, convexity2 ) 116 | count += 1 117 | feat.setAttribute( count, elongation) 118 | count += 1 119 | feat.setAttribute( count, compactness ) 120 | count += 1 121 | feat.setAttribute( count, area/perimeter) 122 | count += 1 123 | feat.setAttribute( count, complexity) 124 | count += 1 125 | 126 | ##If needed copying extra attributes 127 | if copyAttribute : 128 | countTemp = 0; 129 | for field in fieldsTemp : 130 | if field.name() != idLayerPolygon: 131 | feat.setAttribute( count, f.attribute(countTemp)) 132 | count+=1 133 | countTemp += 1 134 | 135 | 136 | featureList.append(feat) 137 | 138 | pr.addFeatures(featureList) 139 | vl.commitChanges() 140 | vl.updateFields() 141 | vl.endEditCommand() 142 | return vl 143 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # (True) => Means that all attributes will be copied 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | 5 | import sys 6 | import os 7 | 8 | PACKAGE_PARENT = '..' 9 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__)))) 10 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT))) 11 | 12 | 13 | from qgis.core import * 14 | from PyQt5.QtCore import QVariant 15 | from NeatMap.indicatorCalculation import * 16 | from NeatMap.classification import * 17 | from NeatMap.square_packing import * 18 | 19 | 20 | 21 | 22 | def run(): 23 | print("Initializing QGS") 24 | # supply path to qgis install location 25 | QgsApplication.setPrefixPath("/usr", True) 26 | 27 | # create a reference to the QgsApplication, setting the second argument to False disables the GUI 28 | qgs = QgsApplication([], False) 29 | 30 | # load providers 31 | qgs.initQgis() 32 | 33 | input_file = os.path.join(SCRIPT_DIR,'data_test/world.shp') 34 | output_dir = os.path.join(SCRIPT_DIR,'data_test/') 35 | print(input_file) 36 | print(output_dir) 37 | 38 | 39 | fid_atribute = "fid" 40 | 41 | layerName = "layer" 42 | 43 | 44 | print("QGIS was init") 45 | 46 | layer_polygons = QgsVectorLayer(input_file, 'polygons', 'ogr') 47 | 48 | #Export layout 49 | crs=QgsCoordinateReferenceSystem("epsg:-1") 50 | 51 | saveOption = QgsVectorFileWriter.SaveVectorOptions() 52 | saveOption.driverName = "ESRI ShapeFile" 53 | saveOption.fileEncoding = "utf-8" 54 | 55 | transformContext = QgsProject.instance().transformContext() 56 | 57 | print("Calculate indicators") 58 | #Step 1 : calculating indicator 59 | # (layerName) => The name of the output layer 60 | # (layer_polygons) => The input layer 61 | # (fid_atribute) => The name of the fid attribute 62 | # (True) => Means that all attributes will be copied 63 | layerOut = calculate(layerName,layer_polygons,fid_atribute, True); 64 | 65 | print("Export indicator shapefile") 66 | #Export features with attributes 67 | error = QgsVectorFileWriter.writeAsVectorFormatV2(layerOut, os.path.join(output_dir,"indicator.shp"), transformContext, saveOption ) 68 | 69 | 70 | #Determining the attribute to use for the classification 71 | attributes = ["area", "elongation" , "compact."] 72 | #Output classification attribute 73 | classAttribute = "class" 74 | 75 | #Step 2 : Applying the classification 76 | print("Classification") 77 | # (layerOut) : the input layer (the output from previous step) 78 | # (attributes) : the list of attributes on which the classificatino will be proceeded 79 | # (10) : the number of classes 80 | # (layerName) : the name of the output layer name 81 | # (classAttribute) : the name of the attribute in which the class will be stored) 82 | # f(id_atribute) => The name of the fid attribute 83 | # (True) => Means that all attributes will be copied 84 | layerClassified = kmeans(layerOut, attributes, 10, layerName, classAttribute, fid_atribute, True) 85 | 86 | print("Export classified layer") 87 | #Export features with classificatinoadvanced_layout 88 | error = QgsVectorFileWriter.writeAsVectorFormatV2(layerClassified, os.path.join(output_dir,"classified.shp"), transformContext, saveOption ) 89 | 90 | 91 | #Step 3 ! Applying a naive layout 92 | #Secondary attribute to sort the feature (descending) 93 | attSecondary = "area" 94 | 95 | print("Preparing layout") 96 | # (layerClassified) : the input layer (the output from previous step) 97 | # (classAttribute) : the name of the attribute in which the class will be stored) 98 | # (attSecondary) : the secondary ranking attribute 99 | # (layerName) : the name of the output layer name 100 | # (True) => Means that all attributes will be copied 101 | newLayoutLayer = naive_layout(layerClassified, classAttribute , attSecondary, layerName, True) 102 | print("Export layout") 103 | #Naive layout 104 | error = QgsVectorFileWriter.writeAsVectorFormatV2(newLayoutLayer, os.path.join(output_dir,"naiveLayout.shp"), transformContext, saveOption ) 105 | 106 | print("Preparing layout2") 107 | #Step 3 bis : other layout method (with the bounding boxes to debug the rectangle packing) 108 | # (layerClassified) : the input layer (the output from previous step) 109 | # (classAttribute) : the name of the attribute in which the class will be stored) 110 | # (attSecondary) : the secondary ranking attribute 111 | # (layerName) : the name of the output layer name 112 | # (True) => Means that all attributes will be copied 113 | otherLayout, layoutBoundingBox = advanced_layout(layerClassified, classAttribute , attSecondary, layerName, True) 114 | 115 | print("Export layout") 116 | #Bounding boxes used for pack layout production 117 | error = QgsVectorFileWriter.writeAsVectorFormatV2(layoutBoundingBox, os.path.join(output_dir,"boundingBox.shp"), transformContext, saveOption ) 118 | 119 | print("Export layout") 120 | #Packed layout 121 | error = QgsVectorFileWriter.writeAsVectorFormatV2(otherLayout, os.path.join(output_dir,"otherLayout.shp"), transformContext, saveOption ) 122 | 123 | print("Preparing layout3") 124 | #Step 3 ter : other layout method, less optimal that in Step 3 bis. The widestbox is placed at first and the other one 125 | #Are placed on this box, according to the x axis etc (like making a wall with bricks) 126 | # (layerClassified) : the input layer (the output from previous step) 127 | # (classAttribute) : the name of the attribute in which the class will be stored) 128 | # (attSecondary) : the secondary ranking attribute 129 | # (layerName) : the name of the output layer name 130 | # (True) => Means that all attributes will be copied 131 | otherLayout2, layoutBoundingBox2 = fast_layout(layerClassified, classAttribute , attSecondary, layerName, True) 132 | 133 | print("Export layout") 134 | #Bounding boxes used for fast layout production 135 | error = QgsVectorFileWriter.writeAsVectorFormatV2(layoutBoundingBox2, os.path.join(output_dir,"boundingBox2.shp"), transformContext, saveOption ) 136 | 137 | print("Export layout") 138 | #Packed layout 139 | error = QgsVectorFileWriter.writeAsVectorFormatV2(otherLayout2, os.path.join(output_dir,"otherLayout2.shp"), transformContext, saveOption ) 140 | 141 | 142 | run() 143 | -------------------------------------------------------------------------------- /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 PyQt4.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 | -------------------------------------------------------------------------------- /morpho.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | NeatMap 5 | A QGIS plugin 6 | A simple QGIS python plugin for building neat maps. 7 | ------------------- 8 | begin : 2016-11-30 9 | git sha : $Format:%H$ 10 | copyright : (C) 2016 - 2018 by IGN 11 | email : julien.perret@gmail.com; mickael.brasebin@ign.fr 12 | ***************************************************************************/ 13 | 14 | /*************************************************************************** 15 | * * 16 | * This program is free software; you can redistribute it and/or modify * 17 | * it under the terms of the GNU General Public License as published by * 18 | * the Free Software Foundation; either version 2 of the License, or * 19 | * (at your option) any later version. * 20 | * * 21 | ***************************************************************************/ 22 | 23 | """ 24 | 25 | from PyQt5.QtGui import QTransform 26 | from qgis.core import QgsField, QgsGeometry, QgsPointXY, QgsRectangle, QgsWkbTypes 27 | import math 28 | 29 | """ 30 | SMBR computation. 31 | """ 32 | def normalizedAngle(angle): 33 | clippedAngle = angle 34 | if ( clippedAngle >= math.pi * 2 or clippedAngle <= -2 * math.pi ): 35 | clippedAngle = math.fmod( clippedAngle, 2 * math.pi) 36 | if ( clippedAngle < 0.0 ): 37 | clippedAngle += 2 * math.pi 38 | return clippedAngle 39 | 40 | def lineAngle(x1, y1, x2, y2): 41 | at = math.atan2( y2 - y1, x2 - x1 ) 42 | a = -at + math.pi / 2.0 43 | return normalizedAngle( a ) 44 | 45 | def compute_SMBR(geom): 46 | area = float("inf") 47 | angle = 0 48 | width = float("inf") 49 | height = float("inf") 50 | if (geom is None): 51 | return QgsGeometry() 52 | hull = geom.convexHull() 53 | if ( hull.isEmpty() ): 54 | return QgsGeometry() 55 | x = hull.asPolygon() 56 | vertexId = 0 57 | pt0 = x[0][vertexId] 58 | pt1 = pt0 59 | prevAngle = 0.0 60 | size = len(x[0]) 61 | for vertexId in range(0, size-0): 62 | pt2 = x[0][vertexId] 63 | currentAngle = lineAngle( pt1.x(), pt1.y(), pt2.x(), pt2.y() ) 64 | rotateAngle = 180.0 / math.pi * (currentAngle - prevAngle) 65 | prevAngle = currentAngle 66 | t = QTransform.fromTranslate( pt0.x(), pt0.y() ) 67 | t.rotate(rotateAngle) 68 | t.translate( -pt0.x(), -pt0.y() ) 69 | hull.transform(t) 70 | bounds = hull.boundingBox() 71 | currentArea = bounds.width() * bounds.height() 72 | if ( currentArea < area ): 73 | minRect = bounds 74 | area = currentArea 75 | angle = 180.0 / math.pi * currentAngle 76 | width = bounds.width() 77 | height = bounds.height() 78 | pt2 = pt1 79 | minBounds = QgsGeometry.fromRect( minRect ) 80 | minBounds.rotate( angle, QgsPointXY( pt0.x(), pt0.y() ) ) 81 | if ( angle > 180.0 ): 82 | angle = math.fmod( angle, 180.0 ) 83 | return minBounds, area, angle, width, height 84 | 85 | def m(c,i,g): 86 | attr = c.attribute(i) 87 | area = c.geometry().intersection(g).area() 88 | return (attr,area) 89 | 90 | def find_areas(geom, index, dictionary, idAttribute): 91 | return [m(candidate, idAttribute, geom) 92 | for candidate in 93 | map(lambda id:dictionary[id], index.intersects(geom.boundingBox())) 94 | if candidate.geometry().intersects(geom)] 95 | 96 | def findIRIS_line(geom,layer_IRIS,nom_idIRIS): 97 | intersections = [] 98 | for iris in layer_IRIS.getFeatures(): 99 | if iris.geometry().intersects(geom): 100 | intersections.append([iris.attribute(nom_idIRIS),iris.geometry().intersection(geom).length()]) 101 | iris_id = 0 102 | length_max = 0 103 | for element in intersections: 104 | if element[1]>length_max: 105 | iris_id = element[0] 106 | length_max = element[1] 107 | return iris_id 108 | 109 | 110 | def findIRIS(geom,layer_IRIS,nom_idIRIS): 111 | intersections = findIRIS_areas(geom,layer_IRIS,nom_idIRIS) 112 | iris_id = 0 113 | aire_max = 0 114 | for element in intersections: 115 | if element[1]>aire_max: 116 | iris_id = element[0] 117 | aire_max = element[1] 118 | return iris_id 119 | 120 | def find(geom, index, dictionary, idAttribute): 121 | intersections = find_areas(geom, index, dictionary, idAttribute) 122 | return max(intersections, key=lambda x: x[1])[0] 123 | 124 | def distance_from_polygon_to_layer(geom, index, dictionary, layer_id): 125 | #Centroid of input buildings 126 | point = geom.pointOnSurface().asPoint() 127 | #Cprint(point.asWkt()) 128 | distance = dictionary[index.nearestNeighbor(point,1)[0]].geometry().distance(geom) 129 | 130 | #Cprint(distance) 131 | bbox = geom.buffer(distance*1.5,3).boundingBox() 132 | #Cprint(bbox.asWktPolygon()) 133 | 134 | return min( 135 | ((f.geometry().distance(geom), f.attribute(layer_id)) 136 | for f in map(lambda id: dictionary[id], index.intersects(bbox))), 137 | key=lambda x: x[0]) 138 | 139 | def compute_elongation(d1, d2): 140 | """ 141 | Calcul de l'élongation. 142 | """ 143 | elongation = min(d1,d2)/max(d1,d2) 144 | return elongation 145 | 146 | def compute_compactness(area, perimeter): 147 | """ 148 | Calcul de la compacité. 149 | """ 150 | return 4 * math.pi * area / (perimeter * perimeter) 151 | 152 | 153 | def complexityPolygon(geom): 154 | 155 | 156 | return len(geom)-1 157 | 158 | def compute_complexity(geom): 159 | type = geom.wkbType() 160 | if (type == QgsWkbTypes.MultiPolygon): # new part for multipolylines 161 | multiP = geom.asMultiPolygon() 162 | count = 0 163 | for v in multiP: 164 | count = count + complexityPolygon(v) 165 | return count 166 | elif (type == QgsWkbTypes.Polygon): 167 | polygon = geom.asPolygon() 168 | count = 0 169 | for v in polygon : 170 | count = count + complexityPolygon(v) 171 | return count 172 | 173 | return 0 174 | 175 | def compute_convexity1(geom, area): 176 | """ 177 | Calcul de la convexité selon l'enveloppe convexe. 178 | """ 179 | convexhull = geom.convexHull() 180 | convexity1 = area/convexhull.area() 181 | return convexity1 182 | 183 | def compute_convexity2(area, SMBR_area): 184 | """ 185 | Calcul de la convexité selon le SMBR. 186 | """ 187 | convexity2 = area/SMBR_area 188 | return convexity2 189 | 190 | def compute_formFactor(hauteur, SMBR_width, SMBR_height): 191 | """ 192 | Calcul du facteur de forme 193 | """ 194 | formFactor = 2*hauteur/(SMBR_width+SMBR_height) 195 | return formFactor 196 | 197 | def compute_formIndice(hauteur, area): 198 | """ 199 | Calcul de l'indice de forme 200 | """ 201 | formIndice = hauteur**2 / area 202 | return formIndice 203 | -------------------------------------------------------------------------------- /help/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # NeatMap 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.pngmath', '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'NeatMap' 44 | copyright = u'2018, IGN' 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', 'NeatMap.tex', u'NeatMap Documentation', 182 | u'IGN', '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'NeatMap Documentation', 215 | [u'IGN'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #/*************************************************************************** 2 | # Neatmp 3 | # 4 | # A simple QGIS python plugin for building neat cities. 5 | # ------------------- 6 | # begin : 2016-11-30 7 | # git sha : $Format:%H$ 8 | # copyright : (C) 2016 by IGN 9 | # email : julien.perret@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 | neatmap.py neatmap_dialog.py neatmap_about_dialog.py app.py classification.py get-pip.py indicatorCalculation.py morpho.py square_packing.py 42 | 43 | PLUGINNAME = NeatMap 44 | 45 | PY_FILES = \ 46 | __init__.py \ 47 | neatmap.py neatmap_dialog.py neatmap_about_dialog.py app.py classification.py get-pip.py indicatorCalculation.py morpho.py square_packing.py 48 | 49 | UI_FILES = neatmap_dialog_base.ui neatmap_about_dialog.ui 50 | 51 | EXTRAS = metadata.txt icon.png 52 | 53 | EXTRA_DIRS = data_test 54 | 55 | COMPILED_RESOURCE_FILES = resources.py 56 | 57 | PEP8EXCLUDE=pydev,resources.py,conf.py,third_party,ui 58 | 59 | 60 | ################################################# 61 | # Normally you would not need to edit below here 62 | ################################################# 63 | 64 | HELP = help/build/html 65 | 66 | PLUGIN_UPLOAD = $(c)/plugin_upload.py 67 | 68 | RESOURCE_SRC=$(shell grep '^ *@@g;s/.*>//g' | tr '\n' ' ') 69 | 70 | QGISDIR=.qgis2 71 | 72 | default: compile 73 | 74 | compile: $(COMPILED_RESOURCE_FILES) 75 | 76 | %.py : %.qrc $(RESOURCES_SRC) 77 | pyrcc5 -o $*.py $< 78 | 79 | %.qm : %.ts 80 | $(LRELEASE) $< 81 | 82 | test: compile transcompile 83 | @echo 84 | @echo "----------------------" 85 | @echo "Regression Test Suite" 86 | @echo "----------------------" 87 | 88 | @# Preceding dash means that make will continue in case of errors 89 | @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ 90 | export QGIS_DEBUG=0; \ 91 | export QGIS_LOG_FILE=/dev/null; \ 92 | nosetests -v --with-id --with-coverage --cover-package=. \ 93 | 3>&1 1>&2 2>&3 3>&- || true 94 | @echo "----------------------" 95 | @echo "If you get a 'no module named qgis.core error, try sourcing" 96 | @echo "the helper script we have provided first then run make test." 97 | @echo "e.g. source run-env-linux.sh ; make test" 98 | @echo "----------------------" 99 | 100 | deploy: compile doc transcompile 101 | @echo 102 | @echo "------------------------------------------" 103 | @echo "Deploying plugin to your .qgis2 directory." 104 | @echo "------------------------------------------" 105 | # The deploy target only works on unix like operating system where 106 | # the Python plugin directory is located at: 107 | # $HOME/$(QGISDIR)/python/plugins 108 | mkdir -p $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 109 | cp -vf $(PY_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 110 | cp -vf $(UI_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 111 | cp -vf $(COMPILED_RESOURCE_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 112 | cp -vf $(EXTRAS) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 113 | cp -vfr i18n $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 114 | cp -vfr $(HELP) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)/help 115 | # Copy extra directories if any 116 | # (temporarily removed) 117 | 118 | 119 | # The dclean target removes compiled python files from plugin directory 120 | # also deletes any .git entry 121 | dclean: 122 | @echo 123 | @echo "-----------------------------------" 124 | @echo "Removing any compiled python files." 125 | @echo "-----------------------------------" 126 | find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname "*.pyc" -delete 127 | find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname ".git" -prune -exec rm -Rf {} \; 128 | 129 | 130 | derase: 131 | @echo 132 | @echo "-------------------------" 133 | @echo "Removing deployed plugin." 134 | @echo "-------------------------" 135 | rm -Rf $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) 136 | 137 | zip: deploy dclean 138 | @echo 139 | @echo "---------------------------" 140 | @echo "Creating plugin zip bundle." 141 | @echo "---------------------------" 142 | # The zip target deploys the plugin and creates a zip file with the deployed 143 | # content. You can then upload the zip file on http://plugins.qgis.org 144 | rm -f $(PLUGINNAME).zip 145 | cd $(HOME)/$(QGISDIR)/python/plugins; zip -9r $(CURDIR)/$(PLUGINNAME).zip $(PLUGINNAME) 146 | 147 | package: compile 148 | # Create a zip package of the plugin named $(PLUGINNAME).zip. 149 | # This requires use of git (your plugin development directory must be a 150 | # git repository). 151 | # To use, pass a valid commit or tag as follows: 152 | # make package VERSION=Version_0.3.2 153 | @echo 154 | @echo "------------------------------------" 155 | @echo "Exporting plugin to zip package. " 156 | @echo "------------------------------------" 157 | rm -f $(PLUGINNAME).zip 158 | git archive --prefix=$(PLUGINNAME)/ -o $(PLUGINNAME).zip $(VERSION) 159 | echo "Created package: $(PLUGINNAME).zip" 160 | 161 | upload: zip 162 | @echo 163 | @echo "-------------------------------------" 164 | @echo "Uploading plugin to QGIS Plugin repo." 165 | @echo "-------------------------------------" 166 | $(PLUGIN_UPLOAD) $(PLUGINNAME).zip 167 | 168 | transup: 169 | @echo 170 | @echo "------------------------------------------------" 171 | @echo "Updating translation files with any new strings." 172 | @echo "------------------------------------------------" 173 | @chmod +x scripts/update-strings.sh 174 | @scripts/update-strings.sh $(LOCALES) 175 | 176 | transcompile: 177 | @echo 178 | @echo "----------------------------------------" 179 | @echo "Compiled translation files to .qm files." 180 | @echo "----------------------------------------" 181 | @chmod +x scripts/compile-strings.sh 182 | @scripts/compile-strings.sh $(LRELEASE) $(LOCALES) 183 | 184 | transclean: 185 | @echo 186 | @echo "------------------------------------" 187 | @echo "Removing compiled translation files." 188 | @echo "------------------------------------" 189 | rm -f i18n/*.qm 190 | 191 | clean: 192 | @echo 193 | @echo "------------------------------------" 194 | @echo "Removing uic and rcc generated files" 195 | @echo "------------------------------------" 196 | rm $(COMPILED_UI_FILES) $(COMPILED_RESOURCE_FILES) 197 | 198 | doc: 199 | @echo 200 | @echo "------------------------------------" 201 | @echo "Building documentation using sphinx." 202 | @echo "------------------------------------" 203 | cd help; make html 204 | 205 | pylint: 206 | @echo 207 | @echo "-----------------" 208 | @echo "Pylint violations" 209 | @echo "-----------------" 210 | @pylint --reports=n --rcfile=pylintrc . || true 211 | @echo 212 | @echo "----------------------" 213 | @echo "If you get a 'no module named qgis.core' error, try sourcing" 214 | @echo "the helper script we have provided first then run make pylint." 215 | @echo "e.g. source run-env-linux.sh ; make pylint" 216 | @echo "----------------------" 217 | 218 | 219 | # Run pep8 style checking 220 | #http://pypi.python.org/pypi/pep8 221 | pep8: 222 | @echo 223 | @echo "-----------" 224 | @echo "PEP8 issues" 225 | @echo "-----------" 226 | @pep8 --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128 --exclude $(PEP8EXCLUDE) . || true 227 | @echo "-----------" 228 | @echo "Ignored in PEP8 check:" 229 | @echo $(PEP8EXCLUDE) 230 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Icon of the project](../image_doc/doc_images/icon.png?raw=true) 2 | 3 | 4 | NeatMap 5 | ============ 6 | [![Build Status](https://travis-ci.org/IGNF/NeatMap.svg?branch=master)](https://travis-ci.org/IGNF/NeatMap) 7 | 8 | NeatMap is a plugin for [QGIS](https://www.qgis.org/fr/site/) that allows the production of an "organized" set of polygon features according to their shapes. In order to proceed, three steps are necessary : 1/ morphological indicators calculation, 2/ classification of the polygon features using these indicators and 3/ generation of the disposition of the features. 9 | 10 | The project is developed as an Open-Source library based on : 11 | - [QGIS API V3.0](https://www.qgis.org/fr/site/), for morphological operators and layout generation ; 12 | - [Scikit learn 0.19.1](http://scikit-learn.org/stable/index.html) : for the classification. Normally, the installation of thie library is automatic but if it fails, please refer to the [installation guide](https://scikit-learn.org/stable/install.html). 13 | 14 | 15 | ![Sample of result](../image_doc/doc_images/sample.png?raw=true) 16 | 17 | Introduction 18 | --------------------- 19 | 20 | This research library has been initiated during a [ENSG](http://www.ensg.eu) student work and continues with [COGIT team](http://recherche.ign.fr/labos/cogit/accueilCOGIT.php) research developments. 21 | 22 | The layout idea was based on the artistic work of [Armel Caron : tidy cities](http://www.armellecaron.fr/works/les-villes-rangees/) and the aim is to question the ability of automatic algorithms to generate similar layouts. The idea is not to produce on-demand art, but to asses the expressivity of common morphological indicators to discriminate polygon shapes and the part of subjectivity in the original artistic productions. 23 | 24 | 25 | General principle 26 | --------------------- 27 | 28 | ![Schema of the worflow of the NeatMap Plugin](../image_doc/doc_images/rankingPrinciple.png?raw=true) 29 | 30 | 31 | The general idea of the approach is to regroup the feature with similar characteristics into groups and to correctly arrange these groups. These characteristics are assessed through a set of morphological indicators and the groups are created with a classification method. Inside the group, an attribute is used to rank the different features (for example, the area in the image presenting the principle). Each group can be modeled as a rectangle and the aim of the layout generator is to find an optimal way to arrange these rectangles. 32 | 33 | 34 | The workflow 35 | --------------------- 36 | ![Schema of the worflow of the NeatMap Plugin](../image_doc/doc_images/generalSchema.png?raw=true) 37 | 38 | 39 | 40 | The plugin is composed of 3 steps : 41 | - (1) the calculation of common morphological indicators on a polygon layer ; 42 | - (2) the classification through a k-mean method ; 43 | - (3) the generation of classified layouts. 44 | If these three steps are consecutive, they can be run independently if a layer with the relevant attributes is provided. For example, if you have produced your own indicators, you can directly proceed to the classification or if your features are already classified to generate a layout with them. 45 | 46 | 47 | 48 | Plugin installation 49 | --------------------- 50 | The only requirement is to get QGIS 3.3 or later installed. You can download it from [QGIS official website](https://www.qgis.org/fr/site/). 51 | 52 | Currently the plugin is on QGIS repositories as an experimental plugin [Neatmap page on QGIS repo](http://plugins.qgis.org/plugins/NeatMap/). 53 | 54 | You can also install it manually. You have to download the [automatically generated zip file](https://github.com/IGNF/NeatMap/archive/master.zip) and to unzip it in the plugins folder (https://gis.stackexchange.com/questions/274311/qgis-3-plugin-folder-location). 55 | 56 | In QGIS, in the "Extenion" menu > "Install Extension" option, you have just to activate the "NeatMap" plugin. 57 | 58 | **NOTE** : if the plugin is mising, you may have to allow experimental plugins, in the parameter menu. 59 | 60 | GUI of the plugin 61 | --------------------- 62 | 63 | The GUI of the plugin is basically composed of three parts that reflect the three steps of the workflow (indicator calculation, classification and layout generation). Each part has its own options and one "Ok" button that allows to run separately the different steps. 64 | 65 | **Note : the option "Copy attributes between two steps" is a general option. It allows to keep the attribute of the layer between two steps and works for each step.** 66 | 67 | 68 | ![GUI of the plugin](../image_doc/doc_images/generalGUI.png?raw=true) 69 | 70 | 71 | Indicators Calculation 72 | --------------------- 73 | This steps aims at calculating morphological indicators from a polygon layer. 74 | 75 | It requires as input : 76 | - **Input layer** : The name of a polygon layer with an ID field ; 77 | - **Output layer name** : The name of the output layer ; 78 | - **ID attribute** : The name of the attribute field. 79 | 80 | By clicking on the "Ok" button of this section, a new layer with the following indicators is produced and added in QGIS : 81 | - **area** : the area of the geometry ; 82 | - **SMBR_area** : the area of the smallest bounding rectangle ; 83 | - **SMBR_angle** : the orientation of the smallest bounding rectangle ; 84 | - **SMBR_h** : the height of the smallest bounding rectangle ; 85 | - **SMBR_w** : the width of the smallest bounding rectangle ; 86 | - **convexity1** : convexity relative to the convex hull ; 87 | - **convexity2** : convexity relative to the smallest bounding rectangle ; 88 | - **elongation** : elongation of the building (between 0 and 1) ; 89 | - **compact** : compactness of the polygon ; 90 | - **area/perim** : ratio between area and perimeter ; 91 | - **complexity** : the number of segments of the polygon. 92 | 93 | Classification 94 | --------------------- 95 | This step determines different classes of features through a k-mean classification method. 96 | 97 | It requires as input : 98 | - **Input layer** : The name of a polygon layer with an ID field ; 99 | - **ID attribute** : The name of the attribute field ; 100 | - In the central panel, a list of numerical attributes can be selected. You can use the indicators calculated during the previsous step, your own indicators or a combination of both. During the process, the attribute values are scaled between 0 and 1; 101 | - **Number of classes** : the number of classes in which the features will be classified. This parameter is very important as it defines the number of groups used to produce the final layout ; 102 | - **Classification attribute** : a new attribute that stored the identifiant of the different classes ; 103 | - **Output layer name** : The name of the output layer. 104 | 105 | By clicking on the "Ok" button of the section, a new layer will be produced. It contains a new attribute that informs about the class of the features and the attributes used for classification. 106 | 107 | An example of classification of the world countries with 10 classes : 108 | 109 | ![Map of classified countries](../image_doc/doc_images/classified.png?raw=true) 110 | 111 | 112 | Layout generation 113 | --------------------- 114 | This step aims at generating a layout according to a classification attribute (you can use your own classification made by an other method or the one produced by the preivous step). 115 | 116 | The required information are : 117 | - The type of algorithm : two layout algorithm are available ; 118 | - ***Input layer*** : The name of a 1perline.png layer with an ID field ; 119 | - ***Classification attribute*** : The name of the attribute where the classes are described (this attribute can be generated from the previous step or an other method) ; 120 | - ***Secondary ranking attribute*** : The attribute that classify the polygon among a class (refer to "Principle section). This attribute must be numerical. 121 | - **Output layer name** : The name of the output layer. 122 | 123 | By clicking on the "Ok" button of the section, a new layer will be produced, with a new layout according to the choosen method, the layer has the same CRS as the input one 124 | 125 | ### 1 line per class 126 | 127 | ![Layout with one class per line](../image_doc/doc_images/1perline.png?raw=true) 128 | 129 | The first method is very basic and generate a layout for which each class is on a line and the different features follow the line ordered according to the secondary attribute. 130 | 131 | 132 | ### Chazelle packing method 133 | 134 | This method aims at producing the layout that is contained into the minimal possible rectangle. As each class is contained into a bounding rectangle, this problem is of the class of rectangle packing problems. In our implementation, we use the Chazelle (1983) heuristics to packing the rectangle of the classes. 135 | 136 | *Chazelle, B., 1983, The Bottom-left Bin-packing Heuristic : An Efficient Implementation, IEEE Transactions on Computers, Volume 32 Issue 8, August 1983 Pages 697-707* 137 | 138 | The following image presents the application of the Chazelle method on the rectangle of each class from the previous classification and their related features. 139 | 140 | ![Application of Chazelle Method on rectangle with feature](../image_doc/doc_images/chazelleMethod.png?raw=true) 141 | 142 | As the occupation of the layout is not optimal, we implemented an extension method that increases the width of the rectangle and applies the transformation on their related features. 143 | 144 | ![Application of the extension of the features](../image_doc/doc_images/extendMethod.png?raw=true) 145 | 146 | **Warning** : this method produces optimized results but is very time consuming, we do not advice to use this method when the number of class is greater than 15. 147 | 148 | ### Simple method 149 | 150 | This method is similar than the previons one. The method is very fast but the results are less optimized in terms of layout area. The extent width is determined according to the widest class. Then the method is a greedy placement by decreasing width. 151 | 152 | A post-process phase is applied once all classes are placed and extend the extent of the different classes until it is possible (i.e. it does not intersect an other class extent). Then the objects that belongs to this class are added and horizontally distributed in the class extent. 153 | 154 | Standalone use of the code 155 | --------------------- 156 | The code can be used as a standalone application, globally the code is splitted into several python files : 157 | - *morpho.py* : the file that contain the calculation of the morphological indicators ; 158 | - *indicatorCalculation.py* : the file that create new layers with the morphological indicators ; 159 | - *classification.py* : the file that contains the feature classificatino with scikit-learn ; 160 | - *square_packing.py* : this file contains the method to produce the output layout ; 161 | - *neatmap.py* : the code of the QGIS plugin 162 | 163 | The following script shows how to use the code through the most important functions. 164 | The full code is available in the file *app.py*. It can be directly run as it with data from data_test directory. 165 | 166 | ```python3 167 | layer_polygons = QgsVectorLayer(os.path.join(input_dir,'world.shp'), 'polygons', 'ogr') 168 | 169 | #Export layout 170 | crs=QgsCoordinateReferenceSystem("epsg:-1") 171 | 172 | #Step 1 : calculating indicator 173 | # (layerName) => The name of the output layer 174 | # (layer_polygons) => The input layer 175 | # (fid_atribute) => The name of the fid attribute 176 | # (True) => Means that all attributes will be copied 177 | layerOut = calculate(layerName,layer_polygons,fid_atribute, True); 178 | 179 | #Export features with attributes 180 | error = QgsVectorFileWriter.writeAsVectorFormat(layerOut, os.path.join(output_dir,"indicator.shp"),"utf-8", layerOut.crs(), "ESRI Shapefile") 181 | 182 | 183 | #Determining the attribute to use for the classification 184 | attributes = ["area", "elongation" , "compact."] 185 | #Output classification attribute 186 | classAttribute = "class" 187 | 188 | #Step 2 : Applying the classification 189 | # (layerOut) : the input layer (the output from previous step) 190 | # (attributes) : the list of attributes on which the classification will be proceeded 191 | # (10) : the number of classes 192 | # (layerName) : the name of the output layer name 193 | # (classAttribute) : the name of the attribute in which the class will be stored) 194 | # f(id_atribute) => The name of the fid attribute 195 | # (True) => Means that all attributes will be copied 196 | layerClassified = kmeans(layerOut, attributes, 10, layerName, classAttribute, fid_atribute, True) 197 | 198 | #Export features with classificatinoadvanced_layout 199 | error = QgsVectorFileWriter.writeAsVectorFormat(layerClassified, os.path.join(output_dir,"classified.shp"),"utf-8", layerClassified.crs(), "ESRI Shapefile") 200 | 201 | 202 | #Step 3 ! Applying a naive layout 203 | #Secondary attribute to sort the feature (descending) 204 | attSecondary = "area" 205 | 206 | # (layerClassified) : the input layer (the output from previous step) 207 | # (classAttribute) : the name of the attribute in which the class will be stored) 208 | # (attSecondary) : the secondary ranking attribute 209 | # (layerName) : the name of the output layer name 210 | # (True) => Means that all attributes will be copied 211 | newLayoutLayer = naive_layout(layerClassified, classAttribute , attSecondary, layerName, True) 212 | 213 | #Naive layout 214 | error = QgsVectorFileWriter.writeAsVectorFormat(newLayoutLayer, os.path.join(output_dir,"naiveLayout.shp"),"utf-8", crs, "ESRI Shapefile") 215 | 216 | 217 | #Step 3 bis : other layout method (with the bounding boxes to debug the rectangle packing) 218 | # (layerClassified) : the input layer (the output from previous step) 219 | # (classAttribute) : the name of the attribute in which the class will be stored) 220 | # (attSecondary) : the secondary ranking attribute 221 | # (layerName) : the name of the output layer name 222 | # (True) => Means that all attributes will be copied 223 | otherLayout, layoutBoundingBox = advanced_layout(layerClassified, classAttribute , attSecondary, layerName, True) 224 | 225 | #Bounding boxes used for pack layout production 226 | error = QgsVectorFileWriter.writeAsVectorFormat(layoutBoundingBox, os.path.join(output_dir,"boundingBox.shp"),"utf-8", crs, "ESRI Shapefile") 227 | 228 | 229 | #Packed layout 230 | error = QgsVectorFileWriter.writeAsVectorFormat(otherLayout, os.path.join(output_dir,"otherLayout.shp"),"utf-8", crs, "ESRI Shapefile") 231 | 232 | #Step 3 ter : other layout method, less optimal that in Step 3 bis. The widestbox is placed at first and the other one 233 | #Are placed on this box, according to the x axis etc (like making a wall with bricks) 234 | # (layerClassified) : the input layer (the output from previous step) 235 | # (classAttribute) : the name of the attribute in which the class will be stored) 236 | # (attSecondary) : the secondary ranking attribute 237 | # (layerName) : the name of the output layer name 238 | # (True) => Means that all attributes will be copied 239 | otherLayout2, layoutBoundingBox2 = fast_layout(layerClassified, classAttribute , attSecondary, layerName, True) 240 | 241 | #Bounding boxes used for fast layout production 242 | error = QgsVectorFileWriter.writeAsVectorFormat(layoutBoundingBox2, os.path.join(output_dir,"boundingBox2.shp"),"utf-8", crs, "ESRI Shapefile") 243 | 244 | 245 | #Packed layout 246 | error = QgsVectorFileWriter.writeAsVectorFormat(otherLayout2, os.path.join(output_dir,"otherLayout2.shp"),"utf-8", crs, "ESRI Shapefile") 247 | 248 | qgs.exitQgis() 249 | 250 | ``` 251 | 252 | Data samples 253 | --------------------- 254 | * [World countries](https://github.com/IGNF/NeatMap/raw/image_doc/data_sample/world.tar.gz) : the intial dataset is available on [Thematic Mapping Website](http://thematicmapping.org/downloads/world_borders.php) but multi-polygon features are splitted into polygon features. 255 | * [Pars blocks](https://github.com/IGNF/NeatMap/raw/image_doc/data_sample/paris_block.tar.gz) : this is an extract of Paris blocks generated by agregation of a small buffer from the [French Open-Source Cadastre Dataset](https://www.data.gouv.fr/fr/datasets/cadastre/) 256 | 257 | If you have somer interesting datasets that suit well with the approach do not hesitate to contact us (and if you produce some interesting results). 258 | 259 | Conditions for use 260 | --------------------- 261 | This software is free to use under licensed under the GPL version 3.0 or greater. However, if you use this library in a research paper, you are kindly requested to acknowledge the use of this software. 262 | 263 | Furthermore, we are interested in every feedbacks about this library if you find it useful, if you want to contribute or if you have some suggestions to improve it. 264 | 265 | 266 | 267 | Contact for feedbacks 268 | --------------------- 269 | [Mickaël Brasebin](https://mbrasebin.github.io) & [Julien Perret](http://recherche.ign.fr/labos/cogit/cv.php?prenom=Julien&nom=Perret) 270 | [LASTIG Laboratory](http://recherche.ign.fr/) 271 | 272 | Contributors 273 | --------------------- 274 | * Rose Mathelier and Bruce Thomas from ENSG. Development of a very first version with static classification method and simple layout. 275 | 276 | 277 | Future developments 278 | --------------------- 279 | - Using other classification technics as lots of different ones are implemented in [Scikit learn 0.19.1](http://scikit-learn.org/stable/) 280 | 281 | 282 | Troubleshootings 283 | --------------------- 284 | - *Error during indicators calculation step* : you need to use a layer with polygon (and not multi-polygon), non-empty and well formed geometries 285 | -------------------------------------------------------------------------------- /neatmap_dialog_base.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | NeatMapDialogBase 4 | 5 | 6 | 7 | 0 8 | 0 9 | 359 10 | 781 11 | 12 | 13 | 14 | NeatMap 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 20 24 | 25 | 26 | 27 | NeatMap 28 | 29 | 30 | 31 | 32 | 33 | 34 | About 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Qt::ScrollBarAlwaysOn 44 | 45 | 46 | true 47 | 48 | 49 | 50 | 51 | 0 52 | 0 53 | 451 54 | 761 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Copy attributes between two steps 64 | 65 | 66 | false 67 | 68 | 69 | 70 | 71 | 72 | 73 | 1/ Indicators Calculation 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 0 83 | 25 84 | 85 | 86 | 87 | Input layer : 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 0 96 | 25 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 0 110 | 20 111 | 112 | 113 | 114 | Output layer name : 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 0 123 | 25 124 | 125 | 126 | 127 | indicator_layer 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 0 140 | 20 141 | 142 | 143 | 144 | ID attribute : 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 0 153 | 25 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | Ok 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 2/ Classification 174 | 175 | 176 | 177 | 178 | 179 | true 180 | 181 | 182 | 183 | 184 | 0 185 | 0 186 | 403 187 | 73 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 0 200 | 25 201 | 202 | 203 | 204 | Classification attribute : 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 0 213 | 25 214 | 215 | 216 | 217 | class 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Ok 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | Output layer name : 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 0 244 | 25 245 | 246 | 247 | 248 | classified_layer 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | Input layer : 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 0 268 | 25 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 0 282 | 20 283 | 284 | 285 | 286 | ID attribute : 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 0 295 | 25 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | Number of classes : 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 0 316 | 0 317 | 318 | 319 | 320 | 321 | 0 322 | 25 323 | 324 | 325 | 326 | 3 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 3/ Layout positionning 339 | 340 | 341 | 342 | 343 | 344 | 345 | 1 line per class 346 | 347 | 348 | 349 | 350 | Fast simple method 351 | 352 | 353 | 354 | 355 | Chazelle method (may be long for more than 15 classes) 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | Input layer : 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 0 376 | 25 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | Classification attribute : 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 0 397 | 25 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | Secondary ranking attribute : 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 0 418 | 25 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | Output layer name : 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 0 439 | 25 440 | 441 | 442 | 443 | neat_map_layer 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | Ok 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | -------------------------------------------------------------------------------- /square_packing.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from PyQt5.QtCore import QVariant 4 | from .morpho import * 5 | from qgis.core import * 6 | 7 | """ 8 | /*************************************************************************** 9 | NeatMap 10 | A QGIS plugin 11 | A simple QGIS python plugin for building tidy cities. 12 | ------------------- 13 | begin : 2016-11-30 14 | git sha : $Format:%H$ 15 | copyright : (C) 2016 - 2018 by IGN 16 | email : julien.perret@gmail.com; mickael.brasebin@ign.fr 17 | ***************************************************************************/ 18 | 19 | /*************************************************************************** 20 | * * 21 | * This program is free software; you can redistribute it and/or modify * 22 | * it under the terms of the GNU General Public License as published by * 23 | * the Free Software Foundation; either version 2 of the License, or * 24 | * (at your option) any later version. * 25 | * * 26 | ***************************************************************************/ 27 | 28 | """ 29 | 30 | """ 31 | Structures and convention used in the code 32 | 33 | Variable named : boundingBox (QGSFeatureList, width, height, area) 34 | Variable named : rectangle (QGSFeatureList, x, y, width, height, area) 35 | Variable named : vertex (x,y) 36 | 37 | """ 38 | 39 | 40 | 41 | 42 | 43 | 44 | """ 45 | Layout methods 46 | """ 47 | 48 | 49 | 50 | #Basic method : 1 line by class 51 | def naive_layout(vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, copyAtt): 52 | #Transforming feature to rectangles on a same line 53 | boundingBox_tuples, fields = initialise_layout(vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, copyAtt) 54 | #Initializing new layer 55 | vl = QgsVectorLayer("Polygon", outputLayerName, "memory") 56 | pr = vl.dataProvider() 57 | #Getting fields for the layer (the feature are initialized) 58 | #fields = boundingBox_tuples[0][0].fields() 59 | #Update 60 | pr.addAttributes(fields) 61 | vl.updateFields() 62 | #List of feature for the vectorlayer 63 | featureList = [] 64 | #We only apply a y translation on the rectangle 65 | current_y = 0 66 | 67 | #For each rectangle 68 | for boundingBox in boundingBox_tuples: 69 | #We get the list of corresponding feature 70 | featureListTemp = boundingBox[0] 71 | #We translate the geometry and update current_y 72 | for feature in featureListTemp: 73 | geometry = feature.geometry() 74 | geometry.translate(0, current_y + boundingBox[2]/2 ) 75 | feature.setGeometry(geometry) 76 | featureList.append(feature) 77 | current_y = current_y + boundingBox[2] 78 | 79 | 80 | #Commit changes 81 | pr.addFeatures(featureList) 82 | vl.commitChanges() 83 | 84 | return vl 85 | 86 | 87 | def advanced_layout(vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, copyAtt): 88 | #1- We generate a basic layout with no placement (1 bounding box = 1 class) 89 | boundingBox_tuples, fields = initialise_layout(vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, copyAtt) 90 | #2 - Determining the possible bounding boxes ordered by area 91 | minimumBoundingBoxes = minimumBoundingBox(boundingBox_tuples) 92 | #2 - Packing the bounding box into the minimumBounding box b with smallest area 93 | rectngle_tuple, b = pack(boundingBox_tuples, minimumBoundingBoxes, 0) 94 | 95 | #3 - Extend pack rectangles 96 | extendRectangleTuple(rectngle_tuple, b) 97 | 98 | # can be transformed into VectorLayer with => fromPlaceRectangleToVectorLayer(rectngle_tuple) 99 | #3 - Displacing the geographic feature 100 | vl = movingFeature(rectngle_tuple, vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, fields) 101 | return vl, fromPlaceRectangleToVectorLayer(rectngle_tuple) 102 | 103 | 104 | def fast_layout(vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, copyAtt): 105 | #1- We generate a basic layout with no placement (1 bounding box = 1 class) 106 | boundingBox_tuples, fields = initialise_layout(vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, copyAtt) 107 | #2 - Determining a unique possible boundingBow with as width the widthest box and the sum of all heights 108 | minimumBoundingBoxes = minimumUniqueBoundingBox(boundingBox_tuples) 109 | #2 - Packing the bounding box into the minimumBounding box b with smallest area 110 | rectngle_tuple, b = pack(boundingBox_tuples, minimumBoundingBoxes, 1) 111 | 112 | #3 - Extend pack rectangles 113 | extendRectangleTuple(rectngle_tuple, b) 114 | 115 | # can be transformed into VectorLayer with => fromPlaceRectangleToVectorLayer(rectngle_tuple) 116 | #3 - Displacing the geographic feature 117 | vl = movingFeature(rectngle_tuple, vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, fields) 118 | return vl, fromPlaceRectangleToVectorLayer(rectngle_tuple) 119 | 120 | 121 | 122 | 123 | 124 | #def equal(rectangle1, rectangle2): 125 | # return (rectangle1[1] == rectangle2[1]) and (rectangle1[2] == rectangle2[2]) and (rectangle1[3] == rectangle2[3]) and (rectangle1[4] == rectangle2[4]) 126 | """ 127 | Secondary methods 128 | """ 129 | 130 | 131 | #Basic method that generates the bounding box for the different classes 132 | #Rotate the feature according to their orientation 133 | # 134 | def initialise_layout(vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, copyAtt): 135 | # provide file name index and field's unique values 136 | fni = vectorLayer.fields().indexFromName(attributeClass) 137 | unique_values = vectorLayer.uniqueValues(fni) 138 | fields = [vectorLayer.fields().field(attributeClass), vectorLayer.fields().field(secondaryRankingAttribute)] 139 | 140 | 141 | tempAttributeList = [] 142 | if copyAtt : 143 | tempAttributeList = vectorLayer.fields() 144 | for fTemp in tempAttributeList : 145 | if (fTemp.name() != secondaryRankingAttribute) and (fTemp.name() != attributeClass): 146 | fields.append(fTemp) 147 | 148 | 149 | 150 | 151 | #That tuples contain bounding boxes 152 | #(1) a feature list for a given class 153 | #(2) the width of the rectangle of the class 154 | #(3) the height of the rectangle of the class 155 | #(4) the area of the rectangle of the class (3 * 2) 156 | boundingBox_tuples = [] 157 | 158 | #For each class 159 | for val in unique_values: 160 | #We list the features of the class 161 | featureList = [] 162 | 163 | #The features corresponding to the class are selected and ordered by secondaryRankingAttribute 164 | expr = QgsExpression( "\""+str(attributeClass)+"\"="+str(val)) 165 | request = QgsFeatureRequest( expr) 166 | request = request.addOrderBy("\""+str(secondaryRankingAttribute)+"\"",False) 167 | it = vectorLayer.getFeatures(request) 168 | #The x of the current feature 169 | x_current = 0 170 | 171 | #The heighest bow is necessary to shift the next line 172 | heighestBox = 0; 173 | 174 | for featCurrent in it : 175 | # print("Valeurs : class value " + str(featCurrent.attribute(attributeClass)) + " secondary value" + str(featCurrent.attribute(secondaryRankingAttribute))) 176 | geom = featCurrent.geometry() 177 | #We determine box of the current geometry 178 | minBounds, area, angle, width, height = compute_SMBR(geom) 179 | #The centroid of the box 180 | centroid = minBounds.centroid().asPoint() 181 | 182 | #We check that the 183 | if(width > height) : 184 | angle = angle + 90 185 | width, height = height, width 186 | 187 | #Rotate of the geometry according to SMBR angle 188 | err = geom.rotate( -angle, centroid) 189 | 190 | 191 | #Determining the translation into a local referential 192 | dx = x_current - centroid.x() + width/2.0 193 | dy = - centroid.y() 194 | 195 | heighestBox = max(heighestBox, height) 196 | 197 | x_current = x_current + (width) 198 | 199 | err = geom.translate(dx,dy) 200 | 201 | #We create a feature with the fields and the transformed geometry 202 | new_feature = QgsFeature() 203 | new_feature.setGeometry(geom) 204 | new_feature.initAttributes(len(fields)) 205 | new_feature.setAttribute(0, featCurrent.attribute(attributeClass)) 206 | new_feature.setAttribute(1, featCurrent.attribute(secondaryRankingAttribute)) 207 | 208 | 209 | if copyAtt : 210 | countAt = 2; 211 | countTemp = 0; 212 | for fTemp in tempAttributeList : 213 | if (fTemp.name() != secondaryRankingAttribute) and (fTemp.name() != attributeClass): 214 | new_feature.setAttribute(countAt, featCurrent.attribute(countTemp)) 215 | countAt = countAt+1 216 | countTemp= countTemp +1 217 | 218 | 219 | featureList.append(new_feature) 220 | 221 | 222 | #The rectangle is added to the tuple 223 | boundingBox_tuples.append([featureList, x_current, heighestBox, x_current * heighestBox]) 224 | 225 | return boundingBox_tuples, fields 226 | 227 | #Determine a unique minimum box with as width the widest box 228 | #and as height the sum of all heights 229 | def minimumUniqueBoundingBox(boundingBox_tuple): 230 | widestBox = 0 231 | totalHeight = 0 ; 232 | for boundingBox in boundingBox_tuple: 233 | widestBox = max(widestBox, boundingBox[1]) 234 | totalHeight = totalHeight + boundingBox[2] 235 | 236 | #Width, height, area 237 | boundingBox = [] 238 | boundingBox.append([None, widestBox , totalHeight , widestBox * totalHeight ]) 239 | return boundingBox 240 | 241 | #Determine all the candidate bounding boxes sorted by area 242 | def minimumBoundingBox(boundingBox_tuple): 243 | # Testing all boxes in increasing order and keep the smallest 244 | 245 | #Lower bound : sum of the areas of the given boundingBox 246 | #Upper bound : greedy method : highest boundingBox and all the boundingBoxs 247 | lowerArea = 0; 248 | totalWidth = 0 ; 249 | heighestBox = 0; 250 | widestBox = 0 251 | 252 | 253 | 254 | for boundingBox in boundingBox_tuple: 255 | lowerArea = lowerArea + boundingBox[3] 256 | totalWidth = totalWidth + boundingBox[1] 257 | heighestBox = max(heighestBox, boundingBox[2]) 258 | widestBox = max(widestBox, boundingBox[1]) 259 | upperArea = totalWidth * heighestBox 260 | 261 | 262 | nb_BoundingBox = len(boundingBox_tuple) 263 | 264 | possibleHeight = [] 265 | possibleWidth = [] 266 | 267 | 268 | for i in range(1, nb_BoundingBox+1) : 269 | #print(str(i) + " / " + str(nb_BoundingBox) + " Calculating combinaison") 270 | for rectangle in combinaison(boundingBox_tuple, i): 271 | widthSum = 0 272 | heightSum = 0 273 | for r in rectangle: 274 | widthSum = widthSum + r[1] 275 | heightSum = heightSum + r[2] 276 | 277 | 278 | possibleHeight.append(heightSum) 279 | possibleWidth.append(widthSum) 280 | 281 | 282 | #All possible width and height 283 | possibleHeight = sorted(possibleHeight) 284 | possibleWidth = sorted(possibleWidth) 285 | 286 | #print("Sorting possible height and width") 287 | 288 | #Width, height, area 289 | boundingBox = [] 290 | 291 | append = boundingBox.append 292 | 293 | #countWidth = 0 294 | for width in possibleWidth : 295 | #countWidth = countWidth+1 296 | #print("Width : " + str(countWidth) + " / " + str(len(possibleWidth))) 297 | #The width must be at least the height of the tallest rectangle 298 | if width < widestBox : 299 | continue 300 | 301 | #The height must be at least the height of the tallest rectangle 302 | for height in possibleHeight: 303 | if height < heighestBox: 304 | continue 305 | 306 | #The area must be enough to contain all rectangles 307 | area = width * height 308 | if area < lowerArea: 309 | continue 310 | 311 | if area > upperArea: 312 | break 313 | 314 | append([None, width, height, area]) 315 | 316 | 317 | 318 | 319 | 320 | resultSorted = sorted(boundingBox, key=lambda tup: tup[3]) 321 | # resultSorted = sorted(boundingBox, key=lambda tup: (abs(1 - tup[1]/tup[2]), tup[1])) 322 | # print(resultSorted) 323 | return resultSorted 324 | 325 | 326 | 327 | #Try to pack the bounding box into the candidate bounding boxes 328 | #Ranking = 0 position priority ordered by distance to origin (for optimal layout) 329 | #Ranking = 1 position priority ordered by y then x 330 | def pack(boundingBox_tuples, boundingBoxes, ranking): 331 | #Recursiev algorithm to find the minimal bounding box in term or arae 332 | indexMin = 0 333 | indexMax = len(boundingBoxes) - 1 334 | bestLayout = None 335 | bestBox = None 336 | 337 | 338 | count = 0 339 | 340 | for bestBox in boundingBoxes : 341 | 342 | #print("Treating : " + str(count+1) + "/" + str(len(boundingBoxes))) 343 | bestLayout = determineLayout(boundingBox_tuples, bestBox, ranking) 344 | if not bestLayout is None: 345 | currentBox = boundingBoxes[count] 346 | break 347 | count = count + 1 348 | 349 | return bestLayout, bestBox 350 | 351 | #Generate a layout relatively to a bounding box 352 | #Placement is organized from widest 353 | def determineLayout(boundingBox_tuples, boundingBox, ranking): 354 | boundingBox_tuples = sorted(boundingBox_tuples, key=lambda tup: tup[1], reverse=True) 355 | #X,Y coordinates 356 | #Originate is lower left point 357 | possibleVertices = [(0,0)] 358 | #feature, X,Y,Width,Length, area 359 | #Originate is lower left point 360 | placedRectangles = [] 361 | #When a new placed rectangle generate a non-reflex vertex 362 | #A supplementary vertice may be generated under it 363 | # Either at y = 0 or at the first met box under it 364 | suppVertix = None 365 | 366 | #For each boxes 367 | for boundingBoxToPlace in boundingBox_tuples : 368 | #A place is not found 369 | isPlaced = False 370 | #We test all the candidate vertices 371 | for vertix in possibleVertices : 372 | #Can we place the rectangle at a given vertex 373 | #Without intersecting the other ? 374 | rectangleOk = canPlaceRectangle(vertix, boundingBoxToPlace,placedRectangles) 375 | if rectangleOk is None: 376 | continue 377 | #Is it in the input bounding box 378 | if not checkIfIsBoundingBox(rectangleOk, boundingBox): 379 | continue 380 | #Yes we keep the position 381 | isPlaced = True 382 | #We determine if a supplementaryVertix is necessary 383 | suppVertix = supplementaryVertix([vertix[0] + boundingBoxToPlace[1], vertix[1]], placedRectangles) 384 | #Append to placed rectangles 385 | placedRectangles.append(rectangleOk) 386 | #we do not need to continue 387 | break; 388 | if not isPlaced: 389 | #It means that the bounding box cannnot be placed 390 | #The algo is stopped and a new layout will be tested 391 | #With an other constraint bounding box 392 | return None; 393 | #We remove the current vertex 394 | possibleVertices.remove(vertix) 395 | #If there is a supplementary vertex, we will use it 396 | if not suppVertix is None: 397 | possibleVertices.append(suppVertix) 398 | 399 | possibleVertices.append([vertix[0] + boundingBoxToPlace[1], vertix[1]]) 400 | 401 | possibleVertices.append([vertix[0], vertix[1] + boundingBoxToPlace[2]]) 402 | 403 | 404 | possibleVertices.append([vertix[0] + boundingBoxToPlace[1], vertix[1] + boundingBoxToPlace[2]]) 405 | 406 | 407 | #Reordering vertices according to origin distance 408 | if (ranking == 1) : 409 | possibleVertices = sorted(possibleVertices, key=lambda x: x[1] * 1000 + x[0]) 410 | else : 411 | possibleVertices = sorted(possibleVertices, key=lambda x: (x[1] * x[1] + x[0] * x[0])) 412 | 413 | 414 | 415 | return placedRectangles 416 | 417 | 418 | 419 | 420 | def movingFeature(rectngle_tuple, vectorLayer, attributeClass, secondaryRankingAttribute, outputLayerName, fields): 421 | #Initializing new layer 422 | vl = QgsVectorLayer("Polygon", outputLayerName, "memory") 423 | pr = vl.dataProvider() 424 | 425 | #Update 426 | pr.addAttributes(fields) 427 | vl.updateFields() 428 | 429 | features = [] 430 | 431 | for rectangle in rectngle_tuple: 432 | #The translation is encoding with X,Y 433 | x = rectangle[1] 434 | y = rectangle[2] + rectangle[4] /2 435 | 436 | for feature in rectangle[0]: 437 | geometry = feature.geometry() 438 | geometry.translate(x,y) 439 | feature.setGeometry(geometry) 440 | features.append(feature) 441 | 442 | 443 | pr.addFeatures(features) 444 | vl.commitChanges() 445 | return vl 446 | 447 | 448 | 449 | #Method to extend the rectangle in width as much as possible and displacing the features inside 450 | #It requires a rectngle_tuple and the bounding box of the layout 451 | def extendRectangleTuple(rectngle_tuple, b): 452 | #Results are stored into the intial rectngle_tuple 453 | 454 | #This is a discrete method 455 | widthStep = b[1]/1000.0; 456 | 457 | 458 | nbRectangle = len(rectngle_tuple) 459 | #Iteration on each rectangle 460 | for i in range(0, nbRectangle): 461 | 462 | #We remove a current rectangle from the list 463 | currentRectangle = rectngle_tuple[i] 464 | rectngle_tuple.remove(currentRectangle) 465 | 466 | #We store the initial width and the width after modifications 467 | initialWidth = currentRectangle[3] 468 | currentWidth = initialWidth 469 | #Boucle until : the rectangle cannot be placed in the bounding box or if it inteersects an other rectangle 470 | conditionCheck = True 471 | while conditionCheck : 472 | #We widthen the current rectangle 473 | currentRectangle = widthenRectangle(currentRectangle, widthStep); 474 | currentWidth = currentWidth + widthStep 475 | 476 | #Does it stay into the initial bounding box ? 477 | if not checkIfIsBoundingBox(currentRectangle, b): 478 | conditionCheck = False 479 | break 480 | 481 | #Does it intersects another rectangle from the list ? 482 | for placeRectangle in rectngle_tuple: 483 | intersected = testIntersection(currentRectangle, placeRectangle) 484 | 485 | if intersected : 486 | conditionCheck = False 487 | break 488 | 489 | #We went a step further we decrease the width 490 | currentRectangle = widthenRectangle(currentRectangle, - widthStep) 491 | currentWidth = currentWidth - widthStep 492 | #We move the features inside the rectangle 493 | extendFeatureInRectangle(currentRectangle, currentWidth, initialWidth) 494 | #We re-insert the rectangle into the list 495 | rectngle_tuple.insert(i, currentRectangle) 496 | return 497 | 498 | #Code to widthen a rectangle 499 | def widthenRectangle(rectangle, step): 500 | return (rectangle[0],rectangle[1],rectangle[2],rectangle[3]+step,rectangle[4]) 501 | 502 | #Code to move the feature inside a rectangle 503 | def extendFeatureInRectangle(currentRectangle,currentWidth, initialWidth ): 504 | #NO width change we can exist 505 | if currentWidth == initialWidth: 506 | return currentRectangle; 507 | #We get the features inside a rectangle 508 | features = currentRectangle[0] 509 | 510 | nbFeatures = len(features) 511 | #The x move for each featuer from a previous one 512 | deltaX = (currentWidth - initialWidth) / (nbFeatures+1) 513 | 514 | #We applied a translation i * deltaX 515 | for i in range(0, nbFeatures): 516 | currentFeature = features[i] 517 | features.remove(currentFeature) 518 | 519 | geometry = currentFeature.geometry() 520 | geometry.translate((i + 1) * deltaX,0) 521 | currentFeature.setGeometry(geometry) 522 | 523 | 524 | features.insert(i, currentFeature) 525 | 526 | #We return the new rectangle 527 | return (features,currentRectangle[1],currentRectangle[2],currentRectangle[3],currentRectangle[4]); 528 | 529 | 530 | """ 531 | Utility functions 532 | """ 533 | 534 | #Assesing combinaison from a tuple 535 | def combinaison(seq, k): 536 | p = [] 537 | i, imax = 0, 2**len(seq)-1 538 | while i<=imax: 539 | s = [] 540 | j, jmax = 0, len(seq)-1 541 | while j<=jmax: 542 | if (i>>j)&1==1: 543 | s.append(seq[j]) 544 | j += 1 545 | if len(s)==k: 546 | p.append(s) 547 | i += 1 548 | return p 549 | 550 | 551 | #Determine if a rectangle can be placed at a given vertex (i.e if it does not intersects other placed rectangles) 552 | def canPlaceRectangle(vertix, rectangle,placedRectangles): 553 | rectangleToTest = (rectangle[0], vertix[0], vertix[1], rectangle[1], rectangle[2], rectangle[3]) 554 | for placeRectangle in placedRectangles: 555 | intersected = testIntersection(rectangleToTest, placeRectangle) 556 | if intersected : 557 | return None; 558 | 559 | return rectangleToTest 560 | 561 | #Check if a rectangle is inside a bounding box 562 | def checkIfIsBoundingBox(placedRectangle, boundingBox): 563 | return (placedRectangle[1] + placedRectangle[3] <= boundingBox[1]) and (placedRectangle[2] + placedRectangle[4] <= boundingBox[2]) 564 | 565 | #Test the intersection between two rectangles 566 | def testIntersection(r1,r2): 567 | 568 | if ((r1[1] < (r2[1] + r2[3])) and (r2[1] < (r1[1]+r1[3])) and 569 | (r1[2] < (r2[2] + r2[4])) and (r2[2] < (r1[2]+r1[4]))): 570 | return True 571 | return False 572 | 573 | #Eventually add a supplementary vertix in the cas of non-reflex vertex 574 | #If a box is added 575 | def supplementaryVertix(vertixIni, placedRectangles): 576 | if(vertixIni[1] == 0): 577 | return None 578 | 579 | newY = 0; 580 | #We only keep the y with the highest value (if not above the rectangle) 581 | for rectangles in placedRectangles: 582 | if( (rectangles[1] < vertixIni[0]) and (rectangles[1] + rectangles[3] > vertixIni[0])): 583 | currentY = rectangles[2] + rectangles[4] 584 | 585 | if(vertixIni[1] < currentY): 586 | continue; 587 | 588 | newY = max(newY, currentY) 589 | 590 | #print("New y :" + str(newY)) 591 | return [vertixIni[0], newY] 592 | 593 | 594 | 595 | 596 | 597 | 598 | """ 599 | Transforming intermediate objects to VectorLayer 600 | """ 601 | 602 | def fromPlaceRectangleToVectorLayer(placedRectangle): 603 | features = [] 604 | 605 | fields = [QgsField("X", QVariant.Double),QgsField("Y", QVariant.Double), QgsField("width", QVariant.Double),QgsField("height", QVariant.Double)] 606 | vl = QgsVectorLayer("Polygon", "temp", "memory") 607 | pr = vl.dataProvider() 608 | vl.startEditing() 609 | 610 | pr.addAttributes(fields) 611 | vl.updateFields() 612 | 613 | for b in placedRectangle: 614 | feat = generateBoundingBox(b[1], b[2], b[3], b[4], fields) 615 | features.append(feat) 616 | 617 | #print("Number of features :" + str(len(features))) 618 | pr.addFeatures(features) 619 | vl.commitChanges() 620 | return vl 621 | 622 | 623 | def fromBoundingBoxToVectorLayer(boundingBox): 624 | features = [] 625 | 626 | fields = [QgsField("width", QVariant.Double),QgsField("height", QVariant.Double)] 627 | vl = QgsVectorLayer("Polygon", "bob", "memory") 628 | pr = vl.dataProvider() 629 | vl.startEditing() 630 | 631 | pr.addAttributes(fields) 632 | vl.updateFields() 633 | 634 | for b in boundingBox: 635 | feat = generateBoundingBox(b[0], b[1], b[2]) 636 | features.append(feat) 637 | 638 | #print("Number of features :" + str(len(features))) 639 | pr.addFeatures(features) 640 | vl.commitChanges() 641 | return vl 642 | 643 | 644 | def generateBoundingBox(x,y, width, height, fields): 645 | gPolygon = QgsGeometry.fromPolygonXY([[QgsPointXY(x, y), QgsPointXY(x+ width, y), QgsPointXY(x + width, y + height), 646 | QgsPointXY(x, y +height)]]) 647 | 648 | 649 | feat = QgsFeature() 650 | feat.setGeometry(gPolygon) 651 | feat.initAttributes(len(fields)) 652 | 653 | feat.setAttribute(0, width) 654 | feat.setAttribute(1, height) 655 | return feat; 656 | -------------------------------------------------------------------------------- /neatmap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | NeatMap 5 | A QGIS plugin 6 | A simple QGIS python plugin for building neat maps. 7 | ------------------- 8 | begin : 2016-11-30 9 | git sha : $Format:%H$ 10 | copyright : (C) 2016 - 2018 by IGN 11 | email : julien.perret@gmail.com; mickael.brasebin@ign.fr 12 | ***************************************************************************/ 13 | 14 | /*************************************************************************** 15 | * * 16 | * This program is free software; you can redistribute it and/or modify * 17 | * it under the terms of the GNU General Public License as published by * 18 | * the Free Software Foundation; either version 2 of the License, or * 19 | * (at your option) any later version. * 20 | * * 21 | ***************************************************************************/ 22 | """ 23 | import os.path 24 | import math 25 | from random import randrange 26 | 27 | from PyQt5.QtCore import QSettings, QTranslator, qVersion, QCoreApplication, QVariant, Qt, pyqtSlot 28 | from PyQt5.QtGui import QIcon, QTransform 29 | from PyQt5.QtWidgets import QAction, QProgressBar, QCheckBox, QFrame, QVBoxLayout 30 | 31 | from .resources import * 32 | 33 | from qgis.core import * 34 | from qgis.gui import * 35 | 36 | #Internal imports 37 | from .indicatorCalculation import * 38 | from .classification import * 39 | from .square_packing import * 40 | #GUI import 41 | from .neatmap_dialog import NeatMapDialog 42 | from .neatmap_about_dialog import NeatMapAboutDialog 43 | 44 | 45 | 46 | class NeatMap: 47 | """QGIS Plugin Implementation.""" 48 | 49 | def __init__(self, iface): 50 | """Constructor. 51 | 52 | :param iface: An interface instance that will be passed to this class 53 | which provides the hook by which you can manipulate the QGIS 54 | application at run time. 55 | :type iface: QgsInterface 56 | """ 57 | print("Initialisation") 58 | 59 | 60 | # Save reference to the QGIS interface 61 | self.iface = iface 62 | # initialize plugin directory 63 | self.plugin_dir = os.path.dirname(__file__) 64 | # initialize locale 65 | locale = QSettings().value('locale/userLocale')[0:2] 66 | locale_path = os.path.join( 67 | self.plugin_dir, 68 | 'i18n', 69 | 'NeatMap_{}.qm'.format(locale)) 70 | 71 | if os.path.exists(locale_path): 72 | self.translator = QTranslator() 73 | self.translator.load(locale_path) 74 | 75 | if qVersion() > '4.3.3': 76 | QCoreApplication.installTranslator(self.translator) 77 | 78 | 79 | # Declare instance attributes 80 | self.actions = [] 81 | self.menu = self.tr(u'&NeatMap') 82 | # TODO: We are going to let the user set this up in a future iteration 83 | self.toolbar = self.iface.addToolBar(u'NeatMap') 84 | self.toolbar.setObjectName(u'NeatMap') 85 | 86 | # noinspection PyMethodMayBeStatic 87 | def tr(self, message): 88 | """Get the translation for a string using Qt translation API. 89 | 90 | We implement this ourselves since we do not inherit QObject. 91 | 92 | :param message: String for translation. 93 | :type message: str, QString 94 | 95 | :returns: Translated version of message. 96 | :rtype: QString 97 | """ 98 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 99 | return QCoreApplication.translate('NeatMap', message) 100 | 101 | 102 | def add_action( 103 | self, 104 | icon_path, 105 | text, 106 | callback, 107 | enabled_flag=True, 108 | add_to_menu=True, 109 | add_to_toolbar=True, 110 | status_tip=None, 111 | whats_this=None, 112 | parent=None): 113 | """Add a toolbar icon to the toolbar. 114 | 115 | :param icon_path: Path to the icon for this action. Can be a resource 116 | path (e.g. ':/plugins/foo/bar.png') or a normal file system path. 117 | :type icon_path: str 118 | 119 | :param text: Text that should be shown in menu items for this action. 120 | :type text: str 121 | 122 | :param callback: Function to be called when the action is triggered. 123 | :type callback: function 124 | 125 | :param enabled_flag: A flag indicating if the action should be enabled 126 | by default. Defaults to True. 127 | :type enabled_flag: bool 128 | 129 | :param add_to_menu: Flag indicating whether the action should also 130 | be added to the menu. Defaults to True. 131 | :type add_to_menu: bool 132 | 133 | :param add_to_toolbar: Flag indicating whether the action should also 134 | be added to the toolbar. Defaults to True. 135 | :type add_to_toolbar: bool 136 | 137 | :param status_tip: Optional text to show in a popup when mouse pointer 138 | hovers over the action. 139 | :type status_tip: str 140 | 141 | :param parent: Parent widget for the new action. Defaults None. 142 | :type parent: QWidget 143 | 144 | :param whats_this: Optional text to show in the status bar when the 145 | mouse pointer hovers over the action. 146 | 147 | :returns: The action that was created. Note that the action is also 148 | added to self.actions list. 149 | :rtype: QAction 150 | """ 151 | 152 | # Create the dialog (after translation) and keep reference 153 | self.dlg = NeatMapDialog() 154 | 155 | icon = QIcon(icon_path) 156 | action = QAction(icon, text, parent) 157 | action.triggered.connect(callback) 158 | action.setEnabled(enabled_flag) 159 | 160 | if status_tip is not None: 161 | action.setStatusTip(status_tip) 162 | 163 | if whats_this is not None: 164 | action.setWhatsThis(whats_this) 165 | 166 | if add_to_toolbar: 167 | self.toolbar.addAction(action) 168 | 169 | if add_to_menu: 170 | self.iface.addPluginToVectorMenu( 171 | self.menu, 172 | action) 173 | 174 | self.actions.append(action) 175 | 176 | return action 177 | 178 | def initGui(self): 179 | """Create the menu entries and toolbar icons inside the QGIS GUI.""" 180 | 181 | icon_path = ':/plugins/NeatMap/icon.png' 182 | self.add_action( 183 | icon_path, 184 | text=self.tr(u'Build a neat map'), 185 | callback=self.run, 186 | parent=self.iface.mainWindow()) 187 | #Run method that performs all the real work 188 | self.prepareGUI() 189 | 190 | 191 | 192 | def unload(self): 193 | """Removes the plugin menu item and icon from QGIS GUI.""" 194 | for action in self.actions: 195 | self.iface.removePluginVectorMenu( 196 | self.tr(u'&NeatMap'), 197 | action) 198 | self.iface.removeToolBarIcon(action) 199 | # remove the toolbar 200 | del self.toolbar 201 | 202 | """ 203 | Fonction run. 204 | """ 205 | 206 | def run(self): 207 | #Updating all the dropboxes 208 | self.updateDropBoxes() 209 | 210 | # show the dialog 211 | self.dlg.show() 212 | 213 | # Run the dialog event loop 214 | result = self.dlg.exec_() 215 | 216 | return 217 | 218 | 219 | 220 | """ 221 | GUI Iniialization 222 | """ 223 | def prepareGUI(self): 224 | #Button to load about Window 225 | self.dlg.aboutButton.clicked.connect(self.clickAbout) 226 | 227 | #Button to process calculation 228 | self.dlg.pushButtonCalculation.clicked.connect(self.processCalculation) 229 | self.dlg.pushButtonClassification.clicked.connect(self.processClassification) 230 | self.dlg.pushButtonLayout.clicked.connect(self.processLayout) 231 | 232 | #DropDown are updated when dropdown layer are activated 233 | self.dlg.inputPolygonLayer.activated.connect(self.updatePolygonLayer) 234 | self.dlg.inputPolygonLayerClass.activated.connect(self.updatePolygonLayerClass) 235 | self.dlg.inputPolygonLayerLayout.activated.connect(self.updateLayoutLayer) 236 | 237 | #Updating the list of available attributes 238 | scrollArea = self.dlg.scrollArea; 239 | scrollArea.setWidgetResizable(True) 240 | scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 241 | inner = QFrame(scrollArea) 242 | inner.setLayout(QVBoxLayout()) 243 | scrollArea.setWidget(inner) 244 | 245 | 246 | 247 | """ 248 | 249 | Refresh/updating interface 250 | 251 | """ 252 | #Update the dropboxes that contains the layer list 253 | def updateDropBoxes(self): 254 | """Update the dropdowns with layers""" 255 | layers = QgsProject.instance().mapLayers().values() 256 | 257 | self.dlg.inputPolygonLayer.clear() 258 | self.dlg.inputPolygonLayerClass.clear() 259 | self.dlg.inputPolygonLayerLayout.clear() 260 | 261 | 262 | for layer in layers: 263 | self.dlg.inputPolygonLayer.addItem(layer.name(),layer) 264 | self.dlg.inputPolygonLayerClass.addItem(layer.name(),layer) 265 | self.dlg.inputPolygonLayerLayout.addItem(layer.name(),layer) 266 | #Refresh the dropboxes that list attributes 267 | self.updatePolygonLayer() 268 | self.updatePolygonLayerClass() 269 | self.updateLayoutLayer() 270 | 271 | """ 272 | 273 | Section 1 274 | 275 | """ 276 | 277 | 278 | 279 | #Refresh the ID attribute list from indicator calculation step 280 | def refreshAttributeDropBox(self): 281 | #Listing layers 282 | layers = QgsProject.instance().mapLayers().values() 283 | self.dlg.intputIDChoice.clear() 284 | 285 | selectedInputLayerIndex = self.dlg.inputPolygonLayer.currentIndex() 286 | 287 | if selectedInputLayerIndex > -1 : 288 | #Getting the selected layer 289 | selectedInputLayer = self.dlg.inputPolygonLayer.itemData(selectedInputLayerIndex) 290 | 291 | count = selectedInputLayer.featureCount(); 292 | 293 | for a in selectedInputLayer.fields(): 294 | self.dlg.intputIDChoice.addItem(a.displayName(),a) 295 | 296 | 297 | 298 | #Action when layer from indicator calculation is refreshed 299 | def updatePolygonLayer(self): 300 | self.refreshAttributeDropBox() 301 | 302 | 303 | """ 304 | 305 | Section 2 306 | 307 | """ 308 | #Action when layer from classification is refreshed 309 | def updatePolygonLayerClass(self): 310 | self.refreshDropDownLayerPanel() 311 | self.refreshAttributeDropBoxClassif() 312 | 313 | #Refresh the ID attribute list from indicator calculation step 314 | def refreshAttributeDropBoxClassif(self): 315 | #Listing layers 316 | layers = QgsProject.instance().mapLayers().values() 317 | self.dlg.intputIDChoiceClassif.clear() 318 | 319 | selectedInputLayerIndex = self.dlg.inputPolygonLayerClass.currentIndex() 320 | 321 | if selectedInputLayerIndex > -1 : 322 | #Getting the selected layer 323 | selectedInputLayer = self.dlg.inputPolygonLayerClass.itemData(selectedInputLayerIndex) 324 | 325 | count = selectedInputLayer.featureCount(); 326 | 327 | for a in selectedInputLayer.fields(): 328 | self.dlg.intputIDChoiceClassif.addItem(a.displayName(),a) 329 | 330 | 331 | 332 | 333 | 334 | # Refresh the panel with the checkbox list 335 | def refreshDropDownLayerPanel(self): 336 | layout = self.dlg.scrollArea.widget().layout() 337 | #Cleaning layout 338 | 339 | for i in reversed(range(layout.count())): 340 | widgetToRemove = layout.itemAt( i ).widget() 341 | # remove it from the layout list 342 | layout.removeWidget( widgetToRemove ) 343 | # remove it from the gui 344 | widgetToRemove.setParent( None ) 345 | 346 | layers = QgsProject.instance().mapLayers().values() 347 | 348 | selectedInputLayerIndex = self.dlg.inputPolygonLayerClass.currentIndex() 349 | 350 | if selectedInputLayerIndex > -1 : 351 | #Getting the selected layer 352 | selectedInputLayer = self.dlg.inputPolygonLayerClass.itemData(selectedInputLayerIndex) 353 | 354 | count = selectedInputLayer.featureCount(); 355 | 356 | 357 | for a in selectedInputLayer.fields(): 358 | if (a.isNumeric()) : 359 | checkBox = QCheckBox(a.displayName()) 360 | layout.addWidget(checkBox) 361 | checkBox.setChecked(True) 362 | 363 | 364 | def categorizedColor(self, vectorLayer, classAttNam): 365 | # provide file name index and field's unique values 366 | fni = vectorLayer.fields().indexFromName(classAttNam) 367 | unique_values = vectorLayer.uniqueValues(fni) 368 | 369 | # fill categories 370 | categories = [] 371 | for unique_value in unique_values: 372 | # initialize the default symbol for this geometry type 373 | symbol = QgsSymbol.defaultSymbol(vectorLayer.geometryType()) 374 | 375 | # configure a symbol layer 376 | layer_style = {} 377 | layer_style['color'] = '%d, %d, %d' % (randrange(0, 256), randrange(0, 256), randrange(0, 256)) 378 | layer_style['outline'] = '#000000' 379 | symbol_layer = QgsSimpleFillSymbolLayer.create(layer_style) 380 | 381 | # replace default symbol layer with the configured one 382 | if symbol_layer is not None: 383 | symbol.changeSymbolLayer(0, symbol_layer) 384 | 385 | # create renderer object 386 | category = QgsRendererCategory(unique_value, symbol, str(unique_value)) 387 | # entry for the list of category items 388 | categories.append(category) 389 | 390 | # create renderer object 391 | renderer = QgsCategorizedSymbolRenderer(classAttNam, categories) 392 | 393 | # assign the created renderer to the layer 394 | if renderer is not None: 395 | vectorLayer.setRenderer(renderer) 396 | 397 | vectorLayer.triggerRepaint() 398 | 399 | """ 400 | 401 | Section 3 402 | 403 | """ 404 | def updateLayoutLayer(self): 405 | self.updateIDClassification() 406 | self.updateSecondaryRanking() 407 | 408 | 409 | def updateIDClassification(self): 410 | layers = QgsProject.instance().mapLayers().values() 411 | self.dlg.classificationAttributeLayout.clear() 412 | 413 | selectedInputLayerIndex = self.dlg.inputPolygonLayerLayout.currentIndex() 414 | 415 | if selectedInputLayerIndex > -1 : 416 | #Getting the selected layer 417 | selectedInputLayer = self.dlg.inputPolygonLayerLayout.itemData(selectedInputLayerIndex) 418 | 419 | count = selectedInputLayer.featureCount(); 420 | 421 | for a in selectedInputLayer.fields(): 422 | if a.isNumeric(): 423 | self.dlg.classificationAttributeLayout.addItem(a.displayName(),a) 424 | 425 | def updateSecondaryRanking(self): 426 | layers = QgsProject.instance().mapLayers().values() 427 | self.dlg.inputSecondaryAttributeLayout.clear() 428 | 429 | selectedInputLayerIndex = self.dlg.inputPolygonLayerLayout.currentIndex() 430 | 431 | if selectedInputLayerIndex > -1 : 432 | #Getting the selected layer 433 | selectedInputLayer = self.dlg.inputPolygonLayerLayout.itemData(selectedInputLayerIndex) 434 | 435 | count = selectedInputLayer.featureCount(); 436 | 437 | for a in selectedInputLayer.fields(): 438 | if a.isNumeric(): 439 | self.dlg.inputSecondaryAttributeLayout.addItem(a.displayName(),a) 440 | 441 | """ 442 | 443 | 444 | Execution phase 1 : calculation 445 | 446 | """ 447 | 448 | 449 | #Processing calculation when ok button from Indicator calculation is pressed 450 | def processCalculation(self): 451 | #Getting the polygonlayer 452 | selectedInputLayerIndex = self.dlg.inputPolygonLayer.currentIndex() 453 | selectedInputLayer = self.dlg.inputPolygonLayer.itemData(selectedInputLayerIndex) 454 | 455 | if selectedInputLayer is None : 456 | QgsMessageLog.logMessage("No selected layer") 457 | return 458 | 459 | QgsMessageLog.logMessage("Layer selected : " + selectedInputLayer.name(), "Neat Map", Qgis.Info) 460 | layername = self.dlg.LineEditTemporaryLayerName.text() 461 | QgsMessageLog.logMessage("Calculating indicator on layer : " + layername, "Neat Map", Qgis.Info) 462 | intputIDChoiceIndex = self.dlg.intputIDChoice.currentIndex() 463 | 464 | intputIDChoiceValue = self.dlg.intputIDChoice.itemData(intputIDChoiceIndex).displayName() 465 | 466 | if intputIDChoiceValue == -1: 467 | QgsMessageLog.logMessage("No ID attribute chosen", "Neat Map", Qgis.Info) 468 | return 469 | QgsMessageLog.logMessage("ID value : " + intputIDChoiceValue, "Neat Map", Qgis.Info) 470 | copyAttribute = self.dlg.copyAtt.isChecked() 471 | 472 | QgsMessageLog.logMessage("Copy attribute : " + str(copyAttribute), "Neat Map", Qgis.Info) 473 | QgsMessageLog.logMessage("Calculating indicator on layer : " + layername, "Neat Map", Qgis.Info) 474 | 475 | vlOut = calculate(layername, selectedInputLayer,intputIDChoiceValue, copyAttribute); 476 | 477 | QgsMessageLog.logMessage("Adding layer to map", "Neat Map", Qgis.Info) 478 | QgsProject.instance().addMapLayer(vlOut) 479 | 480 | #Refresh after processing 481 | 482 | #Updating all layers as a new layer is added 483 | self.updateDropBoxes() 484 | 485 | #Selection of new layer in classificaion menue 486 | self.selectItem(self.dlg.inputPolygonLayerClass,vlOut.name()) 487 | 488 | #Updateing classificaion content 489 | self.updatePolygonLayerClass() 490 | self.selectItem(self.dlg.intputIDChoiceClassif, intputIDChoiceValue) 491 | self.selectItem(self.dlg.inputPolygonLayer,selectedInputLayer.name()) 492 | 493 | 494 | 495 | """ 496 | 497 | 498 | Execution phase 2 : classification 499 | 500 | """ 501 | 502 | 503 | def processClassification(self): 504 | selectedInputLayerIndex = self.dlg.inputPolygonLayerClass.currentIndex() 505 | selectedInputLayer = self.dlg.inputPolygonLayerClass.itemData(selectedInputLayerIndex) 506 | 507 | if(selectedInputLayer is None) : 508 | QgsMessageLog.logMessage("No selected layer") 509 | return 510 | QgsMessageLog.logMessage("Layer selected : " + selectedInputLayer.name(), "Neat Map", Qgis.Info) 511 | 512 | attributes = self.listingCheckedAttributes() 513 | nbAttributes = str(len(attributes)) 514 | if nbAttributes == 0 : 515 | QgsMessageLog.logMessage("No selected attributes", "Neat Map", Qgis.Info) 516 | return 517 | QgsMessageLog.logMessage("Attributes selected : " + nbAttributes, "Neat Map", Qgis.Info) 518 | 519 | strNumberOfClasses = self.dlg.classifNumberOfClasses.text() 520 | numberOfClasses = int(strNumberOfClasses) 521 | 522 | if numberOfClasses < 1: 523 | QgsMessageLog.logMessage("Not enough classes :" + numberOfClasses, "Neat Map", Qgis.Info) 524 | return 525 | QgsMessageLog.logMessage("Number of classes : " + str(numberOfClasses), "Neat Mapy", Qgis.Info) 526 | 527 | layername = self.dlg.classLayerName.text() 528 | QgsMessageLog.logMessage("Calculating indicator on layer : " + layername, "Neat Map", Qgis.Info) 529 | 530 | intputIDChoiceIndex = self.dlg.intputIDChoiceClassif.currentIndex() 531 | 532 | 533 | 534 | intputIDChoiceValue = self.dlg.intputIDChoiceClassif.itemData(intputIDChoiceIndex).displayName() 535 | 536 | if intputIDChoiceValue == -1: 537 | QgsMessageLog.logMessage("No ID attribute chosen", "Neat Map", Qgis.Info) 538 | return 539 | QgsMessageLog.logMessage("ID value : " + intputIDChoiceValue, "TNeat Map", Qgis.Info) 540 | 541 | copyAttribute = self.dlg.copyAtt.isChecked() 542 | QgsMessageLog.logMessage("Copy attribute : " + str(copyAttribute), "Neat Map", Qgis.Info) 543 | 544 | attributeClass = self.dlg.lineEditAttClass.text() 545 | 546 | QgsMessageLog.logMessage("Attribute for classification: " + attributeClass, "Neat Map", Qgis.Info) 547 | 548 | layerClassified = kmeans(selectedInputLayer, attributes, numberOfClasses, layername, attributeClass, intputIDChoiceValue, copyAttribute) 549 | QgsMessageLog.logMessage("Adding layer to map", "Neat Map", Qgis.Info) 550 | QgsProject.instance().addMapLayer(layerClassified) 551 | self.categorizedColor(layerClassified, attributeClass) 552 | 553 | #Refresh after processing 554 | 555 | #Updating all layers as a new layer is added 556 | self.updateDropBoxes() 557 | 558 | #Selection of new layer in classificaion menue 559 | self.selectItem(self.dlg.inputPolygonLayerLayout,layerClassified.name()) 560 | 561 | #Updateing classificaion content 562 | self.updatePolygonLayerClass() 563 | self.updateLayoutLayer() 564 | self.selectItem(self.dlg.classificationAttributeLayout, attributeClass) 565 | self.selectItem(self.dlg.inputSecondaryAttributeLayout,"area") 566 | 567 | """ 568 | 569 | Execution phase 3 : layout 570 | 571 | """ 572 | def processLayout(self): 573 | selectedIndexMethod = self.dlg.comboBoxLayoutMethod.currentIndex() 574 | 575 | copyAttribute = self.dlg.copyAtt.isChecked() 576 | QgsMessageLog.logMessage("Copy attribute : " + str(copyAttribute), "Neat Map", Qgis.Info) 577 | 578 | selectedInputLayerIndex = self.dlg.inputPolygonLayerLayout.currentIndex() 579 | selectedInputLayer = self.dlg.inputPolygonLayerLayout.itemData(selectedInputLayerIndex) 580 | 581 | if(selectedInputLayer is None) : 582 | QgsMessageLog.logMessage("No selected layer") 583 | return 584 | 585 | 586 | QgsMessageLog.logMessage("Layer selected : " + selectedInputLayer.name(), "Neat Map", Qgis.Info) 587 | 588 | intputClassificationAttributeIndex = self.dlg.classificationAttributeLayout.currentIndex() 589 | intputClassificationAttribute = self.dlg.classificationAttributeLayout.itemData(intputClassificationAttributeIndex).displayName() 590 | 591 | if(intputClassificationAttribute is None) : 592 | QgsMessageLog.logMessage("No attribute classification selected") 593 | return 594 | QgsMessageLog.logMessage("Classification attribute : " + intputClassificationAttribute, "Neat Map", Qgis.Info) 595 | 596 | intputClassificationSecondaryAttributeIndex = self.dlg.inputSecondaryAttributeLayout.currentIndex() 597 | intputClassificationSecondaryAttribute = self.dlg.inputSecondaryAttributeLayout.itemData(intputClassificationSecondaryAttributeIndex).displayName() 598 | 599 | 600 | if(intputClassificationSecondaryAttribute is None) : 601 | QgsMessageLog.logMessage("No attribute second classification selected") 602 | return 603 | 604 | QgsMessageLog.logMessage("Secondary classification attribute : " + intputClassificationSecondaryAttribute, "Neat Map", Qgis.Info) 605 | 606 | layerName = self.dlg.inputLayerNameLayout.text() 607 | QgsMessageLog.logMessage("Out layer for classification: " + layerName, "Neat Map", Qgis.Info) 608 | 609 | 610 | copyAttribute = self.dlg.copyAtt.isChecked() 611 | QgsMessageLog.logMessage("Copy attribute : " + str(copyAttribute), "Neat Map", Qgis.Info) 612 | 613 | newLayoutLayer = None; 614 | boundingBoxLayout = None; 615 | if selectedIndexMethod ==0 : 616 | newLayoutLayer = naive_layout(selectedInputLayer, intputClassificationAttribute , intputClassificationSecondaryAttribute, layerName, copyAttribute) 617 | elif selectedIndexMethod == 2: 618 | newLayoutLayer, boundingBoxLayout = advanced_layout(selectedInputLayer, intputClassificationAttribute, intputClassificationSecondaryAttribute, layerName, copyAttribute) 619 | elif selectedIndexMethod ==1 : 620 | newLayoutLayer, boundingBoxLayout = fast_layout(selectedInputLayer, intputClassificationAttribute, intputClassificationSecondaryAttribute, layerName, copyAttribute) 621 | if not boundingBoxLayout is None: 622 | QgsProject.instance().addMapLayer(boundingBoxLayout) 623 | 624 | 625 | QgsProject.instance().addMapLayer(newLayoutLayer) 626 | self.categorizedColor(newLayoutLayer, intputClassificationAttribute) 627 | 628 | 629 | """ 630 | 631 | 632 | About windows 633 | 634 | """ 635 | 636 | def clickAbout(self): 637 | # création d'une fenêtre avec QWidget dont on place la référence dans fen 638 | fen = NeatMapAboutDialog() 639 | 640 | # la fenêtre est rendue visible 641 | fen.show() 642 | fen.exec_() 643 | 644 | """ 645 | 646 | 647 | Util functions 648 | 649 | """ 650 | 651 | def selectItem(self, dialog, text): 652 | for i in range(0,dialog.count()): 653 | if text in dialog.itemText(i): 654 | dialog.setCurrentIndex(i) 655 | 656 | def listingCheckedAttributes(self): 657 | attributes = [] 658 | layout = self.dlg.scrollArea.widget().layout() 659 | for i in reversed(range(layout.count())): 660 | if layout.itemAt(i).widget().isChecked(): 661 | attributes.append(layout.itemAt(i).widget().text()) 662 | return attributes 663 | --------------------------------------------------------------------------------