├── .gitignore ├── CHANGES.rst ├── LICENSE ├── Makefile ├── README.md ├── __init__.py ├── data ├── mat.csv ├── mat_incomplete.csv └── prefecture_FRA.gpkg ├── dist_cartogram.py ├── dist_cartogram_dataset_boxUi.py ├── dist_cartogram_dialog.py ├── dist_cartogram_dialog_base.ui ├── grid.py ├── help ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── img │ ├── DistCartogram_prefecture.png │ ├── MDS_prefecture.png │ ├── a.png │ ├── b.png │ ├── b2.png │ ├── c.png │ ├── cartogram-train.png │ ├── d.png │ ├── factor.png │ ├── from_2_points_layers.png │ ├── from_2_points_layers2.png │ ├── from_2_points_layers3.png │ ├── matrix.png │ ├── screenshot500.png │ ├── source_point_table.png │ └── ui.png │ └── index.rst ├── i18n ├── DistanceCartogram.pro └── DistanceCartogram_fr.ts ├── icon.png ├── metadata.txt ├── plugin_upload.py ├── pylintrc ├── resources.qrc ├── scripts ├── compile-strings.sh ├── run-env-linux.sh └── update-strings.sh ├── test ├── __init__.py ├── qgis_interface.py ├── test_init.py ├── test_resources.py ├── test_translations.py └── utilities.py ├── utils.py └── worker.py /.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/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![png1](help/source/img/screenshot500.png) 19 | 20 | ![png2](help/source/img/cartogram-train.png) 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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /data/prefecture_FRA.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/data/prefecture_FRA.gpkg -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /help/source/img/DistCartogram_prefecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/DistCartogram_prefecture.png -------------------------------------------------------------------------------- /help/source/img/MDS_prefecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/MDS_prefecture.png -------------------------------------------------------------------------------- /help/source/img/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/a.png -------------------------------------------------------------------------------- /help/source/img/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/b.png -------------------------------------------------------------------------------- /help/source/img/b2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/b2.png -------------------------------------------------------------------------------- /help/source/img/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/c.png -------------------------------------------------------------------------------- /help/source/img/cartogram-train.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/cartogram-train.png -------------------------------------------------------------------------------- /help/source/img/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/d.png -------------------------------------------------------------------------------- /help/source/img/factor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/factor.png -------------------------------------------------------------------------------- /help/source/img/from_2_points_layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/from_2_points_layers.png -------------------------------------------------------------------------------- /help/source/img/from_2_points_layers2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/from_2_points_layers2.png -------------------------------------------------------------------------------- /help/source/img/from_2_points_layers3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/from_2_points_layers3.png -------------------------------------------------------------------------------- /help/source/img/matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/matrix.png -------------------------------------------------------------------------------- /help/source/img/screenshot500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/screenshot500.png -------------------------------------------------------------------------------- /help/source/img/source_point_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/source_point_table.png -------------------------------------------------------------------------------- /help/source/img/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/help/source/img/ui.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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">- &quot;<span style=" font-weight:600; font-style:italic;">department</span>&quot; is a layer of <span style=" font-style:italic;">MultiPolygons</span>.<br>This is the background layer to be deformed.</p><p align="justify">- &quot;<span style=" font-weight:600; font-style:italic;">prefecture</span>&quot; 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 &quot;<span style=" font-style:italic;">NOM_COM</span>&quot;.</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 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/QgisDistanceCartogramPlugin/6de342d6fa3e2ce1a805c665dbb81d7fa9ce32e1/icon.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # import qgis libs so that ve set the correct sip api version 2 | import qgis # pylint: disable=W0611 # NOQA 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------