""" /*************************************************************************** Orientation A QGIS plugin Réaliser des cartes d’orientation 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. * * * ***************************************************************************/ """ import os.path import shutil import tempfile from pathlib import Path import processing from processing import QgsProcessingException from processing.tools import dataobjects from qgis.core import ( QgsCoordinateReferenceSystem, QgsCoordinateTransformContext, QgsFeatureRequest, QgsProject, QgsVectorFileWriter, QgsVectorLayer, ) from qgis.PyQt.QtCore import QCoreApplication, QSettings, Qt, QTranslator, QUrl from qgis.PyQt.QtGui import QDesktopServices, QIcon from qgis.PyQt.QtWidgets import ( QAction, QFileDialog, QMenu, QMessageBox, QProgressBar, QToolButton, qApp, ) from qgis.utils import OverrideCursor # Initialize Qt resources from file resources.py from .resources import * 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 = [] self.menu = "CaLiÉc" # 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_kid", text=self.tr("Style kid"), parent=my_menu, add_to_toolbar=False, add_to_menu=False, callback=lambda: self.run("style_kid"), ), 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( icon_path, text=self.tr("Créer le projet"), callback=lambda: None, parent=self.iface.mainWindow(), ) menu_action.setMenu(my_menu) self.iface.pluginToolBar().widgetForAction(menu_action).setPopupMode( QToolButton.InstantPopup ) self.help_action = QAction(QIcon(icon_path), "CaLiÉc", self.iface.mainWindow()) self.iface.pluginHelpMenu().addAction(self.help_action) self.help_action.triggered.connect(self.show_help) # will be set False in run() self.first_start = True @staticmethod def show_help(): QDesktopServices.openUrl( QUrl("https://forge.chapril.org/linux_alpes/caliec/wiki/CaLi%C3%89c") ) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginMenu("CaLiÉc", action) self.iface.removeToolBarIcon(action) self.iface.pluginHelpMenu().removeAction(self.help_action) del self.help_action def run(self, main_style): try: self.main(main_style) finally: self.iface.messageBar().popWidget() qApp.restoreOverrideCursor() def main(self, main_style): # Paramètres du projet tempdir = tempfile.TemporaryDirectory() styles_url = f"https://forge.chapril.org/linux_alpes/caliec/raw/branch/master/{main_style}/styles/" 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) if len(project.mapLayersByName("OpenStreetMap")) == 0: QMessageBox.warning( self.iface.mainWindow(), self.tr("Attention"), self.tr( "Affichez d'abord un fond Openstreetmap (menu XYZ tiles) et cadrez la zone voulue" ), ) return # 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(), self.tr("Zone étendue"), self.tr( "La zone est vraiment trop étendue, descendre en-dessous de 1:20000" ), QMessageBox.Ok, ) return if ( QMessageBox.warning( self.iface.mainWindow(), self.tr("Zone étendue"), self.tr( "La zone est étendue et le téléchargement risque de prendre longtemps.\n" "Continuer ?" ), QMessageBox.Yes | QMessageBox.No, ) == QMessageBox.No ): return # Paramétrage du dossier de travail settings = QSettings() dir = QFileDialog.getExistingDirectory( self.iface.mainWindow(), self.tr( "Sélectionnez un dossier de travail pour enregistrer le projet QGIS" ), 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(), self.tr("Attention"), self.tr("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( self.tr("Chargement"), self.tr("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], "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) # 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"] # On enregistre le layer dans le gpkg options.layerName = layer_name code, error = QgsVectorFileWriter.writeAsVectorFormatV2( layer, str(workDir / "data.gpkg"), QgsCoordinateTransformContext(), options, ) if code != 0: with OverrideCursor(Qt.ArrowCursor): QMessageBox.warning( self.iface.mainWindow(), self.tr("Erreur"), self.tr( f"Erreur à l'export de la couche {layer_name} : \n\n{error[:2000]}" ), ) return 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")) if len(deleted_fields) > 0: with OverrideCursor(Qt.ArrowCursor): QMessageBox.warning( self.iface.mainWindow(), self.tr("Attention"), self.tr( "Les champs suivants sont en doublons et ont été supprimés au profit de leur doublon en minuscule :\n- " + "\n - ".join(deleted_fields) ), )