caliec/orientation/orientation.py

351 lines
15 KiB
Python
Raw Normal View History

2021-03-01 14:32:10 +01:00
"""
/***************************************************************************
Orientation
A QGIS plugin
Réaliser des cartes dorientation
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
-------------------
begin : 2021-03-01
git sha : $Format:%H$
copyright : (C) 2021 by Association Linux-Alpes
email : caliec@linux-alpes.org
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
from qgis.PyQt.QtCore import Qt, QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction, qApp, QFileDialog, QProgressBar, QMessageBox, QMenu, QToolButton
2021-03-01 14:32:10 +01:00
2021-03-12 18:10:19 +01:00
from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransformContext, QgsProject, QgsVectorFileWriter, QgsVectorLayer, QgsFeatureRequest
2021-03-03 13:42:42 +01:00
from qgis.utils import OverrideCursor
2021-03-01 14:32:10 +01:00
from processing import QgsProcessingException
# Initialize Qt resources from file resources.py
from .resources import *
import processing
import os.path
import shutil
import tempfile
from pathlib import Path
from processing.tools import dataobjects
class Orientation:
"""QGIS Plugin Implementation."""
def __init__(self, iface):
"""Constructor.
:param iface: An interface instance that will be passed to this class
which provides the hook by which you can manipulate the QGIS
application at run time.
:type iface: QgsInterface
"""
# Save reference to the QGIS interface
self.iface = iface
# initialize plugin directory
self.plugin_dir = os.path.dirname(__file__)
# initialize locale
locale = QSettings().value('locale/userLocale')[0:2]
locale_path = os.path.join(
self.plugin_dir,
'i18n',
'Orientation_{}.qm'.format(locale))
if os.path.exists(locale_path):
self.translator = QTranslator()
self.translator.load(locale_path)
QCoreApplication.installTranslator(self.translator)
# Declare instance attributes
self.actions = []
2021-03-03 13:42:42 +01:00
self.menu = self.tr('CaLiEc')
2021-03-01 14:32:10 +01:00
# Check if plugin was started the first time in current QGIS session
# Must be set in initGui() to survive plugin reloads
self.first_start = None
# noinspection PyMethodMayBeStatic
def tr(self, message):
"""Get the translation for a string using Qt translation API.
We implement this ourselves since we do not inherit QObject.
:param message: String for translation.
:type message: str, QString
:returns: Translated version of message.
:rtype: QString
"""
# noinspection PyTypeChecker,PyArgumentList,PyCallByClass
return QCoreApplication.translate('Orientation', message)
def add_action(
self,
icon_path,
text,
callback,
enabled_flag=True,
add_to_menu=True,
add_to_toolbar=True,
status_tip=None,
whats_this=None,
parent=None):
"""Add a toolbar icon to the toolbar.
:param icon_path: Path to the icon for this action. Can be a resource
path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
:type icon_path: str
:param text: Text that should be shown in menu items for this action.
:type text: str
:param callback: Function to be called when the action is triggered.
:type callback: function
:param enabled_flag: A flag indicating if the action should be enabled
by default. Defaults to True.
:type enabled_flag: bool
:param add_to_menu: Flag indicating whether the action should also
be added to the menu. Defaults to True.
:type add_to_menu: bool
:param add_to_toolbar: Flag indicating whether the action should also
be added to the toolbar. Defaults to True.
:type add_to_toolbar: bool
:param status_tip: Optional text to show in a popup when mouse pointer
hovers over the action.
:type status_tip: str
:param parent: Parent widget for the new action. Defaults None.
:type parent: QWidget
:param whats_this: Optional text to show in the status bar when the
mouse pointer hovers over the action.
:returns: The action that was created. Note that the action is also
added to self.actions list.
:rtype: QAction
"""
icon = QIcon(icon_path)
action = QAction(icon, text, parent)
action.triggered.connect(callback)
action.setEnabled(enabled_flag)
if status_tip is not None:
action.setStatusTip(status_tip)
if whats_this is not None:
action.setWhatsThis(whats_this)
if add_to_toolbar:
# Adds plugin icon to Plugins toolbar
self.iface.addToolBarIcon(action)
if add_to_menu:
self.iface.addPluginToMenu(
self.menu,
action)
self.actions.append(action)
return action
def initGui(self):
"""Create the menu entries and toolbar icons inside the QGIS GUI."""
icon_path = ':/plugins/caliec/icon.png'
my_menu = QMenu()
my_menu.addActions([self.add_action(icon_path=":/plugins/caliec/vignette_caliec", text=self.tr("Style CaLiÉc"), parent=my_menu,
add_to_toolbar=False, add_to_menu=False,
callback=lambda: self.run("style_caliec")),
self.add_action(icon_path=":/plugins/caliec/vignette_jardin", text=self.tr("Style jardin"), parent=my_menu,
add_to_toolbar=False, add_to_menu=False,
callback=lambda: self.run("style_jardin"))])
menu_action = self.add_action(
2021-03-01 14:32:10 +01:00
icon_path,
text=self.tr('DEV - Créer le projet'),
callback=lambda: None,
2021-03-01 14:32:10 +01:00
parent=self.iface.mainWindow())
menu_action.setMenu(my_menu)
self.iface.pluginToolBar().widgetForAction(menu_action).setPopupMode(QToolButton.InstantPopup)
2021-03-01 14:32:10 +01:00
# will be set False in run()
self.first_start = True
def unload(self):
"""Removes the plugin menu item and icon from QGIS GUI."""
for action in self.actions:
self.iface.removePluginMenu(
2021-03-02 16:24:58 +01:00
self.tr('CaLiÉc'),
2021-03-01 14:32:10 +01:00
action)
self.iface.removeToolBarIcon(action)
def run(self, main_style):
2021-03-01 14:32:10 +01:00
try:
self.main(main_style)
2021-03-01 14:32:10 +01:00
finally:
self.iface.messageBar().popWidget()
qApp.restoreOverrideCursor()
def main(self, main_style):
2021-03-01 14:32:10 +01:00
# Paramètres du projet
tempdir = tempfile.TemporaryDirectory()
styles_url = f'https://forge.chapril.org/linux_alpes/caliec/raw/branch/master/{main_style}/styles/'
2021-03-01 14:32:10 +01:00
lambert93 = QgsCoordinateReferenceSystem("EPSG:2154")
wgspm = QgsCoordinateReferenceSystem("EPSG:3857")
scr = wgspm
project = QgsProject.instance()
project.setCrs(scr)
options = QgsVectorFileWriter.SaveVectorOptions()
options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
options.driverName = "GPKG"
context = dataobjects.createContext()
context.setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)
2021-03-02 16:24:58 +01:00
if len(project.mapLayersByName("OpenStreetMap")) == 0:
QMessageBox.warning(self.iface.mainWindow(), "Attention", "Affichez d'abord un fond Openstreetmap (menu XYZ tiles) et cadrez la zone voulue")
return
2021-03-01 14:32:10 +01:00
# Correspondances des noms des couches et des styles:
# "NOM_DE_LA_COUCHE_OSM": ("nom_du_style", "nom_du_layer")
# Commenter les couches non désirées
# L'ordre impacte directement l'ordre final des couches
names = {
#"OUTPUT_OTHER_RELATIONS": (None, "Autres"),
"OUTPUT_MULTIPOLYGONS": ("multipolygon", "Surfaces"),
#"OUTPUT_MULTILINESTRINGS": (None, "Multilignes"),
"OUTPUT_LINES": ("linestring", "Lignes"),
"OUTPUT_POINTS": ("point", "Points"),
}
# Vérif de l'échelle
if self.iface.mapCanvas().scale() > 10000:
if self.iface.mapCanvas().scale() > 20000:
QMessageBox.warning(self.iface.mainWindow(), "Zone étendue",
"La zone est vraiment trop étendue, descendre en-dessous de 1:20000",
QMessageBox.Ok)
return
2021-03-01 14:32:10 +01:00
if QMessageBox.warning(self.iface.mainWindow(), "Zone étendue",
"La zone est étendue et le téléchargement risque de prendre longtemps.\n"
"Continuer ?",
QMessageBox.Yes | QMessageBox.No) == QMessageBox.No:
2021-03-01 14:32:10 +01:00
return
# Paramétrage du dossier de travail
settings = QSettings()
dir = QFileDialog.getExistingDirectory(self.iface.mainWindow(),
"Sélectionnez un dossier de travail",
settings.value("caliec/workingDir"))
if dir == "":
return
settings.setValue("caliec/workingDir", dir)
workDir = Path(dir)
# Construction de la requête overpass
try:
url = processing.run("quickosm:buildqueryextent",
parameters={"EXTENT": self.iface.mapCanvas().extent()})["OUTPUT_URL"]
except QgsProcessingException:
QMessageBox.warning(self.iface.mainWindow(), "Attention", "Vous n'avez pas installé l'extension QuickOSM")
return
# Barre de chargement animée
qApp.setOverrideCursor(Qt.WaitCursor)
message_bar = self.iface.messageBar()
progress_bar = QProgressBar()
progress_bar.setMaximum(0)
widget = message_bar.createMessage("Chargement", "Récupération des data en ligne")
widget.layout().addWidget(progress_bar)
message_bar.pushWidget(widget)
# Téléchargement du fichier osm et création des couches
osmfile = str(Path(tempdir.name) / "osm.xml")
processing.run("native:filedownloader", {"URL": url, "OUTPUT": osmfile})
layers = processing.run("quickosm:openosmfile", {"FILE": osmfile})
for osm_name, style_layer_names in names.items():
style_name, layer_name = style_layer_names
# On explose les champs
layer = processing.run("native:explodehstorefield",
parameters={"INPUT": layers[osm_name],
2021-03-03 13:42:42 +01:00
"FIELD": "other_tags",
"OUTPUT": "memory:"},
context=context)["OUTPUT"]
# On enlève les champs en doublons en gardant celui des doublons qui est écrit tout en minuscule
field_names = [a.name() for a in layer.fields()]
field_names_lower = [a.lower() for a in field_names]
expected_fields, deleted_fields = [], []
for field_name, field_name_lower in zip(field_names, field_names_lower):
if field_names_lower.count(field_name_lower) == 1 or field_name.lower() == field_name:
expected_fields.append(field_name)
else:
deleted_fields.append(field_name)
2021-03-04 11:57:47 +01:00
# S'il y a des doublons
if len(deleted_fields) > 0:
# On réexplose les champs sans doublons
layer = processing.run("native:explodehstorefield",
parameters={"INPUT": layers[osm_name],
"FIELD": "other_tags",
"OUTPUT": "memory:",
"EXPECTED_FIELDS": ",".join(expected_fields)},
context=context)["OUTPUT"]
2021-03-01 14:32:10 +01:00
# On enregistre le layer dans le gpkg
options.layerName = layer_name
2021-03-12 18:10:19 +01:00
code, error = QgsVectorFileWriter.writeAsVectorFormatV2(layer, str(workDir / "data.gpkg"), QgsCoordinateTransformContext(), options)
2021-03-01 14:32:10 +01:00
if code != 0:
2021-03-03 13:42:42 +01:00
with OverrideCursor(Qt.ArrowCursor):
QMessageBox.warning(self.iface.mainWindow(), 'Erreur', f"Erreur à l'export de la couche {layer_name} : \n\n{error[:2000]}")
return
2021-03-01 14:32:10 +01:00
new_layer = QgsVectorLayer(str(workDir / f"data.gpkg|layername={layer_name}"), layer_name)
# Les layers suivants seront enregistrés dans le gpkg déjà existant
options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
# On charge le style
if style_name is not None:
stylefile = str((Path(tempdir.name) / style_name).with_suffix('.qml'))
processing.run("native:filedownloader",
parameters={"URL": styles_url + f"{style_name}.qml", "OUTPUT": stylefile})
new_layer.loadNamedStyle(stylefile)
# On charge le nouveau layer
project.addMapLayer(new_layer)
try:
project.removeMapLayer(project.mapLayersByName("OpenStreetMap")[0])
except IndexError:
pass
self.iface.mapCanvas().refreshAllLayers()
project.write(str(workDir / 'orient.qgs'))
2021-03-03 13:42:42 +01:00
if len(deleted_fields) > 0:
with OverrideCursor(Qt.ArrowCursor):
QMessageBox.warning(self.iface.mainWindow(), "Attention",
"Les champs suivants sont en doublons et ont été supprimés au profit de leur doublon en minuscule :\n- " +
"\n - ".join(deleted_fields))