├── icon.png
├── help
├── source
│ ├── img
│ │ ├── a.png
│ │ ├── b.png
│ │ ├── b2.png
│ │ ├── c.png
│ │ ├── d.png
│ │ ├── ui.png
│ │ ├── factor.png
│ │ ├── matrix.png
│ │ ├── MDS_prefecture.png
│ │ ├── cartogram-train.png
│ │ ├── screenshot500.png
│ │ ├── from_2_points_layers.png
│ │ ├── source_point_table.png
│ │ ├── from_2_points_layers2.png
│ │ ├── from_2_points_layers3.png
│ │ └── DistCartogram_prefecture.png
│ ├── index.rst
│ └── conf.py
├── make.bat
└── Makefile
├── data
├── prefecture_FRA.gpkg
└── mat_incomplete.csv
├── test
├── __init__.py
├── test_resources.py
├── test_translations.py
├── utilities.py
├── test_init.py
└── qgis_interface.py
├── resources.qrc
├── i18n
├── DistanceCartogram.pro
└── DistanceCartogram_fr.ts
├── scripts
├── compile-strings.sh
├── run-env-linux.sh
└── update-strings.sh
├── __init__.py
├── metadata.txt
├── .gitignore
├── dist_cartogram_dialog.py
├── README.md
├── CHANGES.rst
├── dist_cartogram_dataset_boxUi.py
├── plugin_upload.py
├── utils.py
├── Makefile
├── pylintrc
├── worker.py
├── grid.py
├── dist_cartogram_dialog_base.ui
└── dist_cartogram.py
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/icon.png
--------------------------------------------------------------------------------
/help/source/img/a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/a.png
--------------------------------------------------------------------------------
/help/source/img/b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/b.png
--------------------------------------------------------------------------------
/help/source/img/b2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/b2.png
--------------------------------------------------------------------------------
/help/source/img/c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/c.png
--------------------------------------------------------------------------------
/help/source/img/d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/d.png
--------------------------------------------------------------------------------
/help/source/img/ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/ui.png
--------------------------------------------------------------------------------
/data/prefecture_FRA.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/data/prefecture_FRA.gpkg
--------------------------------------------------------------------------------
/help/source/img/factor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/factor.png
--------------------------------------------------------------------------------
/help/source/img/matrix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/matrix.png
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
1 | # import qgis libs so that ve set the correct sip api version
2 | import qgis # pylint: disable=W0611 # NOQA
3 |
--------------------------------------------------------------------------------
/help/source/img/MDS_prefecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/MDS_prefecture.png
--------------------------------------------------------------------------------
/help/source/img/cartogram-train.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/cartogram-train.png
--------------------------------------------------------------------------------
/help/source/img/screenshot500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/screenshot500.png
--------------------------------------------------------------------------------
/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | icon.png
4 |
5 |
6 |
--------------------------------------------------------------------------------
/help/source/img/from_2_points_layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/from_2_points_layers.png
--------------------------------------------------------------------------------
/help/source/img/source_point_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/source_point_table.png
--------------------------------------------------------------------------------
/help/source/img/from_2_points_layers2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/from_2_points_layers2.png
--------------------------------------------------------------------------------
/help/source/img/from_2_points_layers3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/from_2_points_layers3.png
--------------------------------------------------------------------------------
/help/source/img/DistCartogram_prefecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/HEAD/help/source/img/DistCartogram_prefecture.png
--------------------------------------------------------------------------------
/i18n/DistanceCartogram.pro:
--------------------------------------------------------------------------------
1 | SOURCES = ../dist_cartogram.py ../dist_cartogram_dialog_baseUi.py ../dist_cartogram_dataset_boxUi.py ../worker.py
2 | TRANSLATIONS = DistanceCartogram_fr.ts
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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__ = "matthieu.viry@cnrs.fr"
12 | __date__ = "2018-07-13"
13 | __copyright__ = "Copyright 2018, Matthieu Viry"
14 |
15 | import unittest
16 |
17 | from PyQt5.QtGui import QIcon
18 |
19 |
20 | class DistCartogramDialogTest(unittest.TestCase):
21 | """Test rerources work."""
22 |
23 | def setUp(self):
24 | """Runs before each test."""
25 | pass
26 |
27 | def tearDown(self):
28 | """Runs after each test."""
29 | pass
30 |
31 | def test_icon_png(self):
32 | """Test we can click OK."""
33 | path = ":/plugins/DistCartogram/icon.png"
34 | icon = QIcon(path)
35 | self.assertFalse(icon.isNull())
36 |
37 |
38 | if __name__ == "__main__":
39 | suite = unittest.makeSuite(DistCartogramResourcesTest)
40 | runner = unittest.TextTestRunner(verbosity=2)
41 | runner.run(suite)
42 |
--------------------------------------------------------------------------------
/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 | pylupdate5 -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 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | DistCartogram
5 | A QGIS plugin
6 | Compute distance cartogram
7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
8 | -------------------
9 | begin : 2018-07-13
10 | copyright : (C) 2018 by Matthieu Viry
11 | email : matthieu.viry@cnrs.fr
12 | git sha : $Format:%H$
13 | ***************************************************************************/
14 |
15 | /***************************************************************************
16 | * *
17 | * This program is free software; you can redistribute it and/or modify *
18 | * it under the terms of the GNU General Public License as published by *
19 | * the Free Software Foundation; either version 2 of the License, or *
20 | * (at your option) any later version. *
21 | * *
22 | ***************************************************************************/
23 | This script initializes the plugin, making it known to QGIS.
24 | """
25 |
26 |
27 | # noinspection PyPep8Naming
28 | def classFactory(iface): # pylint: disable=invalid-name
29 | """Load DistCartogram class from file DistCartogram.
30 |
31 | :param iface: A QGIS interface instance.
32 | :type iface: QgsInterface
33 | """
34 | #
35 | from .dist_cartogram import DistanceCartogram
36 |
37 | return DistanceCartogram(iface)
38 |
--------------------------------------------------------------------------------
/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=DistanceCartogram
11 | qgisMinimumVersion=3.0
12 | description=Compute distance cartogram
13 | version=0.8
14 | author=Matthieu Viry
15 | email=matthieu.viry@cnrs.fr
16 |
17 | about=DistanceCartogram QGIS plugin allows you to create distance cartogram. This is done by extending (by interpolation) to the layer(s) of the study area (territorial divisions, network...) the local displacement between the source coordinates and the image coordinates, derived from the distances between each pair of homologous points (source / image points). DistanceCartogram allows to create distance cartograms in two ways: from two layers of homologous points (source points and image points) or from a layer of points and a durations matrix between them.
18 |
19 | tracker=https://github.com/mthh/QgisDistanceCartogramPlugin/issues
20 | repository=https://github.com/mthh/QgisDistanceCartogramPlugin
21 | # End of mandatory metadata
22 |
23 | # Recommended items:
24 |
25 | # Uncomment the following line and add your changelog:
26 | # changelog=
27 |
28 | # Tags are comma separated with spaces allowed
29 | tags=cartogram,distance,time-space mapping,polygon,deformation
30 |
31 | homepage=https://github.com/mthh/QgisDistanceCartogramPlugin
32 | category=Vector
33 | icon=icon.png
34 | # experimental flag
35 | experimental=False
36 |
37 | # deprecated flag (applies to the whole plugin, not just a single version)
38 | deprecated=False
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | # PyCharm files
107 | .idea/
--------------------------------------------------------------------------------
/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 " "Disaster Reduction"
15 | import unittest
16 | import os
17 |
18 | from PyQt5.QtCore import QCoreApplication, QTranslator
19 |
20 | QGIS_APP = get_qgis_app()
21 |
22 |
23 | class SafeTranslationsTest(unittest.TestCase):
24 | """Test translations work."""
25 |
26 | def setUp(self):
27 | """Runs before each test."""
28 | if "LANG" in iter(os.environ.keys()):
29 | os.environ.__delitem__("LANG")
30 |
31 | def tearDown(self):
32 | """Runs after each test."""
33 | if "LANG" in iter(os.environ.keys()):
34 | os.environ.__delitem__("LANG")
35 |
36 | def test_qgis_translations(self):
37 | """Test that translations work."""
38 | parent_path = os.path.join(__file__, os.path.pardir, os.path.pardir)
39 | dir_path = os.path.abspath(parent_path)
40 | file_path = os.path.join(dir_path, "i18n", "DistCartogram_fr.qm")
41 | app = QCoreApplication([])
42 | translator = QTranslator()
43 | translator.load(file_path)
44 |
45 | QCoreApplication.installTranslator(translator)
46 |
47 | expected_message = "Précision de la grille"
48 | real_message = QCoreApplication.translate(
49 | "DistCartogramDialogBase", "Grid precision"
50 | )
51 | self.assertEqual(real_message, expected_message)
52 |
53 |
54 | if __name__ == "__main__":
55 | suite = unittest.makeSuite(SafeTranslationsTest)
56 | runner = unittest.TextTestRunner(verbosity=2)
57 | runner.run(suite)
58 |
--------------------------------------------------------------------------------
/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 PyQt5 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 |
--------------------------------------------------------------------------------
/dist_cartogram_dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | DistCartogramDialog
5 | A QGIS plugin
6 | Compute distance cartogram
7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
8 | -------------------
9 | begin : 2018-07-13
10 | git sha : $Format:%H$
11 | copyright : (C) 2018 by Matthieu Viry
12 | email : matthieu.viry@cnrs.fr
13 | ***************************************************************************/
14 |
15 | /***************************************************************************
16 | * *
17 | * This program is free software; you can redistribute it and/or modify *
18 | * it under the terms of the GNU General Public License as published by *
19 | * the Free Software Foundation; either version 2 of the License, or *
20 | * (at your option) any later version. *
21 | * *
22 | ***************************************************************************/
23 | """
24 |
25 | from PyQt5 import QtWidgets
26 |
27 | try:
28 | from .dist_cartogram_dialog_baseUi import Ui_DistCartogramDialogBase
29 | except:
30 | from dist_cartogram_dialog_baseUi import Ui_DistCartogramDialogBase
31 |
32 |
33 | class DistCartogramDialog(QtWidgets.QDialog, Ui_DistCartogramDialogBase):
34 | def __init__(self, parent=None):
35 | """Constructor."""
36 | super(DistCartogramDialog, 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/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 |
46 | file_path = os.path.abspath(
47 | os.path.join(os.path.dirname(__file__), os.pardir, "metadata.txt")
48 | )
49 | LOGGER.info(file_path)
50 | metadata = []
51 | parser = configparser.ConfigParser()
52 | parser.optionxform = str
53 | parser.read(file_path)
54 | message = 'Cannot find a section named "general" in %s' % file_path
55 | assert parser.has_section("general"), message
56 | metadata.extend(parser.items("general"))
57 |
58 | for expectation in required_metadata:
59 | message = 'Cannot find metadata "%s" in metadata source (%s).' % (
60 | expectation,
61 | file_path,
62 | )
63 |
64 | self.assertIn(expectation, dict(metadata), message)
65 |
66 |
67 | if __name__ == "__main__":
68 | unittest.main()
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## QgisDistanceCartogramPlugin
2 |
3 |
4 | **DistanceCartogram QGIS plugin** aims to create what is often defined as a **distance cartogram**.
5 |
6 | This is done by extending (by interpolation) to the layer(s) of the study area (territorial divisions, network...) the
7 | local displacement between the source coordinates and the image coordinates, derived from the distances between each pair
8 | of homologous points (source / image points).
9 |
10 | The relation between the source points and the image points must depend on the studied theme: positions in access time or estimated positions in spatial cognition for example.
11 |
12 | **DistanceCartogram QGIS plugin** is currently available in two languages (English and French) and allows you to create distance cartograms in two ways:
13 |
14 | * by providing **2 layers of homologous points** : the source points and the image points,
15 | * by providing a **layer of points** and the durations between a reference point and the other points of the layer (used to create the image points layer).
16 |
17 |
18 | 
19 |
20 | 
21 |
22 | ## About the method
23 |
24 | This plugin is a port of [Darcy](https://thema.univ-fcomte.fr/productions/software/darcy/) software regarding the bidimensional regression and the backgrounds layers deformation.
25 |
26 | All credit for the contribution of the methode goes to **Waldo Tobler** *(University of California, Santa Barbara)* and **Colette Cauvin** *(Théma - Univ. Franche-Comté)* and for the reference Java implementation goes to **Gilles Vuidel** *(Théma - Univ. Franche-Comté)*.
27 |
28 | ## Installation
29 |
30 | This plugin is available in the official [QGIS plugin repository](https://plugins.qgis.org/plugins/dist_cartogram/).
31 |
32 | To install the plugin, you can use the QGIS plugin manager and simply search for `DistanceCartogram`.
33 |
34 | ## Instruction for developers
35 |
36 | To install the plugin for development, you can clone the repository and manage the various actions with the `Makefile` provided.
37 |
38 | Note that you need to have:
39 | - pyqt5-dev-tools installed (`sudo apt install pyqt5-dev-tools`) to use the `pyrcc5` command,
40 | - sphinx installed (`pip install sphinx` or `sudo apt install python3-sphinx`) to generate the documentation.
41 |
42 | ## License
43 |
44 | [GPL-3.0](LICENSE).
45 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changes
2 | =======
3 |
4 | 0.8 (2025-01-20)
5 | ----------------
6 |
7 | - Fix the reading of the CSV durations matrix when the first cell isn't empty.
8 |
9 | - Fix the listening of changes in the QlistWidget that allows the user to select the layer to deform.
10 |
11 | - Improve progression of the progress bar during the cartogram creation.
12 |
13 | - Make the main QDialog non-resizable (since the content does not resize according to the size of the QDialog).
14 |
15 |
16 | 0.7 (2025-01-08)
17 | ----------------
18 |
19 | - Replace median with mean for computing the displacement of points for unipolar distance cartogram (from a layer of points and a duration matrix).
20 |
21 | - Enable the deformation of multiple background layers (previously only one background layer was allowed).
22 |
23 | - Improve wording in the README and in the documentation.
24 |
25 |
26 | 0.6 (2024-12-09)
27 | ----------------
28 |
29 | - Fix bug in the `add` method of Rectangle2D.
30 |
31 | - Fix links to Darcy software in README and in documentation.
32 |
33 | - Format code with `black` + apply some `ruff` suggestions.
34 |
35 |
36 | 0.5 (2023-01-05)
37 | ----------------
38 |
39 | - Skip points with null / empty geometry when creating layer of 'image' points
40 | (fixes a bug occurring when the layer of 'source' points contains empty geometries).
41 |
42 |
43 | 0.4 (2022-12-29)
44 | -----------------
45 |
46 | - Fixes the displacement of source point when the image point is very distant.
47 |
48 | - Fix some numpy deprecation warning.
49 |
50 | - Slightly change strategy for activating the OK button in the dialog.
51 |
52 | - Use __geo_interface__ instead of asJson when extracting coordinates of source / image points.
53 |
54 |
55 | 0.3 (2022-12-26)
56 | ------------------
57 |
58 | - Fix bug with displacement in some conditions
59 |
60 | - Fix bug with argument of progressBar setMaximum (which expects integer value).
61 |
62 |
63 | 0.2 (2018-11-23)
64 | ------------------
65 |
66 | - Ensure the background layer and the point layer are in the same projected CRS.
67 |
68 | - Allow to use non-squared distance matrix as input (#3).
69 |
70 | - Enhance the reference feature selection by sorting the list alphabetically (#4).
71 |
72 | - Ensure the "sample dataset" dialog is put on top of the current Qgis window.
73 |
74 |
75 | 0.1 (2018-08-29)
76 | ------------------
77 |
78 | - First Release.
79 |
--------------------------------------------------------------------------------
/dist_cartogram_dataset_boxUi.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from PyQt5 import QtCore, QtWidgets
3 |
4 |
5 | class Ui_Dialog(object):
6 | def setupUi(self, Dialog):
7 | Dialog.setObjectName("Dialog")
8 | Dialog.resize(620, 300)
9 | self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
10 | self.buttonBox.setGeometry(QtCore.QRect(20, 265, 580, 32))
11 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
12 | self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Ok)
13 | self.buttonBox.setObjectName("buttonBox")
14 | self.mainContentLabel = QtWidgets.QLabel(Dialog)
15 | self.mainContentLabel.setGeometry(QtCore.QRect(20, 10, 590, 191))
16 | self.mainContentLabel.setObjectName("mainContentLabel")
17 | self.matrixPathTextEdit = QtWidgets.QTextEdit(Dialog)
18 | self.matrixPathTextEdit.setGeometry(QtCore.QRect(20, 210, 580, 49))
19 | self.matrixPathTextEdit.setReadOnly(True)
20 | self.matrixPathTextEdit.setTextInteractionFlags(
21 | QtCore.Qt.TextSelectableByKeyboard | QtCore.Qt.TextSelectableByMouse
22 | )
23 | self.matrixPathTextEdit.setObjectName("matrixPathTextEdit")
24 | self.retranslateUi(Dialog)
25 | self.buttonBox.accepted.connect(Dialog.accept)
26 | self.buttonBox.rejected.connect(Dialog.reject)
27 | QtCore.QMetaObject.connectSlotsByName(Dialog)
28 |
29 | def retranslateUi(self, Dialog):
30 | _translate = QtCore.QCoreApplication.translate
31 | Dialog.setWindowTitle(_translate("Dialog", "Distance Cartogram sample dataset"))
32 | self.mainContentLabel.setText(
33 | _translate(
34 | "Dialog",
35 | '
Two layers have been added.
- "department" is a layer of MultiPolygons. This is the background layer to be deformed.
- "prefecture" is a layer of Points. It is between these points that the matrix of travel time by road was calculated. Its identifier field to use is "NOM_COM".
To use Distance Cartogram you will also need to add the travel time matrix. It\'s path is:
',
36 | )
37 | )
38 |
39 |
40 | class DatasetDialog(QtWidgets.QDialog, Ui_Dialog):
41 | def __init__(self, parent=None):
42 | """Constructor."""
43 | super(DatasetDialog, self).__init__(parent)
44 | self.setupUi(self)
45 |
--------------------------------------------------------------------------------
/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 xmlrpc.client
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 | )
35 | print("Connecting to: %s" % hide_password(address))
36 |
37 | server = xmlrpc.client.ServerProxy(address, verbose=VERBOSE)
38 |
39 | try:
40 | plugin_id, version_id = server.plugin.upload(
41 | xmlrpc.client.Binary(open(arguments[0]).read())
42 | )
43 | print("Plugin ID: %s" % plugin_id)
44 | print("Version ID: %s" % version_id)
45 | except xmlrpc.client.ProtocolError as err:
46 | print("A protocol error occurred")
47 | print("URL: %s" % hide_password(err.url, 0))
48 | print("HTTP/HTTPS headers: %s" % err.headers)
49 | print("Error code: %d" % err.errcode)
50 | print("Error message: %s" % err.errmsg)
51 | except xmlrpc.client.Fault as err:
52 | print("A fault occurred")
53 | print("Fault code: %d" % err.faultCode)
54 | print("Fault string: %s" % err.faultString)
55 |
56 |
57 | def hide_password(url, start=6):
58 | """Returns the http url with password part replaced with '*'.
59 |
60 | :param url: URL to upload the plugin to.
61 | :type url: str
62 |
63 | :param start: Position of start of password.
64 | :type start: int
65 | """
66 | start_position = url.find(":", start) + 1
67 | end_position = url.find("@")
68 | return "%s%s%s" % (
69 | url[:start_position],
70 | "*" * (end_position - start_position),
71 | url[end_position:],
72 | )
73 |
74 |
75 | if __name__ == "__main__":
76 | parser = OptionParser(usage="%prog [options] plugin.zip")
77 | parser.add_option(
78 | "-w",
79 | "--password",
80 | dest="password",
81 | help="Password for plugin site",
82 | metavar="******",
83 | )
84 | parser.add_option(
85 | "-u",
86 | "--username",
87 | dest="username",
88 | help="Username of plugin site",
89 | metavar="user",
90 | )
91 | parser.add_option(
92 | "-p", "--port", dest="port", help="Server port to connect to", metavar="80"
93 | )
94 | parser.add_option(
95 | "-s",
96 | "--server",
97 | dest="server",
98 | help="Specify server name",
99 | metavar="plugins.qgis.org",
100 | )
101 | options, args = parser.parse_args()
102 | if len(args) != 1:
103 | print("Please specify zip file.\n")
104 | parser.print_help()
105 | sys.exit(1)
106 | if not options.server:
107 | options.server = SERVER
108 | if not options.port:
109 | options.port = PORT
110 | if not options.username:
111 | # interactive mode
112 | username = getpass.getuser()
113 | print("Please enter user name [%s] :" % username, end=" ")
114 | res = input()
115 | if res != "":
116 | options.username = res
117 | else:
118 | options.username = username
119 | if not options.password:
120 | # interactive mode
121 | options.password = getpass.getpass()
122 | main(options, args)
123 |
--------------------------------------------------------------------------------
/help/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
10 | if NOT "%PAPER%" == "" (
11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
12 | )
13 |
14 | if "%1" == "" goto help
15 |
16 | if "%1" == "help" (
17 | :help
18 | echo.Please use `make ^` where ^ is one of
19 | echo. html to make standalone HTML files
20 | echo. dirhtml to make HTML files named index.html in directories
21 | echo. singlehtml to make a single large HTML file
22 | echo. pickle to make pickle files
23 | echo. json to make JSON files
24 | echo. htmlhelp to make HTML files and a HTML help project
25 | echo. qthelp to make HTML files and a qthelp project
26 | echo. devhelp to make HTML files and a Devhelp project
27 | echo. epub to make an epub
28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
29 | echo. text to make text files
30 | echo. man to make manual pages
31 | echo. changes to make an overview over all changed/added/deprecated items
32 | echo. linkcheck to check all external links for integrity
33 | echo. doctest to run all doctests embedded in the documentation if enabled
34 | goto end
35 | )
36 |
37 | if "%1" == "clean" (
38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
39 | del /q /s %BUILDDIR%\*
40 | goto end
41 | )
42 |
43 | if "%1" == "html" (
44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
45 | echo.
46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
47 | goto end
48 | )
49 |
50 | if "%1" == "dirhtml" (
51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
52 | echo.
53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
54 | goto end
55 | )
56 |
57 | if "%1" == "singlehtml" (
58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
59 | echo.
60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
61 | goto end
62 | )
63 |
64 | if "%1" == "pickle" (
65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
66 | echo.
67 | echo.Build finished; now you can process the pickle files.
68 | goto end
69 | )
70 |
71 | if "%1" == "json" (
72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
73 | echo.
74 | echo.Build finished; now you can process the JSON files.
75 | goto end
76 | )
77 |
78 | if "%1" == "htmlhelp" (
79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
80 | echo.
81 | echo.Build finished; now you can run HTML Help Workshop with the ^
82 | .hhp project file in %BUILDDIR%/htmlhelp.
83 | goto end
84 | )
85 |
86 | if "%1" == "qthelp" (
87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
88 | echo.
89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
90 | .qhcp project file in %BUILDDIR%/qthelp, like this:
91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\template_class.qhcp
92 | echo.To view the help file:
93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\template_class.ghc
94 | goto end
95 | )
96 |
97 | if "%1" == "devhelp" (
98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
99 | echo.
100 | echo.Build finished.
101 | goto end
102 | )
103 |
104 | if "%1" == "epub" (
105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
106 | echo.
107 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
108 | goto end
109 | )
110 |
111 | if "%1" == "latex" (
112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
113 | echo.
114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
115 | goto end
116 | )
117 |
118 | if "%1" == "text" (
119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
120 | echo.
121 | echo.Build finished. The text files are in %BUILDDIR%/text.
122 | goto end
123 | )
124 |
125 | if "%1" == "man" (
126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
127 | echo.
128 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
129 | goto end
130 | )
131 |
132 | if "%1" == "changes" (
133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
134 | echo.
135 | echo.The overview file is in %BUILDDIR%/changes.
136 | goto end
137 | )
138 |
139 | if "%1" == "linkcheck" (
140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
141 | echo.
142 | echo.Link check complete; look for any errors in the above output ^
143 | or in %BUILDDIR%/linkcheck/output.txt.
144 | goto end
145 | )
146 |
147 | if "%1" == "doctest" (
148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
149 | echo.
150 | echo.Testing of doctests in the sources finished, look at the ^
151 | results in %BUILDDIR%/doctest/output.txt.
152 | goto end
153 | )
154 |
155 | :end
156 |
--------------------------------------------------------------------------------
/help/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
14 |
15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
16 |
17 | help:
18 | @echo "Please use \`make ' where is one of"
19 | @echo " html to make standalone HTML files"
20 | @echo " dirhtml to make HTML files named index.html in directories"
21 | @echo " singlehtml to make a single large HTML file"
22 | @echo " pickle to make pickle files"
23 | @echo " json to make JSON files"
24 | @echo " htmlhelp to make HTML files and a HTML help project"
25 | @echo " qthelp to make HTML files and a qthelp project"
26 | @echo " devhelp to make HTML files and a Devhelp project"
27 | @echo " epub to make an epub"
28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
29 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
30 | @echo " text to make text files"
31 | @echo " man to make manual pages"
32 | @echo " changes to make an overview of all changed/added/deprecated items"
33 | @echo " linkcheck to check all external links for integrity"
34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
35 |
36 | clean:
37 | -rm -rf $(BUILDDIR)/*
38 |
39 | html:
40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
41 | @echo
42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
43 |
44 | dirhtml:
45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
48 |
49 | singlehtml:
50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
51 | @echo
52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
53 |
54 | pickle:
55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
56 | @echo
57 | @echo "Build finished; now you can process the pickle files."
58 |
59 | json:
60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
61 | @echo
62 | @echo "Build finished; now you can process the JSON files."
63 |
64 | htmlhelp:
65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
66 | @echo
67 | @echo "Build finished; now you can run HTML Help Workshop with the" \
68 | ".hhp project file in $(BUILDDIR)/htmlhelp."
69 |
70 | qthelp:
71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
72 | @echo
73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/template_class.qhcp"
76 | @echo "To view the help file:"
77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/template_class.qhc"
78 |
79 | devhelp:
80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
81 | @echo
82 | @echo "Build finished."
83 | @echo "To view the help file:"
84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/template_class"
85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/template_class"
86 | @echo "# devhelp"
87 |
88 | epub:
89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
90 | @echo
91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
92 |
93 | latex:
94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
95 | @echo
96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
98 | "(use \`make latexpdf' here to do that automatically)."
99 |
100 | latexpdf:
101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
102 | @echo "Running LaTeX files through pdflatex..."
103 | make -C $(BUILDDIR)/latex all-pdf
104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
105 |
106 | text:
107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
108 | @echo
109 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
110 |
111 | man:
112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
113 | @echo
114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
115 |
116 | changes:
117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
118 | @echo
119 | @echo "The overview file is in $(BUILDDIR)/changes."
120 |
121 | linkcheck:
122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
123 | @echo
124 | @echo "Link check complete; look for any errors in the above output " \
125 | "or in $(BUILDDIR)/linkcheck/output.txt."
126 |
127 | doctest:
128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
129 | @echo "Testing of doctests in the sources finished, look at the " \
130 | "results in $(BUILDDIR)/doctest/output.txt."
131 |
--------------------------------------------------------------------------------
/utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from PyQt5.QtCore import QVariant
3 | from .grid import Point
4 | from qgis.core import (
5 | QgsFeature,
6 | QgsFeatureSink,
7 | QgsGeometry,
8 | QgsRectangle,
9 | QgsVectorLayer,
10 | )
11 |
12 |
13 | def extrapole_line(p1, p2, ratio):
14 | return QgsGeometry.fromWkt(
15 | """LINESTRING ({} {}, {} {})""".format(
16 | p1[0],
17 | p1[1],
18 | p1[0] + ratio * (p2[0] - p1[0]),
19 | p1[1] + ratio * (p2[1] - p1[1]),
20 | )
21 | )
22 |
23 |
24 | def get_merged_extent(layers):
25 | extent = QgsRectangle()
26 | for layer in layers:
27 | extent.combineExtentWith(layer.extent())
28 | return extent
29 |
30 |
31 | def get_total_features(layers):
32 | total = 0
33 | for layer in layers:
34 | total += layer.featureCount()
35 | return total
36 |
37 |
38 | def extract_source_image(source_lyr, image_lyr, id_source, id_image):
39 | source_to_use = []
40 | image_to_use = []
41 | temp_source = []
42 | temp_image = {}
43 |
44 | for ft in source_lyr.getFeatures():
45 | temp_source.append(
46 | (ft[id_source], ft.geometry().__geo_interface__["coordinates"])
47 | )
48 |
49 | for ft in image_lyr.getFeatures():
50 | temp_image[ft[id_image]] = ft.geometry().__geo_interface__["coordinates"]
51 |
52 | for _id_source, geom_source in temp_source:
53 | geom_image = temp_image.get(_id_source, None)
54 | if not geom_image:
55 | continue
56 | source_to_use.append(Point(geom_source[0], geom_source[1]))
57 | image_to_use.append(Point(geom_image[0], geom_image[1]))
58 |
59 | return (source_to_use, image_to_use)
60 |
61 |
62 | def create_image_points(
63 | source_layer,
64 | id_field,
65 | mat_extract,
66 | id_ref_feature,
67 | dest_idx,
68 | factor,
69 | display_image_points,
70 | ):
71 | type_id_field = [
72 | i.typeName().lower()
73 | for i in source_layer.fields().toList()
74 | if i.name() == id_field
75 | ][0]
76 | source_layer_dict = {}
77 |
78 | for ft in source_layer.getFeatures():
79 | id_value = str(ft[id_field])
80 | if id_value not in dest_idx:
81 | continue
82 | source_layer_dict[id_value] = {
83 | "geometry": ft.geometry(),
84 | "dist_euclidienne": None,
85 | "deplacement": None,
86 | "time": mat_extract[dest_idx[id_value]],
87 | }
88 | ref_geometry = source_layer_dict[id_ref_feature]["geometry"]
89 | for ix in source_layer_dict.keys():
90 | if ix == id_ref_feature:
91 | continue
92 | source_layer_dict[ix]["dist_euclidienne"] = ref_geometry.distance(
93 | source_layer_dict[ix]["geometry"]
94 | )
95 | source_layer_dict[ix]["vitesse"] = (
96 | source_layer_dict[ix]["dist_euclidienne"] / source_layer_dict[ix]["time"]
97 | )
98 | ref_vitesse = np.nanmean(
99 | [i["vitesse"] for i in source_layer_dict.values() if "vitesse" in i]
100 | )
101 | for ix in source_layer_dict.keys():
102 | if ix == id_ref_feature:
103 | continue
104 | source_layer_dict[ix]["deplacement"] = (
105 | ref_vitesse / source_layer_dict[ix]["vitesse"]
106 | )
107 | source_to_use, image_to_use = [], []
108 | unused_point = 0
109 | res_geoms = []
110 | ids = []
111 | image_layer = None
112 | coords = ref_geometry.__geo_interface__["coordinates"]
113 | x1, y1 = coords[0], coords[1]
114 | p1 = (x1, y1)
115 | for ix in source_layer_dict.keys():
116 | if ix == id_ref_feature:
117 | ids.append(ix)
118 | if display_image_points:
119 | res_geoms.append(ref_geometry)
120 | source_to_use.append(Point(x1, y1))
121 | image_to_use.append(Point(x1, y1))
122 | continue
123 | item = source_layer_dict[ix]
124 | deplacement = item["deplacement"]
125 | if not item["geometry"].isNull() and not item["geometry"].isEmpty():
126 | coords = item["geometry"].__geo_interface__["coordinates"]
127 | if deplacement < 1:
128 | deplacement = 1 + (deplacement - 1) * factor
129 | li = QgsGeometry.fromWkt(
130 | """LINESTRING ({} {}, {} {})""".format(
131 | p1[0], p1[1], coords[0], coords[1]
132 | )
133 | )
134 | p = li.interpolate(deplacement * item["dist_euclidienne"])
135 | else:
136 | deplacement = 1 + (deplacement - 1) * factor
137 | p2 = (coords[0], coords[1])
138 | li = extrapole_line(p1, p2, 2 * deplacement)
139 | p = li.interpolate(deplacement * item["dist_euclidienne"])
140 | _coords = p.__geo_interface__["coordinates"]
141 | ids.append(ix)
142 | if display_image_points:
143 | res_geoms.append(p)
144 | source_to_use.append(Point(coords[0], coords[1]))
145 | image_to_use.append(Point(_coords[0], _coords[1]))
146 | else:
147 | unused_point += 1
148 |
149 | if display_image_points:
150 | image_layer = QgsVectorLayer(
151 | "Point?crs={}&field={}:{}".format(
152 | source_layer.crs().authid(), id_field, type_id_field
153 | ),
154 | "image_layer",
155 | "memory",
156 | )
157 |
158 | image_layer.startEditing()
159 | image_layer.setCrs(source_layer.crs())
160 |
161 | for ix, geom in zip(ids, res_geoms):
162 | feature = QgsFeature()
163 | feature.setGeometry(geom)
164 | feature.setAttributes([QVariant(ix)])
165 | image_layer.addFeature(feature, QgsFeatureSink.FastInsert)
166 | image_layer.commitChanges()
167 |
168 | return (source_to_use, image_to_use, image_layer, unused_point)
169 |
--------------------------------------------------------------------------------
/data/mat_incomplete.csv:
--------------------------------------------------------------------------------
1 | ,RODEZ,LE PUY-EN-VELAY,POITIERS,VALENCE,CRETEIL,CARCASSONNE,CHAUMONT,CHARTRES,BLOIS,BOBIGNY,VANNES,BOURGES,SAINT-ETIENNE,BORDEAUX,PERPIGNAN,AVIGNON,ALBI,PONTOISE,PARIS,MONTPELLIER,STRASBOURG,SAINT-LO,NIMES,VERSAILLES,AUXERRE,COLMAR,LAON,CAHORS,BAR-LE-DUC,NICE,TARBES,EVREUX,CLERMONT-FERRAND,LA ROCHELLE,DIGNE-LES-BAINS,MONT-DE-MARSAN,NANTERRE,AUCH,CHALONS-EN-CHAMPAGNE,EVRY,TOULOUSE,ROUEN,MARSEILLE-1ER-ARRONDISSEMENT,CAEN,LIMOGES,ANGERS,MENDE,AMIENS,TULLE,NEVERS,ANGOULEME,CHATEAUROUX,METZ,TROYES,MACON,BELFORT,LILLE,ARRAS,EPINAL,TOULON,BOURG-EN-BRESSE,LAVAL,GAP,LYON-1ER-ARRONDISSEMENT,NANTES,AGEN,NANCY,PERIGUEUX,DIJON,ALENCON,ANNECY,CHARLEVILLE-MEZIERES,QUIMPER,GRENOBLE,LONS-LE-SAUNIER,NIORT,MOULINS,FOIX,GUERET,ORLEANS,LE MANS,VESOUL,BESANCON,PAU,SAINT-BRIEUC,AURILLAC,PRIVAS,CHAMBERY,MONTAUBAN,LA ROCHE-SUR-YON,BEAUVAIS,MELUN,TOURS,RENNES
2 | RODEZ,0,173.4,307.3,252.8,414.1,167.1,435.2,386.7,349,426.5,517.9,275.4,234,249.9,185.8,199.9,65.5,446.9,418.7,126.3,551.2,561.3,169.8,412.7,387.6,506.3,522.6,104.8,510.9,342.9,215.4,453.3,168.1,361.1,304.5,262,424.5,184.6,505.2,406,118.3,490.3,242,522.7,198.6,425,92.7,514,163.5,290.8,272.1,268.7,534.6,452.3,296.5,458.1,556.2,535.6,500,279.7,319.4,474.4,330.8,273.7,444.4,171.5,497,189.2,371.2,455.9,356.3,562.2,602.4,312.3,356.8,345.7,242,164.4,252.5,336.7,422.9,440.9,402.5,234,587.4,94.1,230.6,331.1,115.2,407.3,479.1,418.4,361.6,519.9
3 | LE PUY-EN-VELAY,173.1,0,337.6,112.3,358.8,277.2,298,331.4,293.7,371.3,499,220.1,65.7,353.3,279.4,179.8,235.4,391.6,363.4,217.4,411.7,506,178.2,357.5,295.4,366.8,442.9,274.7,373.7,332.5,385.2,398,112.8,434,293,428.7,369.2,354.5,383.1,350.8,288.2,435,231.6,467.4,263.1,371.5,89.2,458.8,214,235.5,344.7,248.8,397.4,341.1,159.3,318.6,500.9,480.3,362.9,269.3,165.8,419.1,256.9,118.6,429.6,341.4,359.8,275.7,234,400.6,188,458.6,583.4,170.6,206.4,390.4,186.7,334.3,217.2,281.4,367.6,301.4,263,403.9,532.1,160.9,96.6,162.8,285.1,439.2,423.8,363.1,306.3,464.6
4 | POITIERS,307.7,336.8,0,402.5,213.3,348.8,315.3,185.8,109.8,225.7,220.3,153,321,160.2,416.7,474.6,316.7,246,217.8,431.1,502.8,268.8,469.5,211.9,235.7,478.7,321.8,233.2,365.8,627.2,307.8,252.5,235,104.6,561.6,239.1,223.7,305.3,315.2,205.2,294.5,256.9,526.3,230.2,112.1,126.9,353.7,313.2,176.4,217.7,92,114.6,411.1,262.4,322.6,430.6,355.4,334.7,403.5,564.1,347.8,181.9,510.2,337.4,146.8,243.1,400.4,169,319.4,163.3,425.2,361.3,304.7,407.8,380.1,61,225.9,346.1,126.5,141.8,130.4,395,375,284.2,291.8,251.9,420.5,400,255.6,109.7,278.3,224,69,224.5
5 | VALENCE,252.6,114.1,402.6,0,353.4,212.5,258.2,381.4,358.7,365.8,564,285.1,90.5,411.2,214.6,83.1,295.6,388.9,357.8,128.5,371.9,541.8,98.1,359.9,255.6,326.9,403.1,338.3,333.9,235.8,355.8,422.5,180.3,499,194,423.2,370.5,333.3,343.3,334.3,264.4,441.9,134.9,499.6,328.8,436.5,181.2,453.1,279.6,266.4,410.3,313.8,357.5,301.3,119.5,278.8,495.5,474.9,323,172.6,125.9,484.1,153.6,78.8,494.6,332.8,319.9,341.4,194.1,465.6,131.4,418.8,648.4,65.1,166.5,455.4,222.2,276.3,282.2,346.4,432.6,261.6,223.2,374.5,597.1,259.9,40.5,105.7,298.5,504.2,418.2,335.2,371.2,529.6
6 | CRETEIL,414.7,359,214.5,354.4,0,472.8,173.4,62.8,121.1,20.4,300.1,157.4,331.9,355.4,521,426.5,440.6,47.7,17.9,461.5,296.7,210.9,441.5,30.7,113,332.9,114.4,357.1,175.1,579.1,503,91.5,257.2,299.8,508,434.3,34.3,469.2,115.5,26.3,418.4,111,478.2,168.7,241.2,183.1,375.9,110.6,300.3,155.9,291.7,171,202.2,120.4,248.6,307.8,148,127.4,261.5,516,265,176.9,456.6,287.1,241.2,435.8,235,332.2,196.6,158.4,347.4,152.5,372,354.3,257.3,256.2,198.1,470.1,242.8,94,138.4,253,252.2,479.4,290,375.8,383.9,346.5,379.5,261,75.7,36.3,147.8,222.4
7 | CARCASSONNE,166.7,276.4,347.8,213.2,472.2,0,460.2,444.8,407.1,484.7,487.7,369.8,292.5,208.6,77.3,160.2,105.3,505,476.8,102.4,573.9,580.5,130.1,470.9,457.6,529,580.7,135.8,535.9,303.2,153.2,511.4,271,319.8,264.8,220.6,482.6,130.7,545.3,464.2,61.8,548.4,202.3,566.6,239.2,443.2,195.6,572.2,215.8,393.7,282.1,309.2,559.6,503.3,321.5,480.8,614.3,593.7,525,240,328,511.1,291.1,280.8,414.1,130.2,522,229.7,396.2,499.8,338.9,620.3,572.1,272.6,368.6,317.1,345,73.7,293,394.8,466.8,463.6,425.2,171.9,559.2,246,219.6,313.3,95.9,377.1,537.2,483,405.5,491.9
8 | CHAUMONT,438.4,299,316,259.8,173.6,461.2,0,209.2,222.6,183.8,446.5,242.6,237.3,456.9,463.4,331.9,500.7,211.1,185.9,377.3,207.9,369.9,346.9,188,134.5,198.7,173,445.4,87.3,484.5,601.1,250.6,279.1,401.3,413.4,535.8,198.6,557.5,113.2,159.5,506.7,270.1,383.6,327.7,342.7,329.6,383.1,238.4,365,224.2,393.2,272.4,143.1,71.2,154.1,153.2,266,245.3,108.6,421.4,162.6,323.4,362,192.5,387.6,524.1,105.5,426.8,79,304.9,241.6,188.8,518.4,259.7,128.5,357.6,228.3,525,317.6,195.5,284.9,97.8,124.9,580.9,436.4,403.6,289.3,251.9,467.8,403,239.7,146.1,249.3,368.9
9 | CHARTRES,386.2,330.4,185.9,380.1,62.5,444.2,208.4,0,92.6,74.9,247.4,128.8,314.6,326.8,492.4,452.2,412,89.3,67.1,432.9,354.7,204.4,467.2,58.3,138.7,381.7,171,328.5,233.2,604.8,474.4,69.8,228.6,260.4,533.7,405.7,72.9,440.6,173.6,54.4,389.8,106.8,503.9,164.4,212.6,130.5,347.3,161.9,271.8,171,263.1,142.4,260.3,155.5,274.4,333.5,204.6,184,296.6,541.7,290.7,124.3,482.3,312.8,188.5,407.2,293.1,303.6,222.3,105.7,373.1,210.5,319.3,380,283,227.6,213.2,441.5,214.3,65.4,85.7,288.1,277.9,450.9,237.3,347.2,409.6,372.2,350.9,208.3,127,73.2,119.2,169.8
10 | BLOIS,353.1,297.4,110.5,363.1,119.7,408.5,221.8,92.3,0,132.2,256.3,95.8,281.6,251.5,459.4,435.1,376.4,152.5,124.3,399.9,409.3,231.4,439.1,118.4,142.2,385.2,228.2,292.9,272.3,587.8,399.1,159,195.6,195.8,522.1,330.4,130.2,396.6,221.7,111.7,354.2,195.9,486.9,192.8,177,128.8,314.3,219.7,236.1,160.5,187.8,102.5,317.5,168.9,277.9,337.1,261.9,241.2,310,524.6,294.3,144.5,470.7,298,186.9,334.4,306.9,264.8,225.9,126,376.7,267.8,339.6,368.4,286.6,152.2,184.1,405.8,178.6,48.3,93,301.5,281.5,375.5,257.6,311.6,381.1,360.6,315.3,197.6,184.7,130.5,43.9,190
11 | BOBIGNY,424.8,369,224.5,364.2,19.4,482.8,183.3,72.8,131.2,0,310.1,167.4,341.7,365.4,531.1,436.3,450.7,30.7,14.1,471.6,300,200.9,451.3,35.9,122.8,336.3,102.1,367.2,178.5,589,513,81.6,267.2,309.8,517.9,444.4,24.5,479.3,118.9,40.4,428.5,101.1,488,158.7,251.3,193.2,385.9,94.2,310.4,165.7,301.7,181,205.6,130.3,258.5,317.6,135.7,115,271.4,525.8,274.8,187,466.5,296.9,251.2,445.8,238.4,342.2,206.4,163.2,357.2,155.8,382,364.1,267.1,266.2,207.9,480.1,252.9,104,148.5,262.9,262,489.5,300,385.8,393.7,356.3,389.6,271.1,59.3,46.2,157.8,232.5
12 |
--------------------------------------------------------------------------------
/help/source/index.rst:
--------------------------------------------------------------------------------
1 | .. DistanceCartogram 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 | DistanceCartogram QGIS Plugin Documentation
7 | ============================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 |
12 | .. index:: Introduction
13 |
14 | Introduction
15 | =================
16 |
17 | **DistanceCartogram QGIS plugin** aims to create what is often defined as a **distance cartogram**.
18 |
19 | This is done by extending (by interpolation) to the layer(s) of the study area (territorial divisions, network...) the
20 | local displacement between the source coordinates and the image coordinates, derived from the distances between each pair
21 | of homologous points (source / image points).
22 |
23 | The relation between the source points and the image points must depend on the studied theme: positions in access time or estimated positions in spatial cognition for example.
24 |
25 | **DistanceCartogram QGIS plugin** is currently available in two languages (English and French) and allows you to create distance cartograms in two ways:
26 |
27 | * by providing **2 layers of homologous points** : the source points and the image points,
28 | * by providing a **layer of points** and the durations between a reference point and the other points of the layer (used to create the image points layer).
29 |
30 |
31 | Notes:
32 |
33 | * This is a port of Darcy_ software regarding the bidimensional regression and the backgrounds layers deformation. All credit goes to *Waldo Tobler* and *Colette Cauvin* for the contribution of the method and to *Gilles Vuidel* for the reference implementation.
34 | * The way the points are moved from the time matrix is quite simple and is explained below. Other methods exists and could be implemented (both in this plugin or by the user while preparing its dataset).
35 |
36 |
37 | .. image:: img/screenshot500.png
38 | :align: center
39 |
40 | .. _Darcy: https://sourcesup.renater.fr/www/transcarto/darcy/
41 | .. _Multidimensional scaling (MDS): https://en.wikipedia.org/wiki/Multidimensional_scaling
42 |
43 |
44 | .. index:: Data
45 |
46 | Expected data
47 | =================
48 |
49 | Example 1
50 | ^^^^^^^^^
51 | - by providing a **layer of points** and a **time matrix between them**:
52 |
53 | .. figure:: img/source_point_table.png
54 | :figwidth: 50%
55 | :align: center
56 |
57 | The source point layer must contain an unique identifier field. These identifiers must match with those used in the matrix.
58 |
59 | .. image:: img/matrix.png
60 | :width: 40%
61 | :align: center
62 |
63 | Example 2
64 | ^^^^^^^^^
65 | - by providing the source points layer and the image points layer:
66 |
67 | .. figure:: img/from_2_points_layers.png
68 | :figwidth: 60%
69 | :align: center
70 |
71 | The source points layer (source_pref, the real location of the prefecture) and the image points layer (image_pref, the computed location of the prefecture given some time-distance data).
72 |
73 | .. figure:: img/from_2_points_layers2.png
74 | :figwidth: 60%
75 | :align: center
76 |
77 | These two layers must have a field containing an unique identifier to match them (here INSEE_COM in both layers).
78 |
79 | .. image:: img/from_2_points_layers3.png
80 | :width: 38%
81 | :align: center
82 |
83 | .. index:: Displacement
84 |
85 | Points displacement from time matrix
86 | =========================================
87 |
88 | The method we propose allows the use of a travel time matrix to move the points of the dataset around a reference point (whose location will remain unchanged).
89 | The calculation of the new position of the points is done in several steps:
90 |
91 | * Calculation of the Euclidean distance between the reference point (*labeled* **64445** *in our example*) and each of the other points.
92 | * Use of travel times and this distance to calculate a speed between the reference point and each of the other points
93 | * Calculation of the average speed
94 | * Calculation of a displacement coefficient to be applied to each point: the ratio between the speed associated with each point and the average speed
95 |
96 | .. figure:: img/a.png
97 | :width: 60%
98 | :align: center
99 |
100 | The source dataset with the reference point: **64445**.
101 |
102 | .. figure:: img/ui.png
103 | :width: 30%
104 | :align: center
105 |
106 | Selection of the feature **64445** as reference point in :guilabel:`DistanceCartogram` interface.
107 |
108 | .. figure:: img/b.png
109 | :width: 60%
110 | :align: center
111 |
112 | The points are moved according to their travel time from the reference point. Some points of this "image" layer are closer, others are farther from the reference point.
113 |
114 | .. figure:: img/b2.png
115 | :width: 60%
116 | :align: center
117 |
118 | Points with a speed greater than the mean speed are getting closer, other are getting farther.
119 |
120 |
121 | .. figure:: img/c.png
122 | :width: 60%
123 | :align: center
124 |
125 | Using this method, all displacement vectors have the reference point as their origin or destination (depending on whether their speed is above or below the average speed).
126 |
127 |
128 | .. figure:: img/factor.png
129 | :width: 60%
130 | :align: center
131 |
132 | The "displacement factor" option allows to apply a coefficient to the displacement of the points: with a factor of 2 the displacement of the point will be 2 times greater than with a factor of 1; conversely with a factor of 0.5 the displacement will be 2 times less important.
133 |
134 | .. figure:: img/d.png
135 | :width: 60%
136 | :align: center
137 |
138 | The background layer is deformed according to the movement of the control points.
139 |
140 | Depending on the thematic needs related to the realization of a distance cartogram, other methods may be considered for moving points.
141 | For example, it might be possible to use the entire travel time matrix to move all points in the dataset (no use of a reference point) with methods such as `Multidimensional scaling (MDS)`_ (example below using the travel time matrix provided in example).
142 |
143 | .. image:: img/MDS_prefecture.png
144 | :align: center
145 |
146 | .. image:: img/DistCartogram_prefecture.png
147 | :align: center
148 |
149 |
150 | .. index:: References
151 |
152 | References
153 | =================
154 |
155 |
156 |
157 |
158 | Indices and tables
159 | ==================
160 |
161 | * :ref:`genindex`
162 | * :ref:`modindex`
163 | * :ref:`search`
164 |
--------------------------------------------------------------------------------
/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 PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal
28 | from qgis.core import QgsMapLayerRegistry
29 | from qgis.gui import QgsMapCanvasLayer
30 |
31 | LOGGER = logging.getLogger("QGIS")
32 |
33 |
34 | # noinspection PyMethodMayBeStatic,PyPep8Naming
35 | class QgisInterface(QObject):
36 | """Class to expose QGIS objects and functions to plugins.
37 |
38 | This class is here for enabling us to run unit tests only,
39 | so most methods are simply stubs.
40 | """
41 |
42 | currentLayerChanged = pyqtSignal(QgsMapCanvasLayer)
43 |
44 | def __init__(self, canvas):
45 | """Constructor
46 | :param canvas:
47 | """
48 | QObject.__init__(self)
49 | self.canvas = canvas
50 | # Set up slots so we can mimic the behaviour of QGIS when layers
51 | # are added.
52 | LOGGER.debug("Initialising canvas...")
53 | # noinspection PyArgumentList
54 | QgsMapLayerRegistry.instance().layersAdded.connect(self.addLayers)
55 | # noinspection PyArgumentList
56 | QgsMapLayerRegistry.instance().layerWasAdded.connect(self.addLayer)
57 | # noinspection PyArgumentList
58 | QgsMapLayerRegistry.instance().removeAll.connect(self.removeAllLayers)
59 |
60 | # For processing module
61 | self.destCrs = None
62 |
63 | @pyqtSlot("QStringList")
64 | def addLayers(self, layers):
65 | """Handle layers being added to the registry so they show up in canvas.
66 |
67 | :param layers: list list of map layers that were added
68 |
69 | .. note:: The QgsInterface api does not include this method,
70 | it is added here as a helper to facilitate testing.
71 | """
72 | # LOGGER.debug('addLayers called on qgis_interface')
73 | # LOGGER.debug('Number of layers being added: %s' % len(layers))
74 | # LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers()))
75 | current_layers = self.canvas.layers()
76 | final_layers = []
77 | for layer in current_layers:
78 | final_layers.append(QgsMapCanvasLayer(layer))
79 | for layer in layers:
80 | final_layers.append(QgsMapCanvasLayer(layer))
81 |
82 | self.canvas.setLayerSet(final_layers)
83 | # LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers()))
84 |
85 | @pyqtSlot("QgsMapLayer")
86 | def addLayer(self, layer):
87 | """Handle a layer being added to the registry so it shows up in canvas.
88 |
89 | :param layer: list list of map layers that were added
90 |
91 | .. note: The QgsInterface api does not include this method, it is added
92 | here as a helper to facilitate testing.
93 |
94 | .. note: The addLayer method was deprecated in QGIS 1.8 so you should
95 | not need this method much.
96 | """
97 | pass
98 |
99 | @pyqtSlot()
100 | def removeAllLayers(self):
101 | """Remove layers from the canvas before they get deleted."""
102 | self.canvas.setLayerSet([])
103 |
104 | def newProject(self):
105 | """Create new project."""
106 | # noinspection PyArgumentList
107 | QgsMapLayerRegistry.instance().removeAllMapLayers()
108 |
109 | # ---------------- API Mock for QgsInterface follows -------------------
110 |
111 | def zoomFull(self):
112 | """Zoom to the map full extent."""
113 | pass
114 |
115 | def zoomToPrevious(self):
116 | """Zoom to previous view extent."""
117 | pass
118 |
119 | def zoomToNext(self):
120 | """Zoom to next view extent."""
121 | pass
122 |
123 | def zoomToActiveLayer(self):
124 | """Zoom to extent of active layer."""
125 | pass
126 |
127 | def addVectorLayer(self, path, base_name, provider_key):
128 | """Add a vector layer.
129 |
130 | :param path: Path to layer.
131 | :type path: str
132 |
133 | :param base_name: Base name for layer.
134 | :type base_name: str
135 |
136 | :param provider_key: Provider key e.g. 'ogr'
137 | :type provider_key: str
138 | """
139 | pass
140 |
141 | def addRasterLayer(self, path, base_name):
142 | """Add a raster layer given a raster layer file name
143 |
144 | :param path: Path to layer.
145 | :type path: str
146 |
147 | :param base_name: Base name for layer.
148 | :type base_name: str
149 | """
150 | pass
151 |
152 | def activeLayer(self):
153 | """Get pointer to the active layer (layer selected in the legend)."""
154 | # noinspection PyArgumentList
155 | layers = QgsMapLayerRegistry.instance().mapLayers()
156 | for item in layers:
157 | return layers[item]
158 |
159 | def addToolBarIcon(self, action):
160 | """Add an icon to the plugins toolbar.
161 |
162 | :param action: Action to add to the toolbar.
163 | :type action: QAction
164 | """
165 | pass
166 |
167 | def removeToolBarIcon(self, action):
168 | """Remove an action (icon) from the plugin toolbar.
169 |
170 | :param action: Action to add to the toolbar.
171 | :type action: QAction
172 | """
173 | pass
174 |
175 | def addToolBar(self, name):
176 | """Add toolbar with specified name.
177 |
178 | :param name: Name for the toolbar.
179 | :type name: str
180 | """
181 | pass
182 |
183 | def mapCanvas(self):
184 | """Return a pointer to the map canvas."""
185 | return self.canvas
186 |
187 | def mainWindow(self):
188 | """Return a pointer to the main window.
189 |
190 | In case of QGIS it returns an instance of QgisApp.
191 | """
192 | pass
193 |
194 | def addDockWidget(self, area, dock_widget):
195 | """Add a dock widget to the main window.
196 |
197 | :param area: Where in the ui the dock should be placed.
198 | :type area:
199 |
200 | :param dock_widget: A dock widget to add to the UI.
201 | :type dock_widget: QDockWidget
202 | """
203 | pass
204 |
205 | def legendInterface(self):
206 | """Get the legend."""
207 | return self.canvas
208 |
--------------------------------------------------------------------------------
/help/source/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # DistCartogram documentation build configuration file, created by
4 | # sphinx-quickstart on Sun Feb 12 17:11:03 2012.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import sys, os
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | # sys.path.insert(0, os.path.abspath('.'))
20 |
21 | # -- General configuration -----------------------------------------------------
22 |
23 | # If your documentation needs a minimal Sphinx version, state it here.
24 | # needs_sphinx = '1.0'
25 |
26 | # Add any Sphinx extension module names here, as strings. They can be extensions
27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
28 | extensions = ["sphinx.ext.todo", "sphinx.ext.imgmath", "sphinx.ext.viewcode"]
29 |
30 | # Add any paths that contain templates here, relative to this directory.
31 | templates_path = ["_templates"]
32 |
33 | # The suffix of source filenames.
34 | source_suffix = ".rst"
35 |
36 | # The encoding of source files.
37 | # source_encoding = 'utf-8-sig'
38 |
39 | # The master toctree document.
40 | master_doc = "index"
41 |
42 | # General information about the project.
43 | project = "DistCartogram"
44 | copyright = "2018, Matthieu Viry"
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.8"
52 | # The full version, including alpha/beta/rc tags.
53 | release = "0.8"
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 = "pyramid"
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 | (
182 | "index",
183 | "DistCartogram.tex",
184 | "DistCartogram Documentation",
185 | "Matthieu Viry",
186 | "manual",
187 | ),
188 | ]
189 |
190 | # The name of an image file (relative to this directory) to place at the top of
191 | # the title page.
192 | # latex_logo = None
193 |
194 | # For "manual" documents, if this is true, then toplevel headings are parts,
195 | # not chapters.
196 | # latex_use_parts = False
197 |
198 | # If true, show page references after internal links.
199 | # latex_show_pagerefs = False
200 |
201 | # If true, show URL addresses after external links.
202 | # latex_show_urls = False
203 |
204 | # Additional stuff for the LaTeX preamble.
205 | # latex_preamble = ''
206 |
207 | # Documents to append as an appendix to all manuals.
208 | # latex_appendices = []
209 |
210 | # If false, no module index is generated.
211 | # latex_domain_indices = True
212 |
213 |
214 | # -- Options for manual page output --------------------------------------------
215 |
216 | # One entry per manual page. List of tuples
217 | # (source start file, name, description, authors, manual section).
218 | man_pages = [
219 | ("index", "TemplateClass", "DistCartogram Documentation", ["Matthieu Viry"], 1)
220 | ]
221 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #/***************************************************************************
2 | # DistanceCartogram
3 | #
4 | # Compute distance cartogram
5 | # -------------------
6 | # begin : 2018-07-13
7 | # git sha : $Format:%H$
8 | # copyright : (C) 2018 by Matthieu Viry
9 | # email : matthieu.viry@cnrs.fr
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 = DistanceCartogram_fr
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 | dist_cartogram.py dist_cartogram_dialog.py
42 |
43 | PLUGINNAME = dist_cartogram
44 |
45 | PY_FILES = \
46 | __init__.py \
47 | dist_cartogram.py grid.py \
48 | dist_cartogram_dialog.py \
49 | dist_cartogram_dialog_baseUi.py \
50 | dist_cartogram_dataset_boxUi.py \
51 | utils.py \
52 | worker.py
53 |
54 | # UI_FILES = dist_cartogram_dialog_base.ui
55 |
56 | EXTRAS = metadata.txt icon.png LICENSE
57 |
58 | EXTRA_DIRS = data
59 |
60 | COMPILED_RESOURCE_FILES = resources.py
61 |
62 | PEP8EXCLUDE=pydev,resources.py,conf.py,third_party,ui
63 |
64 | COMPILED_UI_FILES = dist_cartogram_dialog_baseUi.py
65 |
66 | #################################################
67 | # Normally you would not need to edit below here
68 | #################################################
69 |
70 | HELP = help/build/html
71 |
72 | PLUGIN_UPLOAD = $(c)/plugin_upload.py
73 |
74 | RESOURCE_SRC=$(shell grep '^ *@@g;s/.*>//g' | tr '\n' ' ')
75 |
76 | QGISDIR=.local/share/QGIS/QGIS3/profiles/default
77 |
78 | default: compile
79 |
80 | compile: $(COMPILED_RESOURCE_FILES)
81 | pyuic5 dist_cartogram_dialog_base.ui -o dist_cartogram_dialog_baseUi.py
82 |
83 | %.py : %.qrc $(RESOURCES_SRC)
84 | pyrcc5 -o $*.py $<
85 |
86 | %.qm : %.ts
87 | $(LRELEASE) $<
88 |
89 | test: compile transcompile
90 | @echo
91 | @echo "----------------------"
92 | @echo "Regression Test Suite"
93 | @echo "----------------------"
94 |
95 | @# Preceding dash means that make will continue in case of errors
96 | @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \
97 | export QGIS_DEBUG=0; \
98 | export QGIS_LOG_FILE=/dev/null; \
99 | nosetests -v --with-id --with-coverage --cover-package=. \
100 | 3>&1 1>&2 2>&3 3>&- || true
101 | @echo "----------------------"
102 | @echo "If you get a 'no module named qgis.core error, try sourcing"
103 | @echo "the helper script we have provided first then run make test."
104 | @echo "e.g. source run-env-linux.sh ; make test"
105 | @echo "----------------------"
106 |
107 | deploy: compile doc transcompile
108 | @echo
109 | @echo "------------------------------------------"
110 | @echo "Deploying plugin to your .qgis2 directory."
111 | @echo "------------------------------------------"
112 | # The deploy target only works on unix like operating system where
113 | # the Python plugin directory is located at:
114 | # $HOME/$(QGISDIR)/python/plugins
115 | mkdir -p $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)
116 | cp -vf $(PY_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)
117 | # cp -vf $(UI_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)
118 | cp -vf $(COMPILED_RESOURCE_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)
119 | cp -vf $(EXTRAS) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)
120 | cp -vfr i18n $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)
121 | cp -vfr $(HELP) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)/help
122 | cp -R data $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)/
123 | # Copy extra directories if any
124 | # (foreach EXTRA_DIR,(EXTRA_DIRS), cp -R (EXTRA_DIR) (HOME)/(QGISDIR)/python/plugins/(PLUGINNAME)/;)
125 |
126 |
127 | # The dclean target removes compiled python files from plugin directory
128 | # also deletes any .git entry
129 | dclean:
130 | @echo
131 | @echo "-----------------------------------"
132 | @echo "Removing any compiled python files."
133 | @echo "-----------------------------------"
134 | find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname "*.pyc" -delete
135 | find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname ".git" -prune -exec rm -Rf {} \;
136 |
137 |
138 | derase:
139 | @echo
140 | @echo "-------------------------"
141 | @echo "Removing deployed plugin."
142 | @echo "-------------------------"
143 | rm -Rf $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)
144 |
145 | zip: deploy dclean
146 | @echo
147 | @echo "---------------------------"
148 | @echo "Creating plugin zip bundle."
149 | @echo "---------------------------"
150 | # The zip target deploys the plugin and creates a zip file with the deployed
151 | # content. You can then upload the zip file on http://plugins.qgis.org
152 | rm -f $(PLUGINNAME).zip
153 | cd $(HOME)/$(QGISDIR)/python/plugins; zip -9r $(CURDIR)/$(PLUGINNAME).zip $(PLUGINNAME)
154 |
155 | package: compile
156 | # Create a zip package of the plugin named $(PLUGINNAME).zip.
157 | # This requires use of git (your plugin development directory must be a
158 | # git repository).
159 | # To use, pass a valid commit or tag as follows:
160 | # make package VERSION=Version_0.3.2
161 | @echo
162 | @echo "------------------------------------"
163 | @echo "Exporting plugin to zip package. "
164 | @echo "------------------------------------"
165 | rm -f $(PLUGINNAME).zip
166 | git archive --prefix=$(PLUGINNAME)/ -o $(PLUGINNAME).zip $(VERSION)
167 | echo "Created package: $(PLUGINNAME).zip"
168 |
169 | upload: zip
170 | @echo
171 | @echo "-------------------------------------"
172 | @echo "Uploading plugin to QGIS Plugin repo."
173 | @echo "-------------------------------------"
174 | $(PLUGIN_UPLOAD) $(PLUGINNAME).zip
175 |
176 | transup:
177 | @echo
178 | @echo "------------------------------------------------"
179 | @echo "Updating translation files with any new strings."
180 | @echo "------------------------------------------------"
181 | @chmod +x scripts/update-strings.sh
182 | @scripts/update-strings.sh $(LOCALES)
183 |
184 | transcompile:
185 | @echo
186 | @echo "----------------------------------------"
187 | @echo "Compiled translation files to .qm files."
188 | @echo "----------------------------------------"
189 | @chmod +x scripts/compile-strings.sh
190 | @scripts/compile-strings.sh $(LRELEASE) $(LOCALES)
191 |
192 | transclean:
193 | @echo
194 | @echo "------------------------------------"
195 | @echo "Removing compiled translation files."
196 | @echo "------------------------------------"
197 | rm -f i18n/*.qm
198 |
199 | clean:
200 | @echo
201 | @echo "------------------------------------"
202 | @echo "Removing uic and rcc generated files"
203 | @echo "------------------------------------"
204 | rm $(COMPILED_UI_FILES) $(COMPILED_RESOURCE_FILES)
205 |
206 | doc:
207 | @echo
208 | @echo "------------------------------------"
209 | @echo "Building documentation using sphinx."
210 | @echo "------------------------------------"
211 | cd help; make html
212 |
213 | pylint:
214 | @echo
215 | @echo "-----------------"
216 | @echo "Pylint violations"
217 | @echo "-----------------"
218 | @pylint --reports=n --rcfile=pylintrc . || true
219 | @echo
220 | @echo "----------------------"
221 | @echo "If you get a 'no module named qgis.core' error, try sourcing"
222 | @echo "the helper script we have provided first then run make pylint."
223 | @echo "e.g. source run-env-linux.sh ; make pylint"
224 | @echo "----------------------"
225 |
226 |
227 | # Run pep8 style checking
228 | #http://pypi.python.org/pypi/pep8
229 | pep8:
230 | @echo
231 | @echo "-----------"
232 | @echo "PEP8 issues"
233 | @echo "-----------"
234 | @pep8 --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128 --exclude $(PEP8EXCLUDE) . || true
235 | @echo "-----------"
236 | @echo "Ignored in PEP8 check:"
237 | @echo $(PEP8EXCLUDE)
238 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/worker.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # /***************************************************************************
4 | # DistanceCartogram
5 | #
6 | # Compute distance cartogram
7 | # -------------------
8 | # begin : 2018-07-13
9 | # git sha : $Format:%H$
10 | # copyright : (C) 2018 by Matthieu Viry
11 | # email : matthieu.viry@cnrs.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 | from math import sqrt
24 | import traceback
25 |
26 | from PyQt5.QtCore import pyqtSignal, QObject, QVariant
27 |
28 | from qgis.core import (
29 | QgsFeature,
30 | QgsFeatureSink,
31 | QgsGeometry,
32 | QgsPointXY,
33 | QgsVectorLayer,
34 | QgsWkbTypes,
35 | )
36 |
37 | from .grid import Grid
38 |
39 |
40 | class DistCartogramWorker(QObject):
41 | resultComplete = pyqtSignal(object, object, object)
42 | finished = pyqtSignal()
43 | error = pyqtSignal(Exception, str)
44 | progress = pyqtSignal(int)
45 | status = pyqtSignal(str)
46 |
47 | def __init__(
48 | self,
49 | src_pts,
50 | image_pts,
51 | precision,
52 | extent,
53 | layers_to_transform,
54 | to_display,
55 | tr,
56 | total_features,
57 | ):
58 | QObject.__init__(self)
59 |
60 | self.src_pts = src_pts
61 | self.image_pts = image_pts
62 | self.precision = precision
63 | self.extent = extent
64 | self.layers_to_transform = layers_to_transform
65 | self.to_display = to_display
66 | self.tr = tr
67 | self.total_features = total_features
68 |
69 | def get_transformed_layers(self):
70 | transformed_layers = []
71 | for background_layer in self.layers_to_transform:
72 | _t = QgsWkbTypes.displayString(background_layer.wkbType())
73 |
74 | result_layer = QgsVectorLayer(
75 | "{}?crs={}".format(_t, background_layer.crs().authid()),
76 | "{}_cartogram".format(background_layer.name()),
77 | "memory",
78 | )
79 | features_to_add = []
80 | result_layer.setCrs(background_layer.crs())
81 | pr_result_layer = result_layer.dataProvider()
82 | pr_result_layer.addAttributes(background_layer.fields().toList())
83 | result_layer.updateFields()
84 |
85 | for ix, ft in enumerate(background_layer.getFeatures()):
86 | ref_geom = ft.geometry()
87 | ref_coords = ref_geom.__geo_interface__["coordinates"]
88 | if ref_geom.__geo_interface__["type"] == "Point":
89 | new_geom = QgsGeometry.fromPointXY(
90 | QgsPointXY(*self.g._interp_point(*ref_coords))
91 | )
92 | elif ref_geom.__geo_interface__["type"] == "MultiPoint":
93 | new_geom = QgsGeometry.fromMultiPointXY(
94 | [
95 | QgsPointXY(*self.g._interp_point(*ref_coords[ix_coords]))
96 | for ix_coords in range(len(ref_coords))
97 | ]
98 | )
99 | elif ref_geom.__geo_interface__["type"] == "LineString":
100 | new_geom = QgsGeometry.fromPolyLineXY(
101 | [
102 | QgsPointXY(*self.g._interp_point(*ref_coords[ix_coords]))
103 | for ix_coords in range(len(ref_coords))
104 | ]
105 | )
106 | elif ref_geom.__geo_interface__["type"] == "MultiLineString":
107 | lines = []
108 | for ix_line in range(len(ref_coords)):
109 | lines.append(
110 | [
111 | QgsPointXY(
112 | *self.g._interp_point(
113 | *ref_coords[ix_line][ix_coords]
114 | )
115 | )
116 | for ix_coords in range(len(ref_coords[ix_line]))
117 | ]
118 | )
119 | new_geom = QgsGeometry.fromMultiPolylineXY(lines)
120 | elif ref_geom.__geo_interface__["type"] == "Polygon":
121 | rings = []
122 | for ix_ring in range(len(ref_coords)):
123 | rings.append(
124 | [
125 | QgsPointXY(
126 | *self.g._interp_point(
127 | *ref_coords[ix_ring][ix_coords]
128 | )
129 | )
130 | for ix_coords in range(len(ref_coords[ix_ring]))
131 | ]
132 | )
133 | new_geom = QgsGeometry.fromPolygonXY(rings)
134 |
135 | elif ref_geom.__geo_interface__["type"] == "MultiPolygon":
136 | polys = []
137 | for ix_poly in range(len(ref_coords)):
138 | rings = []
139 | for ix_ring in range(len(ref_coords[ix_poly])):
140 | rings.append(
141 | [
142 | QgsPointXY(
143 | *self.g._interp_point(
144 | *ref_coords[ix_poly][ix_ring][ix_coords]
145 | )
146 | )
147 | for ix_coords in range(
148 | len(ref_coords[ix_poly][ix_ring])
149 | )
150 | ]
151 | )
152 | polys.append(rings)
153 | new_geom = QgsGeometry.fromMultiPolygonXY(polys)
154 | else:
155 | self.status.emit("Geometry type error")
156 | continue
157 | feature = QgsFeature()
158 | feature.setGeometry(new_geom)
159 | feature.setAttributes(ft.attributes())
160 | features_to_add.append(feature)
161 | self.progress.emit(1)
162 | pr_result_layer.addFeatures(features_to_add)
163 | result_layer.updateExtents()
164 | transformed_layers.append(result_layer)
165 | return transformed_layers
166 |
167 | def run(self):
168 | try:
169 |
170 | def _get_inter_nb_iter(coef_iter):
171 | return int(coef_iter * sqrt(len(self.src_pts)))
172 |
173 | self.status.emit(self.tr("Creation of interpolation grid..."))
174 | self.g = Grid(self.src_pts, self.precision, self.extent)
175 |
176 | self.status.emit(self.tr("Interpolation process..."))
177 | self.progress.emit(int(0.03 * self.total_features))
178 | self.g.interpolate(self.image_pts, _get_inter_nb_iter(4))
179 |
180 | self.progress.emit(int(0.07 * self.total_features))
181 | self.status.emit(self.tr("Transforming layers..."))
182 | transformed_layers = self.get_transformed_layers()
183 |
184 | self.status.emit(self.tr("Preparing results for displaying..."))
185 | self.progress.emit(int(0.02 * self.total_features))
186 |
187 | if self.to_display["source_grid"]:
188 | polys = self.g._get_grid_coords("source")
189 | source_grid_layer = make_grid_layer(
190 | polys, self.layers_to_transform[0].crs(), "source"
191 | )
192 | else:
193 | source_grid_layer = None
194 |
195 | self.progress.emit(int(0.03 * self.total_features))
196 |
197 | if self.to_display["trans_grid"]:
198 | polys = self.g._get_grid_coords("interp")
199 | trans_grid_layer = make_grid_layer(
200 | polys, self.layers_to_transform[0].crs(), "interp"
201 | )
202 | else:
203 | trans_grid_layer = None
204 |
205 | self.resultComplete.emit(
206 | transformed_layers,
207 | source_grid_layer,
208 | trans_grid_layer,
209 | )
210 | self.finished.emit()
211 | except Exception as e:
212 | self.error.emit(e, traceback.format_exc())
213 |
214 |
215 | def make_grid_layer(polys, crs, _type_grid):
216 | result_layer = QgsVectorLayer(
217 | "Polygon?crs={}&field=ID:integer".format(crs.authid()),
218 | "{}_grid".format(_type_grid),
219 | "memory",
220 | )
221 |
222 | result_layer.startEditing()
223 | result_layer.setCrs(crs)
224 | for ix, geom in enumerate(polys):
225 | new_geom = QgsGeometry.fromPolygonXY(
226 | [
227 | [
228 | QgsPointXY(*geom[ix_ring][ix_coords])
229 | for ix_coords in range(len(geom[ix_ring]))
230 | ]
231 | for ix_ring in range(len(geom))
232 | ]
233 | )
234 | feature = QgsFeature()
235 | feature.setGeometry(new_geom)
236 | feature.setAttributes([QVariant(ix)])
237 | result_layer.addFeature(feature, QgsFeatureSink.FastInsert)
238 | result_layer.commitChanges()
239 | return result_layer
240 |
--------------------------------------------------------------------------------
/grid.py:
--------------------------------------------------------------------------------
1 | from math import ceil, sqrt, pow as m_pow
2 |
3 |
4 | class Node:
5 | __slots__ = ["weight", "i", "j", "source", "interp"]
6 |
7 | def __init__(self, i, j, src=None):
8 | self.weight = 0
9 | self.i = i
10 | self.j = j
11 | self.source = src
12 | self.interp = None
13 |
14 |
15 | class Point:
16 | __slots__ = ["x", "y"]
17 |
18 | def __init__(self, x, y):
19 | self.x = x
20 | self.y = y
21 |
22 | def to_xy(self):
23 | return (self.x, self.y)
24 |
25 | def distance(self, other):
26 | a = self.x - other.x
27 | b = self.y - other.y
28 | return sqrt(a * a + b * b)
29 |
30 |
31 | class Rectangle2D:
32 | __slots__ = ["height", "width", "x", "y"]
33 |
34 | def __init__(self, x, y, width, height):
35 | self.x = x
36 | self.y = y
37 | self.height = height
38 | self.width = width
39 |
40 | def add(self, pt):
41 | if pt.x < self.x:
42 | self.width += self.x - pt.x
43 | self.x = pt.x
44 | elif pt.x > self.x + self.width:
45 | self.width = pt.x - self.x
46 |
47 | if pt.y < self.y:
48 | self.height += self.y - pt.y
49 | self.y = pt.y
50 | elif pt.y > self.y + self.height:
51 | self.height = pt.y - self.y
52 |
53 | @staticmethod
54 | def from_points(points):
55 | if len(points) == 0:
56 | return Rectangle2D(0, 0, 0, 0)
57 | return Rectangle2D.from_bbox(getBoundingRect(points))
58 |
59 | def as_bbox(self):
60 | return (self.x, self.y, self.x + self.width, self.y + self.height)
61 |
62 | @staticmethod
63 | def from_bbox(bbox):
64 | return Rectangle2D(bbox[3] - bbox[1], bbox[2] - bbox[0], bbox[0], bbox[1])
65 |
66 |
67 | def getBoundingRect(points):
68 | minx = float("inf")
69 | miny = float("inf")
70 | maxx = -float("inf")
71 | maxy = -float("inf")
72 | for p in points:
73 | if p.x > maxx:
74 | maxx = p.x
75 | if p.x < minx:
76 | minx = p.x
77 | if p.y > maxy:
78 | maxy = p.y
79 | if p.y < miny:
80 | miny = p.y
81 | return (minx, miny, maxx, maxy)
82 |
83 |
84 | class Grid:
85 | def __init__(self, points, precision, rect=None):
86 | self.interp_points = None
87 | self.points = points
88 | if not rect:
89 | rect = Rectangle2D.from_points(points).as_bbox()
90 | rect = list(rect)
91 | self.rect_width = rect[2] - rect[0]
92 | self.rect_height = rect[3] - rect[1]
93 | self.resolution = (
94 | 1 / precision * sqrt(self.rect_width * self.rect_height / len(points))
95 | )
96 | self.width = ceil(self.rect_width / self.resolution) + 1
97 | self.height = ceil(self.rect_height / self.resolution) + 1
98 | self.dx = self.width * self.resolution - self.rect_width
99 | self.dy = self.height * self.resolution - self.rect_height
100 | rect[0] = rect[0] - self.dx / 2
101 | rect[1] = rect[1] - self.dy / 2
102 | rect[2] = rect[2] + self.dx / 2
103 | rect[3] = rect[3] + self.dy / 2
104 | self.rect_width = rect[2] - rect[0]
105 | self.rect_height = rect[3] - rect[1]
106 |
107 | self.width += 1
108 | self.height += 1
109 | self.min_x = rect[0]
110 | self.max_y = rect[3]
111 | self.nodes = []
112 | resolution = self.resolution
113 | for i in range(self.height):
114 | for j in range(self.width):
115 | self.nodes.append(
116 | Node(
117 | i,
118 | j,
119 | Point(self.min_x + j * resolution, self.max_y - i * resolution),
120 | )
121 | )
122 |
123 | for p in points:
124 | adj_nodes = self.get_adj_nodes(p)
125 | for n in adj_nodes:
126 | n.weight += 1
127 |
128 | def get_node(self, i, j):
129 | if i < 0 or j < 0 or i >= self.height or j >= self.width:
130 | return None
131 | return self.nodes[i * self.width + j]
132 |
133 | def get_i(self, p):
134 | return int((self.max_y - p.y) / self.resolution)
135 |
136 | def get_j(self, p):
137 | return int((p.x - self.min_x) / self.resolution)
138 |
139 | def get_adj_nodes(self, point):
140 | i = self.get_i(point)
141 | j = self.get_j(point)
142 | adj_nodes = [
143 | self.get_node(i, j),
144 | self.get_node(i, j + 1),
145 | self.get_node(i + 1, j),
146 | self.get_node(i + 1, j + 1),
147 | ]
148 | return adj_nodes
149 |
150 | def get_interp_point(self, src_point):
151 | adj_nodes = self.get_adj_nodes(src_point)
152 | resolution = self.resolution
153 | ux1 = src_point.x - adj_nodes[0].source.x
154 | vy1 = src_point.y - adj_nodes[2].source.y
155 | hx1 = (
156 | ux1 / resolution * (adj_nodes[1].interp.x - adj_nodes[0].interp.x)
157 | + adj_nodes[0].interp.x
158 | )
159 | hx2 = (
160 | ux1 / resolution * (adj_nodes[3].interp.x - adj_nodes[2].interp.x)
161 | + adj_nodes[2].interp.x
162 | )
163 | HX = vy1 / resolution * (hx1 - hx2) + hx2
164 | hy1 = (
165 | ux1 / resolution * (adj_nodes[1].interp.y - adj_nodes[0].interp.y)
166 | + adj_nodes[0].interp.y
167 | )
168 | hy2 = (
169 | ux1 / resolution * (adj_nodes[3].interp.y - adj_nodes[2].interp.y)
170 | + adj_nodes[2].interp.y
171 | )
172 | HY = vy1 / resolution * (hy1 - hy2) + hy2
173 |
174 | return Point(HX, HY)
175 |
176 | def _interp_point(self, x, y):
177 | p = self.get_interp_point(Point(x, y))
178 | return (p.x, p.y)
179 |
180 | def get_diff(self, i, j, diff):
181 | if not diff:
182 | diff = [0] * 4
183 | n = self.get_node(i, j)
184 | ny1 = self.get_node(i - 1, j)
185 | ny2 = self.get_node(i + 1, j)
186 | nx1 = self.get_node(i, j - 1)
187 | nx2 = self.get_node(i, j + 1)
188 | resolution = self.resolution
189 | if not nx1:
190 | diff[0] = (nx2.interp.x - n.interp.x) / resolution
191 | diff[1] = (nx2.interp.y - n.interp.y) / resolution
192 | elif not nx2:
193 | diff[0] = (n.interp.x - nx1.interp.x) / resolution
194 | diff[1] = (n.interp.y - nx1.interp.y) / resolution
195 | else:
196 | diff[0] = (nx2.interp.x - nx1.interp.x) / (2 * resolution)
197 | diff[1] = (nx2.interp.y - nx1.interp.y) / (2 * resolution)
198 |
199 | if not ny1:
200 | diff[2] = (n.interp.x - ny2.interp.x) / resolution
201 | diff[3] = (n.interp.y - ny2.interp.y) / resolution
202 | elif not ny2:
203 | diff[2] = (ny1.interp.x - n.interp.x) / resolution
204 | diff[3] = (ny1.interp.y - n.interp.y) / resolution
205 | else:
206 | diff[2] = (ny1.interp.x - ny2.interp.x) / (2 * resolution)
207 | diff[3] = (ny1.interp.y - ny2.interp.y) / (2 * resolution)
208 |
209 | return diff
210 |
211 | def interpolate(self, img_points, nb_iter):
212 | for n in self.nodes:
213 | n.interp = Point(n.source.x, n.source.y)
214 |
215 | # We could probably do the following:
216 | # rect = Rectangle2D.from_points(self.points)
217 | # rect_adj = Rectangle2D.from_points(img_points)
218 | # but the original implementation
219 | # starts with a point at (0, 0) and we don't want to change that
220 | rect = Rectangle2D(0, 0, -1, -1)
221 | for p in self.points:
222 | rect.add(p)
223 |
224 | rect_adj = Rectangle2D(0, 0, -1, -1)
225 | for p in img_points:
226 | rect_adj.add(p)
227 |
228 | self.scaleX = rect_adj.width / rect.width
229 | self.scaleY = rect_adj.height / rect.height
230 |
231 | resolution = self.resolution
232 | width, height = self.width, self.height
233 | rect_dim = self.rect_width * self.rect_height
234 | get_node = self.get_node
235 | get_smoothed, get_adj_nodes = self.get_smoothed, self.get_adj_nodes
236 |
237 | for k in range(nb_iter):
238 | for src_pt, adj_pt in zip(self.points, img_points):
239 | adj_nodes = get_adj_nodes(src_pt)
240 | smoothed_nodes = [get_smoothed(a.i, a.j) for a in adj_nodes]
241 |
242 | ux1 = src_pt.x - adj_nodes[0].source.x
243 | ux2 = resolution - ux1
244 | vy1 = src_pt.y - adj_nodes[2].source.y
245 | vy2 = resolution - vy1
246 | u = 1 / (ux1 * ux1 + ux2 * ux2)
247 | v = 1 / (vy1 * vy1 + vy2 * vy2)
248 | w = [vy1 * ux2, vy1 * ux1, vy2 * ux2, vy2 * ux1]
249 | qx = [0] * 4
250 | qy = [0] * 4
251 | deltaZx = [0] * 4
252 | deltaZy = [0] * 4
253 | sQx = sQy = sW = 0
254 | for i in range(4):
255 | sW += m_pow(w[i], 2)
256 | deltaZx[i] = adj_nodes[i].interp.x - smoothed_nodes[i].x
257 | deltaZy[i] = adj_nodes[i].interp.y - smoothed_nodes[i].y
258 | qx[i] = w[i] * deltaZx[i]
259 | qy[i] = w[i] * deltaZy[i]
260 | sQx += qx[i]
261 | sQy += qy[i]
262 |
263 | hx1 = (
264 | ux1 / resolution * (adj_nodes[1].interp.x - adj_nodes[0].interp.x)
265 | + adj_nodes[0].interp.x
266 | )
267 | hx2 = (
268 | ux1 / resolution * (adj_nodes[3].interp.x - adj_nodes[2].interp.x)
269 | + adj_nodes[2].interp.x
270 | )
271 | HX = vy1 / resolution * (hx1 - hx2) + hx2
272 | hy1 = (
273 | ux1 / resolution * (adj_nodes[1].interp.y - adj_nodes[0].interp.y)
274 | + adj_nodes[0].interp.y
275 | )
276 | hy2 = (
277 | ux1 / resolution * (adj_nodes[3].interp.y - adj_nodes[2].interp.y)
278 | + adj_nodes[2].interp.y
279 | )
280 | HY = vy1 / resolution * (hy1 - hy2) + hy2
281 |
282 | deltaX = adj_pt.x - HX
283 | deltaY = adj_pt.y - HY
284 | dx = deltaX * resolution * resolution
285 | dy = deltaY * resolution * resolution
286 |
287 | for i in range(4):
288 | adjX = (
289 | u
290 | * v
291 | * ((dx - qx[i] + sQx) * w[i] + deltaZx[i] * (w[i] * w[i] - sW))
292 | / adj_nodes[i].weight
293 | )
294 | adj_nodes[i].interp.x += adjX
295 | adjY = (
296 | u
297 | * v
298 | * ((dy - qy[i] + sQy) * w[i] + deltaZy[i] * (w[i] * w[i] - sW))
299 | / adj_nodes[i].weight
300 | )
301 | adj_nodes[i].interp.y += adjY
302 |
303 | p_tmp = Point(0, 0)
304 | for l in range(width * height):
305 | delta = 0
306 | for i in range(height):
307 | for j in range(width):
308 | n = get_node(i, j)
309 | if n.weight == 0:
310 | p_tmp.x = n.interp.x
311 | p_tmp.y = n.interp.y
312 | _p = get_smoothed(i, j)
313 | n.interp.x = _p.x
314 | n.interp.y = _p.y
315 | delta = max([delta, p_tmp.distance(n.interp) / rect_dim])
316 | if l > 5 and sqrt(delta) < 0.0001:
317 | break
318 |
319 | self.interp_points = [
320 | self.get_interp_point(self.points[i]) for i in range(len(img_points))
321 | ]
322 |
323 | return self.interp_points
324 |
325 | def get_smoothed(self, i, j):
326 | get_node = self.get_node
327 | if 1 < i < self.height - 2 and 1 < j < self.width - 2:
328 | a = get_node(i - 1, j).interp
329 | b = get_node(i + 1, j).interp
330 | c = get_node(i, j - 1).interp
331 | d = get_node(i, j + 1).interp
332 | e = get_node(i - 1, j - 1).interp
333 | f = get_node(i + 1, j - 1).interp
334 | g = get_node(i + 1, j + 1).interp
335 | h = get_node(i - 1, j + 1).interp
336 | _i = get_node(i - 2, j).interp
337 | _j = get_node(i + 2, j).interp
338 | k = get_node(i, j - 2).interp
339 | _l = get_node(i, j + 2).interp
340 | return Point(
341 | (
342 | 8 * (a.x + b.x + c.x + d.x)
343 | - 2 * (e.x + f.x + g.x + h.x)
344 | - (_i.x + _j.x + k.x + _l.x)
345 | )
346 | / 20,
347 | (
348 | 8 * (a.y + b.y + c.y + d.y)
349 | - 2 * (e.y + f.y + g.y + h.y)
350 | - (_i.y + _j.y + k.y + _l.y)
351 | )
352 | / 20,
353 | )
354 |
355 | nb = sx = sy = 0
356 | if i > 0:
357 | n = get_node(i - 1, j).interp
358 | sx += n.x
359 | sy += n.y
360 | nb += 1
361 | else:
362 | sy += self.scaleY * self.resolution
363 | if j > 0:
364 | n = get_node(i, j - 1).interp
365 | sx += n.x
366 | sy += n.y
367 | nb += 1
368 | else:
369 | sx -= self.scaleX * self.resolution
370 |
371 | if i < self.height - 1:
372 | n = get_node(i + 1, j).interp
373 | sx += n.x
374 | sy += n.y
375 | nb += 1
376 | else:
377 | sy -= self.scaleY * self.resolution
378 |
379 | if j < self.width - 1:
380 | n = get_node(i, j + 1).interp
381 | sx += n.x
382 | sy += n.y
383 | nb += 1
384 | else:
385 | sx += self.scaleX * self.resolution
386 |
387 | return Point(sx / nb, sy / nb)
388 |
389 | def _get_grid_coords(self, _type="source"):
390 | if _type not in ("source", "interp"):
391 | raise ValueError("Invalid grid type requested")
392 | polys = []
393 | for i in range(self.height - 1):
394 | for j in range(self.width - 1):
395 | polys.append(
396 | [
397 | [
398 | getattr(self.get_node(i, j), _type).to_xy(),
399 | getattr(self.get_node(i + 1, j), _type).to_xy(),
400 | getattr(self.get_node(i + 1, j + 1), _type).to_xy(),
401 | getattr(self.get_node(i, j + 1), _type).to_xy(),
402 | getattr(self.get_node(i, j), _type).to_xy(),
403 | ]
404 | ]
405 | )
406 | return polys
407 |
--------------------------------------------------------------------------------
/i18n/DistanceCartogram_fr.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dialog
5 |
6 |
7 | Distance Cartogram sample dataset
8 | Cartogramme de distance - Données d'exemple
9 |
10 |
11 |
12 | <html><head/><body><p align="justify"><a name="result_box"/>Two layers have been added.</p><p align="justify">- "<span style=" font-weight:600; font-style:italic;">department</span>" is a layer of <span style=" font-style:italic;">MultiPolygons</span>.<br>This is the background layer to be deformed.</p><p align="justify">- "<span style=" font-weight:600; font-style:italic;">prefecture</span>" is a layer of <span style=" font-style:italic;">Points</span>.<br/>It is between these points that the matrix of travel time by road was calculated.<br/>Its identifier field to use is "<span style=" font-style:italic;">NOM_COM</span>".</p><p align="justify"><br/>To use Distance Cartogram you will also need to add the <span style=" font-weight:600; font-style:italic;">travel time matrix</span>.<br/>It's path is:</p></body></html>
13 |
14 |
15 |
16 |
17 | DistCartogramDialogBase
18 |
19 |
20 | DistanceCartogram
21 | Cartogramme de distance
22 |
23 |
24 |
25 | <html><head/><body><p align="center"><span style=" font-size:12pt; font-weight:600;">Distance cartogram creation </span></p></body></html>
26 | <html><head/><body><p align="center"><span style=" font-size:12pt; font-weight:600;">Création de cartogramme de distance </span></p></body></html>
27 |
28 |
29 |
30 | Grid precision
31 | Précision de la grille
32 |
33 |
34 |
35 | Point layer
36 | Couche de points
37 |
38 |
39 |
40 | Displacement factor
41 | Facteur de déplacement
42 |
43 |
44 |
45 | Time matrix
46 | Matrice de temps
47 |
48 |
49 |
50 | Point layer id field
51 | Champ d'ID de la couche de points
52 |
53 |
54 |
55 | Background layer(s)
56 | Couche(s) à déformer
57 |
58 |
59 |
60 | Reference feature
61 | Entité de référence
62 |
63 |
64 |
65 | Transformed layer
66 | Couche(s) transformée(s)
67 |
68 |
69 |
70 | Source grid
71 | Grille d'origine
72 |
73 |
74 |
75 | Transformed grid
76 | Grille déformée
77 |
78 |
79 |
80 | Output
81 | Sortie(s)
82 |
83 |
84 |
85 | Path to a .csv document containing a time matrix between the points from the layer previously selected.
86 | Chemin vers un fichier au format .csv et contenant une matrice de distance/temps de parcours entre les points de la couche précédemment sélectionnée.
87 |
88 |
89 |
90 | The reference feature (it's location will stay unchanged)
91 | L'entité de référence (sa position restera inchangée).
92 |
93 |
94 |
95 | The layer(s) to be deformed
96 | Couche(s) à déformer
97 |
98 |
99 |
100 | From source points and time matrix
101 | À partir d'une couche de points et d'une matrice
102 |
103 |
104 |
105 | Source points layer id field
106 | Champ d'ID de la couche "source"
107 |
108 |
109 |
110 | Source points layer
111 | Couche de points "source"
112 |
113 |
114 |
115 | From source points and image points
116 | À partir de points "source" et de points "image"
117 |
118 |
119 |
120 | Translated point layer
121 | Points déplacés
122 |
123 |
124 |
125 | Translated points layer
126 | Points déplacés
127 |
128 |
129 |
130 | Translated points layer id field
131 | Champ d'ID de la couche "de points déplacés"
132 |
133 |
134 |
135 | DistCartogramWorker
136 |
137 |
138 | Transforming layers...
139 | Transformations des couches...
140 |
141 |
142 |
143 | Creation of interpolation grid...
144 | Création de la grille d'interpolation
145 |
146 |
147 |
148 | Interpolation process...
149 | Interpolation en cours
150 |
151 |
152 |
153 | Preparing results for displaying...
154 | Préparation des résultats pour l'affichage
155 |
156 |
157 |
158 | DistanceCartogram
159 |
160 |
161 | &DistanceCartogram
162 | DistanceCartogram
163 |
164 |
165 |
166 | Create distance cartogram
167 | Création de cartogramme de distance
168 |
169 |
170 |
171 | Layers have to be in the same (projected) crs
172 | Les couches doivent être dans le même système de coordonnées (projeté)
173 |
174 |
175 |
176 | Layers have to be in a projected crs
177 | Les couches doivent être dans un système de coordonnées projeté
178 |
179 |
180 |
181 | Error
182 | Erreur
183 |
184 |
185 |
186 | No match between point layer ID and matrix ID
187 | Pas des correspondance entre les ID de la couche de points et ceux de la matrice
188 |
189 |
190 |
191 | Success
192 | Succès
193 |
194 |
195 |
196 | Matches found between point layer ID and matrix ID
197 | Correspondances trouvées entre les ID de la couche de points et ceux de la matrice
198 |
199 |
200 |
201 | Identifiant values have to be uniques
202 | Les valeurs d'ID doivent être uniques.
203 |
204 |
205 |
206 | Not enough matching features between source and image layer
207 | Pas assez de correspondances trouvées entre la couche source et la couche image
208 |
209 |
210 |
211 | File {} not found
212 | Ficher {} non trouvé
213 |
214 |
215 |
216 | Error while reading the matrix - All values (excepting columns/lines id) must be numbers
217 | Erreur lors de la lecture de la matrice. Toutes les valeurs (sauf les ID de ligne/colonne) doivent être des nombres
218 |
219 |
220 |
221 | An unexpected error has occurred while reading the CSV matrix. Please see the âPluginsâ section of the message log for details.
222 | Une erreur innatendue est survenue pendant la lecture du fichier CSV. Des détails sont disponibles dans la section "Plugins" du journal des messages.
223 |
224 |
225 |
226 | Lines and columns index have to be the same
227 | Les ID présents sur les lignes et les colonnes doivent être les mêmes.
228 |
229 |
230 |
231 | An error occurred during distance cartogram creation. Please see the âPluginsâ section of the message log for details.
232 | Une erreur est survenue durant la création du cartogramme de distance. Des détails sont disponibles dans la section "Plugins" du journal des messages.
233 |
234 |
235 |
236 | DistanceCartogram computation cancelled by user
237 | La création de cartogramme de distance a été annulée par l'utilisateur
238 |
239 |
240 |
241 | Starting...
242 | Calcul en cours...
243 |
244 |
245 |
246 | Cancel
247 | Annulation
248 |
249 |
250 |
251 | Starting
252 | En cours
253 |
254 |
255 |
256 | Creation of image points layer
257 | Création de la couche de points "image"
258 |
259 |
260 |
261 | DistanceCartogram: The "image" point layer is empty.This is probably due to a problem of non-correspondence between the identifiers of the features the point layer and the identifiers in the provided matrix.
262 | DistanceCartogram : La couche de points déplacés est vide. Ceci est probablement du à un problème de non-correspondance entre les ID de la couche de points et ceux de la matrice.
263 |
264 |
265 |
266 | Number of features differ between source and image layers - Only feature with matching ids will be taken into account
267 | Le nombre d'entités est différent entre la couche "source" et la couche "image" - Seulement les entités ayant des ID qui correspondent seront prise en compte.
268 |
269 |
270 |
271 | Add sample dataset
272 | Ajout de données d'exemple
273 |
274 |
275 |
276 |
--------------------------------------------------------------------------------
/dist_cartogram_dialog_base.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | DistCartogramDialogBase
4 |
5 |
6 |
7 | 0
8 | 0
9 | 793
10 | 702
11 |
12 |
13 |
14 | DistanceCartogram
15 |
16 |
17 |
18 |
19 | 530
20 | 665
21 | 251
22 | 32
23 |
24 |
25 |
26 | Qt::Horizontal
27 |
28 |
29 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
30 |
31 |
32 |
33 |
34 |
35 | 10
36 | 10
37 | 781
38 | 41
39 |
40 |
41 |
42 | <html><head/><body><p align="center"><span style=" font-size:12pt; font-weight:600;">Distance cartogram creation </span></p></body></html>
43 |
44 |
45 |
46 |
47 |
48 | 0
49 | 60
50 | 791
51 | 541
52 |
53 |
54 |
55 | 0
56 |
57 |
58 | false
59 |
60 |
61 |
62 | From source points and time matrix
63 |
64 |
65 |
66 |
67 | 10
68 | 10
69 | 771
70 | 514
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 0
79 | 0
80 |
81 |
82 |
83 |
84 | 300
85 | 0
86 |
87 |
88 |
89 |
90 | 320
91 | 16777215
92 |
93 |
94 |
95 | The layer(s) to be deformed
96 |
97 |
98 | Background layer(s)
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | 300
107 | 0
108 |
109 |
110 |
111 |
112 | 320
113 | 16777215
114 |
115 |
116 |
117 | Point layer
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | 370
126 | 0
127 |
128 |
129 |
130 |
131 | 400
132 | 16777215
133 |
134 |
135 |
136 | true
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | 300
145 | 0
146 |
147 |
148 |
149 |
150 | 320
151 | 16777215
152 |
153 |
154 |
155 | Point layer id field
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | 370
164 | 0
165 |
166 |
167 |
168 |
169 | 400
170 | 16777215
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | 300
180 | 0
181 |
182 |
183 |
184 |
185 | 320
186 | 16777215
187 |
188 |
189 |
190 | Path to a .csv document containing a time matrix between the points from the layer previously selected.
191 |
192 |
193 | Time matrix
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 | 370
202 | 0
203 |
204 |
205 |
206 |
207 | 400
208 | 27
209 |
210 |
211 |
212 | Qt::TabFocus
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 | 300
221 | 0
222 |
223 |
224 |
225 |
226 | 320
227 | 16777215
228 |
229 |
230 |
231 | The reference feature (it's location will stay unchanged)
232 |
233 |
234 | Reference feature
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 | 370
243 | 0
244 |
245 |
246 |
247 |
248 | 400
249 | 16777215
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 | 300
259 | 0
260 |
261 |
262 |
263 |
264 | 320
265 | 16777215
266 |
267 |
268 |
269 | Displacement factor
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 | 100
278 | 0
279 |
280 |
281 |
282 |
283 | 200
284 | 16777215
285 |
286 |
287 |
288 | 1.000000000000000
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 | 300
297 | 0
298 |
299 |
300 |
301 |
302 | 320
303 | 16777215
304 |
305 |
306 |
307 | Grid precision
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 | 100
316 | 0
317 |
318 |
319 |
320 |
321 | 200
322 | 16777215
323 |
324 |
325 |
326 | 0.500000000000000
327 |
328 |
329 | 5.000000000000000
330 |
331 |
332 | 2.000000000000000
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 | 140
341 | 0
342 |
343 |
344 |
345 |
346 | 270
347 | 60
348 |
349 |
350 |
351 | Output
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 | false
361 |
362 |
363 | Qt::NoFocus
364 |
365 |
366 | Transformed layer(s)
367 |
368 |
369 | true
370 |
371 |
372 | true
373 |
374 |
375 | false
376 |
377 |
378 |
379 |
380 |
381 |
382 | Translated point layer
383 |
384 |
385 |
386 |
387 |
388 |
389 | Source grid
390 |
391 |
392 |
393 |
394 |
395 |
396 | Transformed grid
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 | 30
407 | 75
408 |
409 |
410 |
411 |
412 | 400
413 | 75
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 | From source points and image points
424 |
425 |
426 |
427 |
428 | 10
429 | 10
430 | 771
431 | 471
432 |
433 |
434 |
435 |
436 | QLayout::SetDefaultConstraint
437 |
438 |
439 |
440 |
441 |
442 | 400
443 | 0
444 |
445 |
446 |
447 |
448 | 400
449 | 16777215
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 | 300
459 | 0
460 |
461 |
462 |
463 |
464 | 320
465 | 16777215
466 |
467 |
468 |
469 | Translated points layer id field
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 | 140
478 | 0
479 |
480 |
481 |
482 |
483 | 270
484 | 60
485 |
486 |
487 |
488 | Output
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 | 400
497 | 0
498 |
499 |
500 |
501 |
502 | 400
503 | 16777215
504 |
505 |
506 |
507 | true
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 | 400
516 | 0
517 |
518 |
519 |
520 |
521 | 400
522 | 16777215
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 | 300
532 | 0
533 |
534 |
535 |
536 |
537 | 320
538 | 16777215
539 |
540 |
541 |
542 | Source points layer
543 |
544 |
545 |
546 |
547 |
548 |
549 | Qt::Vertical
550 |
551 |
552 | QSizePolicy::Fixed
553 |
554 |
555 |
556 | 20
557 | 30
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 | false
568 |
569 |
570 | Qt::NoFocus
571 |
572 |
573 | Transformed layer(s)
574 |
575 |
576 | true
577 |
578 |
579 | true
580 |
581 |
582 | false
583 |
584 |
585 |
586 |
587 |
588 |
589 | Source grid
590 |
591 |
592 |
593 |
594 |
595 |
596 | Transformed grid
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 | 400
607 | 0
608 |
609 |
610 |
611 |
612 | 400
613 | 16777215
614 |
615 |
616 |
617 | true
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 | 300
626 | 0
627 |
628 |
629 |
630 |
631 | 320
632 | 16777215
633 |
634 |
635 |
636 | Path to a .csv document containing a time matrix between the points from the layer previously selected.
637 |
638 |
639 | Translated points layer
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 | 0
648 | 0
649 |
650 |
651 |
652 |
653 | 300
654 | 0
655 |
656 |
657 |
658 |
659 | 320
660 | 16777215
661 |
662 |
663 |
664 | The layer(s) to be deformed
665 |
666 |
667 | Background layer(s)
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 | 300
676 | 27
677 |
678 |
679 |
680 |
681 | 320
682 | 16777215
683 |
684 |
685 |
686 | Source points layer id field
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 | 100
695 | 0
696 |
697 |
698 |
699 |
700 | 200
701 | 16777215
702 |
703 |
704 |
705 | 0.500000000000000
706 |
707 |
708 | 5.000000000000000
709 |
710 |
711 | 2.000000000000000
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 | 190
720 | 0
721 |
722 |
723 |
724 |
725 | 320
726 | 16777215
727 |
728 |
729 |
730 | Grid precision
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 | 30
739 | 75
740 |
741 |
742 |
743 |
744 | 400
745 | 75
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 | 10
758 | 665
759 | 91
760 | 32
761 |
762 |
763 |
764 | QDialogButtonBox::Help
765 |
766 |
767 |
768 |
769 |
770 | 0
771 | 590
772 | 791
773 | 71
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 | QgsFieldComboBox
782 | QComboBox
783 | qgsfieldcombobox.h
784 |
785 |
786 | QgsFileWidget
787 | QWidget
788 | qgsfilewidget.h
789 |
790 |
791 | QgsMapLayerComboBox
792 | QComboBox
793 | qgsmaplayercombobox.h
794 |
795 |
796 |
797 | gridTabWidget
798 | pointLayerComboBox
799 | mFieldComboBox
800 | matrixQgsFileWidget
801 | refFeatureComboBox
802 | doubleSpinBoxDeplacement
803 | doubleSpinBoxGridPrecision
804 | checkBoxImagePointLayer
805 | checkBoxSourceGrid
806 | checkBoxTransformedGrid
807 | pointLayerComboBox_2
808 | mFieldComboBox_2
809 | imagePointLayerComboBox_2
810 | mImageFieldComboBox_2
811 | doubleSpinBoxGridPrecision_2
812 | checkBoxSourceGrid_2
813 | checkBoxTransformedGrid_2
814 |
815 |
816 |
817 |
818 | button_box
819 | accepted()
820 | DistCartogramDialogBase
821 | accept()
822 |
823 |
824 | 20
825 | 20
826 |
827 |
828 | 20
829 | 20
830 |
831 |
832 |
833 |
834 | button_box
835 | rejected()
836 | DistCartogramDialogBase
837 | reject()
838 |
839 |
840 | 20
841 | 20
842 |
843 |
844 | 20
845 | 20
846 |
847 |
848 |
849 |
850 |
851 |
--------------------------------------------------------------------------------
/dist_cartogram.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | DistanceCartogram
5 | A QGIS plugin
6 | Compute distance cartogram
7 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
8 | -------------------
9 | begin : 2018-07-13
10 | git sha : $Format:%H$
11 | copyright : (C) 2018 by Matthieu Viry
12 | email : matthieu.viry@cnrs.fr
13 | ***************************************************************************/
14 |
15 | /***************************************************************************
16 | * *
17 | * This program is free software; you can redistribute it and/or modify *
18 | * it under the terms of the GNU General Public License as published by *
19 | * the Free Software Foundation; either version 2 of the License, or *
20 | * (at your option) any later version. *
21 | * *
22 | ***************************************************************************/
23 | """
24 | import numpy as np
25 | import csv
26 | import os.path
27 |
28 | from PyQt5.QtCore import (
29 | QCoreApplication,
30 | QSettings,
31 | Qt,
32 | QThread,
33 | QTranslator,
34 | QUrl,
35 | qVersion,
36 | )
37 | from PyQt5.QtGui import QIcon, QDesktopServices
38 | from PyQt5.QtWidgets import (
39 | QAction,
40 | QDialogButtonBox,
41 | QLabel,
42 | QProgressBar,
43 | QPushButton,
44 | QSizePolicy,
45 | QListWidgetItem,
46 | )
47 | from qgis.core import (
48 | Qgis,
49 | QgsCoordinateReferenceSystem,
50 | QgsMapLayerProxyModel,
51 | QgsMessageLog,
52 | QgsProject,
53 | QgsMapLayerType,
54 | )
55 | from qgis.gui import QgsMessageBar
56 |
57 | # Initialize Qt resources from file resources.py
58 | from .resources import *
59 |
60 | # Import the code for the dialog
61 | from .dist_cartogram_dialog import DistCartogramDialog
62 |
63 | # Code for the small dialog displayed when sample dataset is added
64 | from .dist_cartogram_dataset_boxUi import DatasetDialog
65 |
66 | # QThread worker to compute the cartogram in background
67 | from .worker import DistCartogramWorker
68 |
69 | # Helpers to manipulate data to prepare for bidimensionnal regression
70 | from .utils import (
71 | create_image_points,
72 | extract_source_image,
73 | get_total_features,
74 | get_merged_extent,
75 | )
76 |
77 |
78 | class DistanceCartogram:
79 | """QGIS Plugin Implementation."""
80 |
81 | def __init__(self, iface):
82 | """Constructor.
83 |
84 | :param iface: An interface instance that will be passed to this class
85 | which provides the hook by which you can manipulate the QGIS
86 | application at run time.
87 | :type iface: QgsInterface
88 | """
89 | # Save reference to the QGIS interface
90 | self.iface = iface
91 | # initialize plugin directory
92 | self.plugin_dir = os.path.dirname(__file__)
93 | # initialize locale
94 | locale = QSettings().value("locale/userLocale")[0:2]
95 | locale_path = os.path.join(
96 | self.plugin_dir, "i18n", "DistanceCartogram_{}.qm".format(locale)
97 | )
98 |
99 | if os.path.exists(locale_path):
100 | self.translator = QTranslator()
101 | self.translator.load(locale_path)
102 |
103 | if qVersion() > "4.3.3":
104 | QCoreApplication.installTranslator(self.translator)
105 |
106 | # Create the dialog (after translation) and keep reference
107 | self.dlg = DistCartogramDialog()
108 |
109 | self.dlg.setMinimumSize(self.dlg.width(), self.dlg.height())
110 | self.dlg.setMaximumSize(self.dlg.width(), self.dlg.height())
111 |
112 | self.dlg.msg_bar = QgsMessageBar()
113 | self.dlg.msg_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
114 | self.dlg.bottomVerticalLayout.addWidget(self.dlg.msg_bar)
115 | self.col_ix = None
116 | self.line_ix = None
117 | self.time_matrix = None
118 |
119 | # Params for first tab:
120 | self.dlg.backgroundLayersListWidget.currentRowChanged.connect(
121 | self.state_ok_button
122 | )
123 | self.dlg.backgroundLayersListWidget.itemChanged.connect(self.state_ok_button)
124 | self.dlg.pointLayerComboBox.setFilters(QgsMapLayerProxyModel.PointLayer)
125 | self.dlg.pointLayerComboBox.layerChanged.connect(self.fill_field_combo_box)
126 |
127 | self.dlg.matrixQgsFileWidget.setFilter("*.csv")
128 | self.dlg.matrixQgsFileWidget.fileChanged.connect(self.read_matrix)
129 |
130 | self.dlg.mFieldComboBox.fieldChanged.connect(self.state_ok_button)
131 |
132 | self.dlg.refFeatureComboBox.currentIndexChanged.connect(self.state_ok_button)
133 |
134 | self.dlg.refFeatureComboBox.activated.connect(self.state_ok_button)
135 |
136 | # Params for second tab:
137 | self.dlg.backgroundLayersListWidget_2.currentRowChanged.connect(
138 | self.state_ok_button
139 | )
140 | self.dlg.backgroundLayersListWidget_2.itemChanged.connect(self.state_ok_button)
141 | self.dlg.pointLayerComboBox_2.setFilters(QgsMapLayerProxyModel.PointLayer)
142 | self.dlg.pointLayerComboBox_2.layerChanged.connect(
143 | self.fill_field_combo_box_source
144 | )
145 |
146 | self.dlg.imagePointLayerComboBox_2.setFilters(QgsMapLayerProxyModel.PointLayer)
147 | self.dlg.imagePointLayerComboBox_2.layerChanged.connect(
148 | self.fill_field_combo_box_image
149 | )
150 |
151 | self.dlg.mFieldComboBox_2.fieldChanged.connect(self.state_ok_button)
152 |
153 | self.dlg.mImageFieldComboBox_2.fieldChanged.connect(self.state_ok_button)
154 |
155 | self.dlg.button_box_help.helpRequested.connect(self.show_help)
156 |
157 | # Declare instance attributes
158 | self.actions = []
159 | self.menu = self.tr("&DistanceCartogram")
160 | # TODO: We are going to let the user set this up in a future iteration
161 | self.toolbar = self.iface.addToolBar("DistanceCartogram")
162 | self.toolbar.setObjectName("DistanceCartogram")
163 | self.fill_file_widget_with_sample_value = False
164 |
165 | def update_layers_in_list():
166 | # List the layers
167 | layers = [
168 | layer
169 | for layer in QgsProject.instance().mapLayers().values()
170 | if layer.type() == QgsMapLayerType.VectorLayer
171 | ]
172 | items = [f"{i.name()} [{i.crs().authid()}]" for i in layers]
173 |
174 | # Clean the content of the widgets
175 | self.dlg.backgroundLayersListWidget.clear()
176 | self.dlg.backgroundLayersListWidget_2.clear()
177 |
178 | # Add them in our QListWidget:
179 | for s in items:
180 | items = [QListWidgetItem(s), QListWidgetItem(s)]
181 | for i in items:
182 | i.setFlags(i.flags() | Qt.ItemIsUserCheckable)
183 | i.setCheckState(Qt.Unchecked)
184 | self.dlg.backgroundLayersListWidget.addItem(items[0])
185 | self.dlg.backgroundLayersListWidget_2.addItem(items[1])
186 |
187 | # The logic to keep the layers in our QListWidget synced with the
188 | # layers in the QGIS project
189 | QgsProject.instance().layersAdded.connect(update_layers_in_list)
190 | QgsProject.instance().layersRemoved.connect(update_layers_in_list)
191 | update_layers_in_list()
192 |
193 | # noinspection PyMethodMayBeStatic
194 | def tr(self, message):
195 | """Get the translation for a string using Qt translation API.
196 |
197 | We implement this ourselves since we do not inherit QObject.
198 |
199 | :param message: String for translation.
200 | :type message: str, QString
201 |
202 | :returns: Translated version of message.
203 | :rtype: QString
204 | """
205 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
206 | return QCoreApplication.translate("DistanceCartogram", message)
207 |
208 | def add_action(
209 | self,
210 | icon_path,
211 | text,
212 | callback,
213 | enabled_flag=True,
214 | add_to_menu=True,
215 | add_to_toolbar=True,
216 | status_tip=None,
217 | whats_this=None,
218 | parent=None,
219 | ):
220 | """Add a toolbar icon to the toolbar.
221 |
222 | :param icon_path: Path to the icon for this action. Can be a resource
223 | path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
224 | :type icon_path: str
225 |
226 | :param text: Text that should be shown in menu items for this action.
227 | :type text: str
228 |
229 | :param callback: Function to be called when the action is triggered.
230 | :type callback: function
231 |
232 | :param enabled_flag: A flag indicating if the action should be enabled
233 | by default. Defaults to True.
234 | :type enabled_flag: bool
235 |
236 | :param add_to_menu: Flag indicating whether the action should also
237 | be added to the menu. Defaults to True.
238 | :type add_to_menu: bool
239 |
240 | :param add_to_toolbar: Flag indicating whether the action should also
241 | be added to the toolbar. Defaults to True.
242 | :type add_to_toolbar: bool
243 |
244 | :param status_tip: Optional text to show in a popup when mouse pointer
245 | hovers over the action.
246 | :type status_tip: str
247 |
248 | :param parent: Parent widget for the new action. Defaults None.
249 | :type parent: QWidget
250 |
251 | :param whats_this: Optional text to show in the status bar when the
252 | mouse pointer hovers over the action.
253 |
254 | :returns: The action that was created. Note that the action is also
255 | added to self.actions list.
256 | :rtype: QAction
257 | """
258 |
259 | icon = QIcon(icon_path)
260 | action = QAction(icon, text, parent)
261 | action.triggered.connect(callback)
262 | action.setEnabled(enabled_flag)
263 |
264 | if status_tip is not None:
265 | action.setStatusTip(status_tip)
266 |
267 | if whats_this is not None:
268 | action.setWhatsThis(whats_this)
269 |
270 | if add_to_toolbar:
271 | self.toolbar.addAction(action)
272 |
273 | if add_to_menu:
274 | self.iface.addPluginToVectorMenu(self.menu, action)
275 |
276 | self.actions.append(action)
277 |
278 | return action
279 |
280 | def initGui(self):
281 | """Create the menu entries and toolbar icons inside the QGIS GUI."""
282 |
283 | icon_path = ":/plugins/dist_cartogram/icon.png"
284 | self.add_action(
285 | icon_path,
286 | text=self.tr("Create distance cartogram"),
287 | callback=self.run,
288 | parent=self.iface.mainWindow(),
289 | )
290 | self.add_action(
291 | icon_path,
292 | text=self.tr("Add sample dataset"),
293 | callback=self.add_sample_dataset,
294 | parent=self.iface.mainWindow(),
295 | add_to_toolbar=False,
296 | )
297 |
298 | def unload(self):
299 | """Removes the plugin menu item and icon from QGIS GUI."""
300 | for action in self.actions:
301 | self.iface.removePluginVectorMenu(self.tr("&DistanceCartogram"), action)
302 | self.iface.removeToolBarIcon(action)
303 | # remove the toolbar
304 | del self.toolbar
305 |
306 | def show_help(self):
307 | """Display application help to the user."""
308 | help_file = "file:///{}/help/index.html".format(self.plugin_dir)
309 | QDesktopServices.openUrl(QUrl(help_file))
310 |
311 | def add_sample_dataset(self):
312 | base_uri = "|".join(
313 | [
314 | os.path.join(self.plugin_dir, "data", "prefecture_FRA.gpkg"),
315 | "layername={}",
316 | ]
317 | )
318 |
319 | layerDpt = self.iface.addVectorLayer(
320 | base_uri.format("departement"), "departement", "ogr"
321 | )
322 |
323 | layerPref = self.iface.addVectorLayer(
324 | base_uri.format("prefecture"), "prefecture", "ogr"
325 | )
326 |
327 | crs = QgsCoordinateReferenceSystem("EPSG:2154")
328 |
329 | layerDpt.setCrs(crs)
330 | layerPref.setCrs(crs)
331 |
332 | csv_path = os.path.join(self.plugin_dir, "data", "mat.csv")
333 |
334 | dataset_dialog = DatasetDialog()
335 | dataset_dialog.show()
336 | dataset_dialog.activateWindow()
337 | dataset_dialog.matrixPathTextEdit.setText(csv_path)
338 | _rv = dataset_dialog.exec_()
339 | self.fill_file_widget_with_sample_value = True
340 |
341 | def fill_field_combo_box(self, layer):
342 | self.dlg.mFieldComboBox.setLayer(layer)
343 | self.state_ok_button()
344 |
345 | def fill_field_combo_box_source(self, layer):
346 | self.dlg.mFieldComboBox_2.setLayer(layer)
347 | self.state_ok_button()
348 |
349 | def fill_field_combo_box_image(self, layer):
350 | self.dlg.mImageFieldComboBox_2.setLayer(layer)
351 | self.state_ok_button()
352 |
353 | def check_layers_crs(self, layers):
354 | crs = []
355 | for lyr in layers:
356 | proj = lyr.crs()
357 | crs.append((proj.authid(), proj.isGeographic()))
358 |
359 | self.dlg.msg_bar.clearWidgets()
360 |
361 | if not all([crs[0][0] == authid[0] for authid in crs]):
362 | self.dlg.msg_bar.pushCritical(
363 | self.tr("Error"),
364 | self.tr("Layers have to be in the same (projected) crs"),
365 | )
366 | return False
367 |
368 | elif any([a[1] for a in crs]):
369 | self.dlg.msg_bar.pushCritical(
370 | self.tr("Error"), self.tr("Layers have to be in a projected crs")
371 | )
372 | return False
373 |
374 | return True
375 |
376 | def check_values_id_field(self, layer, id_field):
377 | if not self.line_ix:
378 | return
379 | ids = [str(ft[id_field]) for ft in layer.getFeatures()]
380 | if not any(_id in self.line_ix for _id in ids):
381 | self.dlg.msg_bar.clearWidgets()
382 | self.dlg.msg_bar.pushCritical(
383 | self.tr("Error"),
384 | self.tr("No match between point layer ID and matrix ID"),
385 | )
386 | return False
387 | self.dlg.msg_bar.clearWidgets()
388 | self.dlg.msg_bar.pushSuccess(
389 | self.tr("Success"),
390 | self.tr("Matches found between point layer ID and matrix ID"),
391 | )
392 | return True
393 |
394 | def check_match_id_image_source(self, src_lyr, src_id_field, img_lyr, img_id_field):
395 | source_ids = [ft[src_id_field] for ft in src_lyr.getFeatures()]
396 | image_ids = [ft[img_id_field] for ft in img_lyr.getFeatures()]
397 | set_source_ids = set(source_ids)
398 | set_image_ids = set(image_ids)
399 |
400 | self.dlg.msg_bar.clearWidgets()
401 |
402 | if len(source_ids) != len(set_source_ids) or len(image_ids) != len(
403 | set_image_ids
404 | ):
405 | self.dlg.msg_bar.pushCritical(
406 | self.tr("Error"), self.tr("Identifiant values have to be uniques")
407 | )
408 | return False
409 |
410 | if len(set_source_ids.intersection(set_image_ids)) < 3:
411 | self.dlg.msg_bar.pushCritical(
412 | self.tr("Error"),
413 | self.tr(
414 | "Not enough matching features between " "source and image layer"
415 | ),
416 | )
417 | return False
418 |
419 | return True
420 |
421 | def read_matrix(self, filepath):
422 | self.col_ix = None
423 | self.line_ix = None
424 | self.time_matrix = None
425 | col_ix = {}
426 | line_ix = {}
427 |
428 | if not filepath:
429 | return
430 |
431 | self.dlg.msg_bar.clearWidgets()
432 |
433 | if not os.path.exists(filepath) or os.path.isdir(filepath):
434 | self.dlg.msg_bar.pushCritical(
435 | self.tr("Error"), self.tr("File {} not found".format(filepath))
436 | )
437 | return
438 | try:
439 | with open(filepath, "r") as dest_f:
440 | data_iter = csv.reader(dest_f, quotechar='"')
441 | header = next(data_iter)
442 |
443 | for i, _id in enumerate(header):
444 | if i == 0:
445 | continue
446 | col_ix[str(_id)] = i - 1
447 |
448 | d = []
449 |
450 | for i, data in enumerate(data_iter):
451 | d.append(data[1:])
452 | line_ix[data[0]] = i
453 | try:
454 | self.time_matrix = np.array(d, dtype=float)
455 |
456 | except ValueError as err:
457 | self.dlg.msg_bar.pushCritical(
458 | self.tr("Error"),
459 | self.tr(
460 | "Error while reading the matrix - All values "
461 | "(excepting columns/lines id) must be numbers"
462 | ),
463 | )
464 |
465 | QgsMessageLog.logMessage(
466 | "{}: {}".format(err.__class__, err),
467 | level=Qgis.Critical,
468 | tag="Plugins",
469 | )
470 | return
471 |
472 | except Exception as err:
473 | self.dlg.msg_bar.pushCritical(
474 | self.tr("Error"),
475 | self.tr(
476 | "An unexpected error has occurred while reading the "
477 | "CSV matrix. Please see the “Plugins” section of the "
478 | "message log for details."
479 | ),
480 | )
481 | QgsMessageLog.logMessage(
482 | "{}: {}".format(err.__class__, err), level=Qgis.Critical, tag="Plugins"
483 | )
484 | return
485 |
486 | if not any(k in line_ix for k in col_ix.keys()):
487 | self.time_matrix = None
488 | self.dlg.msg_bar.pushCritical(
489 | self.tr("Error"),
490 | self.tr(
491 | "Lines and columns index have to be (at least partially) the same"
492 | ),
493 | )
494 | return
495 |
496 | self.dlg.refFeatureComboBox.clear()
497 | self.dlg.refFeatureComboBox.addItems(list(sorted(line_ix.keys())))
498 | self.dlg.refFeatureComboBox.setCurrentIndex(0)
499 | self.col_ix = col_ix
500 | self.line_ix = line_ix
501 | self.state_ok_button()
502 |
503 | def updateStatusMessage(self, message=""):
504 | try:
505 | self.statusMessageLabel.setText("DistanceCartogram: " + message)
506 | except:
507 | pass
508 |
509 | def updateProgressBar(self, increase=1):
510 | try:
511 | self.progressBar.setValue(self.progressBar.value() + increase)
512 | except:
513 | pass
514 |
515 | def reset_fields(self):
516 | # self.dlg.pointLayerComboBox.setCurrentIndex(-1)
517 | # self.dlg.backgroundLayerComboBox.setCurrentIndex(-1)
518 | # self.dlg.refFeatureComboBox.setCurrentIndex(-1)
519 | layer = self.dlg.pointLayerComboBox.currentLayer()
520 | nb_field = self.dlg.mFieldComboBox.count()
521 | if nb_field < 1 and layer:
522 | self.fill_field_combo_box(layer)
523 | layer_source = self.dlg.pointLayerComboBox.currentLayer()
524 | nb_field = self.dlg.mFieldComboBox_2.count()
525 | if nb_field < 1 and layer_source:
526 | self.fill_field_combo_box_source(layer_source)
527 | layer_image = self.dlg.pointLayerComboBox.currentLayer()
528 | nb_field = self.dlg.mImageFieldComboBox_2.count()
529 | if nb_field < 1 and layer_image:
530 | self.fill_field_combo_box_image(layer_image)
531 | self.state_ok_button()
532 |
533 | def has_layers_selected(self, which):
534 | widget = getattr(self.dlg, which)
535 | for n in range(widget.count()):
536 | if widget.item(n).checkState() == Qt.Checked:
537 | return True
538 |
539 | def get_layers_selected(self, which):
540 | widget = getattr(self.dlg, which)
541 | layers = []
542 | for n in range(widget.count()):
543 | if widget.item(n).checkState() == Qt.Checked:
544 | name = widget.item(n).text().rpartition(" ")[0]
545 | layer = QgsProject.instance().mapLayersByName(name)[0]
546 | layers.append(layer)
547 | return layers
548 |
549 | def state_ok_button(self):
550 | result = False
551 |
552 | if self.dlg.gridTabWidget.currentIndex() == 0:
553 | a = self.dlg.pointLayerComboBox.currentIndex()
554 | b = self.has_layers_selected("backgroundLayersListWidget")
555 | c = self.dlg.refFeatureComboBox.currentIndex()
556 | d = self.dlg.mFieldComboBox.currentIndex()
557 | e = self.dlg.matrixQgsFileWidget.filePath()
558 |
559 | if a == -1 or not b:
560 | result = False
561 |
562 | else:
563 | result = self.check_layers_crs(
564 | (
565 | self.dlg.pointLayerComboBox.currentLayer(),
566 | *self.get_layers_selected("backgroundLayersListWidget"),
567 | )
568 | )
569 |
570 | if (
571 | c == -1
572 | or d == -1
573 | or not self.check_values_id_field(
574 | self.dlg.pointLayerComboBox.currentLayer(),
575 | self.dlg.mFieldComboBox.currentField(),
576 | )
577 | or not e
578 | ):
579 | result = False
580 |
581 | else:
582 | a = self.has_layers_selected("backgroundLayersListWidget_2")
583 | b = self.dlg.pointLayerComboBox_2.currentIndex()
584 | c = self.dlg.mFieldComboBox_2.currentIndex()
585 | d = self.dlg.imagePointLayerComboBox_2.currentIndex()
586 | e = self.dlg.mImageFieldComboBox_2.currentIndex()
587 |
588 | if (
589 | not a
590 | or b == -1
591 | or c == -1
592 | or d == -1
593 | or e == -1
594 | or not self.check_match_id_image_source(
595 | self.dlg.pointLayerComboBox_2.currentLayer(),
596 | self.dlg.mFieldComboBox_2.currentField(),
597 | self.dlg.imagePointLayerComboBox_2.currentLayer(),
598 | self.dlg.mImageFieldComboBox_2.currentField(),
599 | )
600 | ):
601 | result = False
602 | else:
603 | result = self.check_layers_crs(
604 | (
605 | self.dlg.pointLayerComboBox_2.currentLayer(),
606 | *self.get_layers_selected("backgroundLayersListWidget"),
607 | )
608 | )
609 |
610 | self.dlg.button_box.button(QDialogButtonBox.Ok).setEnabled(result)
611 |
612 | def startWorker(
613 | self, src_pts, img_pts, precision, max_extent, layers, total_features
614 | ):
615 | worker = DistCartogramWorker(
616 | src_pts,
617 | img_pts,
618 | precision,
619 | max_extent,
620 | layers,
621 | self.display,
622 | self.tr,
623 | total_features,
624 | )
625 | thread = QThread()
626 | worker.moveToThread(thread)
627 |
628 | # connecting signals+slots
629 | worker.finished.connect(self.workerFinished)
630 | worker.resultComplete.connect(self.cartogram_complete)
631 | worker.error.connect(self.workerError)
632 | worker.progress.connect(self.updateProgressBar)
633 | worker.status.connect(self.updateStatusMessage)
634 | thread.started.connect(worker.run)
635 | thread.start()
636 |
637 | self.worker = worker
638 | self.thread = thread
639 |
640 | def stopWorker(self):
641 | if hasattr(self, "worker"):
642 | self.worker.stopped = True
643 |
644 | def push_error(self, e, exceptionString):
645 | self.iface.messageBar().pushCritical(
646 | self.tr("Error"),
647 | self.tr(
648 | "An error occurred during distance cartogram creation. "
649 | + "Please see the “Plugins” section of the message "
650 | + "log for details."
651 | ),
652 | )
653 | QgsMessageLog.logMessage(exceptionString, level=Qgis.Critical, tag="Plugins")
654 |
655 | def workerError(self, e, exceptionString):
656 | self.push_error(e, exceptionString)
657 | self.workerFinished()
658 |
659 | def workerFinished(self):
660 | try:
661 | self.worker.deleteLater()
662 | except:
663 | pass
664 | self.thread.quit()
665 | self.thread.wait()
666 | self.thread.terminate()
667 | self.thread.deleteLater()
668 | self.iface.messageBar().popWidget(self.messageBarItem)
669 |
670 | def cartogram_complete(
671 | self, result_layers=None, source_grid_layer=None, trans_grid_layer=None
672 | ):
673 | if (
674 | hasattr(self, "worker")
675 | and hasattr(self.worker, "stopped")
676 | and self.worker.stopped
677 | ):
678 | return
679 | if result_layers is not None:
680 | if self.display["source_grid"]:
681 | QgsProject.instance().addMapLayer(source_grid_layer)
682 | if self.display["trans_grid"]:
683 | QgsProject.instance().addMapLayer(trans_grid_layer)
684 |
685 | for result_layer in result_layers:
686 | QgsProject.instance().addMapLayer(result_layer)
687 |
688 | if self.display["image_points"]:
689 | QgsProject.instance().addMapLayer(self.image_layer)
690 |
691 | self.iface.messageBar().popWidget(self.messageBarItem)
692 | else:
693 | QgsMessageLog.logMessage(
694 | self.tr("DistanceCartogram computation cancelled by user")
695 | )
696 |
697 | def run(self):
698 | """Run method that performs all the real work"""
699 | # show the dialog
700 | self.dlg.show()
701 | # If the last action was to add the sample dataset, pref-fill the
702 | # dedicated QgsFileWidget with the path of the sample csv matrix
703 | if self.fill_file_widget_with_sample_value:
704 | self.fill_file_widget_with_sample_value = False
705 | csv_path = os.path.join(self.plugin_dir, "data", "mat.csv")
706 | self.dlg.matrixQgsFileWidget.setFilePath(csv_path)
707 | self.dlg.mFieldComboBox.setField("NOM_COM")
708 | # ...
709 | self.reset_fields()
710 | # Run the dialog event loop
711 | result = self.dlg.exec_()
712 | # See if OK was pressed
713 | if result:
714 | if not self.dlg.button_box.button(QDialogButtonBox.Ok).isEnabled():
715 | return
716 | # set up all widgets for status reporting
717 | self.progressBar = QProgressBar()
718 | self.progressBar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
719 | self.progressBar.setMaximum(100)
720 |
721 | self.statusMessageLabel = QLabel(self.tr("Starting..."))
722 | self.statusMessageLabel.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
723 |
724 | cancelButton = QPushButton(self.tr("Cancel"))
725 | cancelButton.clicked.connect(self.stopWorker)
726 |
727 | self.messageBarItem = self.iface.messageBar().createMessage("")
728 | self.messageBarItem.layout().addWidget(self.statusMessageLabel)
729 | self.messageBarItem.layout().addWidget(self.progressBar)
730 | self.messageBarItem.layout().addWidget(cancelButton)
731 |
732 | self.iface.messageBar().pushWidget(self.messageBarItem, Qgis.Info)
733 |
734 | self.updateStatusMessage(self.tr("Starting"))
735 |
736 | if self.dlg.gridTabWidget.currentIndex() == 0:
737 | if self.time_matrix is None:
738 | self.read_matrix(self.dlg.matrixQgsFileWidget.filePath())
739 | background_layers = self.get_layers_selected(
740 | "backgroundLayersListWidget"
741 | )
742 | source_layer = self.dlg.pointLayerComboBox.currentLayer()
743 | id_ref_feature = self.dlg.refFeatureComboBox.currentText()
744 | id_field = self.dlg.mFieldComboBox.currentField()
745 | source_idx, dest_idx = self.line_ix, self.col_ix
746 | mat_extract = self.time_matrix[source_idx[id_ref_feature]]
747 | precision = self.dlg.doubleSpinBoxGridPrecision.value()
748 | deplacement_factor = self.dlg.doubleSpinBoxDeplacement.value()
749 |
750 | total_features = get_total_features(background_layers)
751 |
752 | self.progressBar.setMaximum(int(0.20 * total_features + total_features))
753 |
754 | self.display = {
755 | "source_grid": self.dlg.checkBoxSourceGrid.isChecked(),
756 | "trans_grid": self.dlg.checkBoxTransformedGrid.isChecked(),
757 | "image_points": self.dlg.checkBoxImagePointLayer.isChecked(),
758 | }
759 | self.updateStatusMessage(self.tr("Creation of image points layer"))
760 | # We create the layer of 'image' points from the layer
761 | # of 'source' points.
762 | # We (obviously) skip features whose geometry is Null / empty
763 | # (and return a count of them in the unused_points variable).
764 | # As these points aren't displayed on the map, I think
765 | # it is not necessary to warn the user about that
766 | # (but this may change in the future).
767 | (
768 | source_to_use,
769 | image_to_use,
770 | image_layer,
771 | unused_points,
772 | ) = create_image_points(
773 | source_layer,
774 | id_field,
775 | mat_extract,
776 | id_ref_feature,
777 | dest_idx,
778 | deplacement_factor,
779 | self.display["image_points"],
780 | )
781 | self.updateProgressBar(int(0.05 * total_features))
782 | if len(source_to_use) == 0 or len(image_to_use) == 0:
783 | self.iface.messageBar().pushCritical(
784 | self.tr("Error"),
785 | self.tr(
786 | "DistanceCartogram: "
787 | 'The "image" point layer is empty.'
788 | "This is probably due to a problem of "
789 | "non-correspondence between the identifiers of the"
790 | " features the point layer and the identifiers in "
791 | "the provided matrix."
792 | ),
793 | )
794 | return
795 | self.image_layer = image_layer
796 | extent = get_merged_extent(background_layers + [source_layer])
797 | max_extent = (
798 | extent.xMinimum(),
799 | extent.yMinimum(),
800 | extent.xMaximum(),
801 | extent.yMaximum(),
802 | )
803 | self.startWorker(
804 | source_to_use,
805 | image_to_use,
806 | precision,
807 | max_extent,
808 | background_layers,
809 | total_features,
810 | )
811 |
812 | else:
813 | background_layers = self.get_layers_selected(
814 | "backgroundLayersListWidget_2"
815 | )
816 | source_layer = self.dlg.pointLayerComboBox_2.currentLayer()
817 | id_field_source = self.dlg.mFieldComboBox_2.currentField()
818 | image_layer = self.dlg.imagePointLayerComboBox_2.currentLayer()
819 | id_field_image = self.dlg.mImageFieldComboBox_2.currentField()
820 | precision = self.dlg.doubleSpinBoxGridPrecision_2.value()
821 | #
822 | self.display = {
823 | "source_grid": self.dlg.checkBoxSourceGrid_2.isChecked(),
824 | "trans_grid": self.dlg.checkBoxTransformedGrid_2.isChecked(),
825 | "image_points": False,
826 | }
827 |
828 | total_features = get_total_features(background_layers)
829 |
830 | self.progressBar.setMaximum(int(0.20 * total_features + total_features))
831 |
832 | if source_layer.featureCount() != image_layer.featureCount():
833 | self.updateStatusMessage(
834 | self.tr(
835 | "Number of features differ between source and image "
836 | "layers - Only feature with matching ids will be "
837 | "taken into account"
838 | )
839 | )
840 | source_to_use, image_to_use = extract_source_image(
841 | source_layer, image_layer, id_field_source, id_field_image
842 | )
843 |
844 | self.updateProgressBar(int(0.05 * total_features))
845 |
846 | extent = get_merged_extent(
847 | background_layers + [source_layer, image_layer]
848 | )
849 | max_extent = (
850 | extent.xMinimum(),
851 | extent.yMinimum(),
852 | extent.xMaximum(),
853 | extent.yMaximum(),
854 | )
855 | self.startWorker(
856 | source_to_use,
857 | image_to_use,
858 | precision,
859 | max_extent,
860 | background_layers,
861 | total_features,
862 | )
863 |
--------------------------------------------------------------------------------