diff --git a/get_nib.py b/get_nib.py new file mode 100644 index 0000000000000000000000000000000000000000..55196f602df83ce2309acc5aae743439dee2cee1 --- /dev/null +++ b/get_nib.py @@ -0,0 +1,432 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + getnib + A QGIS plugin + + Plugin "NIB-ortofoto-prosjekt" + Hent alle of-prosjekt i et utsnitt fra Norge i bilder (WMS) + Utsnittet kan være bounding boksen til + - et kartlag i Layers panel + - en fil man laster opp + - aktuelt map canvas + +Ressures brukt: + - Plugin Builder for å få riktig oppsett og nødvendige filer + - https://www.qgistutorials.com/en/docs/3/building_a_python_plugin.html + NB! husk å lage og å kopiere over compile.bat (trengs for å kompilere resources.py slik at f.eks. eget icon skal bli synlig) + Se også https://gis.stackexchange.com/questions/136861/getting-layer-by-name-in-pyqgis + - https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/cheat_sheet.html# + - https://docs.qgis.org/3.16/en/docs/pyqgis_developer_cookbook/plugins/index.html + - https://norgeibilder.no/dok/webtjenester.pdf for å få tilgang til Metadata og se påkrevd format på input-verdier + - installert QGIS plugin "Releod plugin" for å oppdatere endringer gjort i plugin-en + - OSGeo4W-installasjon av QGIS med Qt Designer inkludert mtp. utforming av plugin + - logo hentet fra https://www.flaticon.com + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-12-22 + git sha : $Format:%H$ + copyright : (C) 2021 by ban, NIBIO + email : ban@nibio.no + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +""" +#Default modules +from qgis.PyQt.QtCore import QUrl, QSettings, QTranslator, QCoreApplication +from qgis.PyQt.QtGui import QIcon, QDesktopServices +from qgis.PyQt.QtWidgets import QAction, QFileDialog, QMessageBox + +#Import additional modules +from qgis.core import QgsProject, Qgis, QgsMessageLog, QgsRasterLayer, QgsVectorLayer, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsPointXY +from qgis.gui import QgsMapCanvas +from qgis.utils import iface, showPluginHelp +from urllib import request, parse # for å lese url, gå gjennom streng +import os + +# Initialize Qt resources from file resources.py +from .resources import * +# Import the code for the dialog +from .get_nib_dialog import getnibDialog +import os.path + + +class getnib: + """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', + 'getnib_{}.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 = self.tr(u'&NIB of-prosjekt') + + # 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('getnib', 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/get_nib/icon.png' + self.add_action( + icon_path, + text=self.tr(u'Hent of-prosjekt'), + callback=self.run, + parent=self.iface.mainWindow()) + + # 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( + self.tr(u'&NIB of-prosjekt'), + action) + self.iface.removeToolBarIcon(action) + + + """ Additional methods """ + def select_input_file(self): + """ Får å velge fil som skal tjene som bounding box """ + filename, _filter = QFileDialog.getOpenFileName( + self.dlg, "Select a file ","", 'geo-files (*.shp *.geojson *.gpkg *.gml *.jpg *.tif);; All files (*.*)') + self.dlg.lineEdit.setText(filename) + + def show_help(self): + """ Display application help to the user. """ + help_file = 'file:///%s/index.html' % self.plugin_dir + # For testing path: + # QMessageBox.information(None, 'Help File', help_file) + QDesktopServices.openUrl(QUrl(help_file)) + + def check_bbsize(self, srid, xmin, xmax, ymin, ymax): + """ Check length and height of bounding box. Limit is set to 50 km """ + sourceCrs = QgsCoordinateReferenceSystem(srid) # Input project or layer crs + destCrs = QgsCoordinateReferenceSystem('EPSG:3035') # ETRS89-extended / LAEA Europe + transformContext = QgsProject.instance().transformContext() + xform = QgsCoordinateTransform(sourceCrs, destCrs, transformContext) + # Forward transformation: src -> dest + # Computes length and height in LAE regardless input crs + ll = xform.transform(QgsPointXY(xmin,ymin)) # Bounding box's lower left corner + ur = xform.transform(QgsPointXY(xmax,ymax)) # Bounding box's upper right corner + # Get the maximum height and length of the bounding box (limit is set to 50 km) + xdist = (ur.x()-ll.x())/1000 # Get lenght in km (xmax-xmin) + ydist = (ur.y()-ll.y())/1000 # Get height in km (ymax-ymin) + if xdist > 50 or ydist > 50: + self.iface.messageBar().pushMessage("Error", "Height or length of bounding box can't be > 50 km",level=Qgis.Critical, duration=3) + ok = False + else: + ok = True # Bounding box is small enough + return ok + """ end additional methods """ + + + def run(self): + """Run method that performs all the real work""" + + # Create the dialog with elements (after translation) and keep reference + # Only create GUI ONCE in callback, so that it will only load when the plugin is started + if self.first_start == True: + self.first_start = False + self.dlg = getnibDialog() + """ Additional code """ + #connect the select_input_file method to the clicked signal of the push button widget + self.dlg.pushButton.clicked.connect(self.select_input_file) + self.dlg.toolButton_help.clicked.connect(self.show_help) + + # Fetch the currently loaded layers + layers = QgsProject.instance().mapLayers() + # Clear the contents of the combobox from previous runs + self.dlg.comboBox.clear() + # Populate the combobox with names of all the loaded layers + layer_list = [] + unique_lyr = [] + for layer in layers.values(): + item = layer.name() + layer_list.append(item) # Includes duplicates + unique_lyr = list(set(layer_list)) # No duplicates + self.dlg.comboBox.addItems(unique_lyr) # Populate combobox + self.dlg.comboBox.model().sort(0) # Sort layer names (includes filepath) alfabetically + # Clear the contents of the lineEdit from previous runs + self.dlg.lineEdit.clear() + # As default: Remember user input from previous run + """ If this should not be the default, remove comment # from next line """ + # self.dlg.checkBoxNib.setChecked(False) # Unchecked + # As default: Set map canvas checkbox to checked + """ If this should not be the default, comment # next line """ + self.dlg.checkBox.setChecked(True) # Checked + + # Get projects epsg-code # e.g. + crs_proj_str = iface.mapCanvas().mapSettings().destinationCrs().authid() # EPSG:25832 + crs_proj_int = int(crs_proj_str[5:]) # 25832 + """ end additional code """ + + # show the dialog + self.dlg.show() + # Run the dialog event loop + result = self.dlg.exec_() + # See if OK was pressed + if result: + # Do something useful here - delete the line containing pass and + # substitute with your code. + # pass + """" Additional code""" + selectedLayer = '' # Initialise + reset = False + if self.dlg.checkBoxNib.isChecked(): + reset = True + # If checked, use the bounding box (= extent of current map canvas) + if self.dlg.checkBox.isChecked(): + # Get the extent of current map canvas (coordinates in the project's crs) + e = iface.mapCanvas().extent() + xmin = e.xMinimum() + xmax = e.xMaximum() + ymin = e.yMinimum() + ymax = e.yMaximum() + ok = self.check_bbsize(crs_proj_str, xmin, xmax, ymin, ymax) # Check if bb is small enough + else: # If not checked, use existing layer or open new from file + fname = self.dlg.lineEdit.text() # get the text (path and filename) + lname = os.path.splitext(os.path.basename(fname))[0] #get only the filename without the extension, will be used as layer name in the Layers panel in QGIS + # No file is chosen + if fname == "": # Use selected layer from combobox + lyr_name = self.dlg.comboBox.currentText() # Get the layer name + if lyr_name != "": # If a layer is present/chosen + selectedLayer = QgsProject.instance().mapLayersByName(lyr_name)[0] # Get this vector layer + # Get the extent of the file (coordinates in the selected layer's crs) + e = selectedLayer.extent() + else: # If no file or layer is selcted, throw an error mesage and end plugin + self.iface.messageBar().pushMessage("Error", "Nothing selected!", level=Qgis.Critical, duration=3) + return # Return from (end) plugin + else: # Use selected file + file = r""+fname+"" # Reads the file + fLayer = QgsVectorLayer(file, lname, "ogr") # If vector fie + if not fLayer.isValid(): # If not valid vector laayer + fLayer = QgsRasterLayer(file, lname, "gdal") # If raster file + if not fLayer.isValid(): # If not valid raster layer either, show error message and end plugin + self.iface.messageBar().pushMessage("Error", "Not a valid geo-file: "+ str(lname),level=Qgis.Critical, duration=3) + return # Return from (end) plugin + QgsProject.instance().addMapLayer(fLayer, True) # Add the layer and show it (True) + selectedLayer = QgsProject.instance().mapLayersByName(lname)[0] # Get this layer + # Get the extent of the active layer (coordinates in the selected file's crs) + e = selectedLayer.extent() + + # Activate (select) the selected layer in the combobox or in the lineEdit + iface.setActiveLayer(selectedLayer) + # Zoom to activated layer + iface.zoomToActiveLayer() + + # Get selected layer's epsg:code + crs_lyr = selectedLayer.crs() # example: + crs_lyr_str = crs_lyr.authid() # EPSG:4258 + try: + crs_lyr_int = int(crs_lyr_str[5:]) # 4258 + except: # In case a non-geo-layer (e.g. csv-file) is selected from the combobox + self.iface.messageBar().pushMessage("Error", "Layer is missing crs", level=Qgis.Critical, duration=3) + layers = [] # Empty layers to get a fresh start when rerunning the plugin + return # Return from (end) plugin + + # Get the extent of the active layer (coordinates in file or layer crs) + xmin = e.xMinimum() + xmax = e.xMaximum() + ymin = e.yMinimum() + ymax = e.yMaximum() + # If selected file or layer and project have different crs, the layer's + # bounding box coordinates must be transformed into the project's crs + if crs_lyr_int != crs_proj_int: + sourceCrs = QgsCoordinateReferenceSystem(crs_lyr_str) + destCrs = QgsCoordinateReferenceSystem(crs_proj_str) + transformContext = QgsProject.instance().transformContext() + xform = QgsCoordinateTransform(sourceCrs, destCrs, transformContext) + # forward transformation: src -> dest + pt1 = xform.transform(QgsPointXY(xmin,ymin)) + pt2 = xform.transform(QgsPointXY(xmax,ymax)) + xmin = pt1.x() + xmax = pt2.x() + ymin = pt1.y() + ymax = pt2.y() + # Check if bb is small enough - input crs and transformed bb coordinates are in project crs + ok = self.check_bbsize(crs_proj_str, xmin, xmax, ymin, ymax) + else: # Selected file or layer and project have the same crs + # Check if bb is small enough - input crs and bb coordinates are in layer crs = project crs + ok = self.check_bbsize(crs_lyr_str, xmin, xmax, ymin, ymax) + + if ok: # If bb small enough, get the corner coordinates (to be used in url-request) + # Set bounding box corner coordinates as geojson (x1,y1;x2,y2;...) + coords = "%f,%f;%f,%f;%f,%f;%f,%f" %(xmin,ymin,xmin,ymax,xmax,ymax,xmax,ymin) + else: # If bb too large + return # Return from (end) plugin + + # Accessing layers' tree root + root = QgsProject.instance().layerTreeRoot() + + # If reset box is checked, group "Nib-prosjekt" is deleted before recreated + # and filled with layers + if reset: # The reset box is checked + group = root.findGroup('Nib-prosjekt') # Find the group + root.removeChildNode(group) # Remove the group + # Recreate Nib-prosjekt group + # Add a layer group to be used for all orthophoto-projects within the active layer's extent + if not root.findGroup("Nib-prosjekt"): # If the group don't exist + group = root.addGroup('Nib-prosjekt') # add the group and name it "Nib-prosjekt" + # group.setExpanded(False) # Collapse the layer group + + # Load Norge i bilder-project based on active layer's bounding box + # (geojson-format x1,y1;x2,y2;x3,y3;...) + # https://stackoverflow.com/questions/50337388/how-to-use-special-character-%c3%a6-%c3%b8-or-%c3%a5-in-a-urllib-request-urlopen-in-python-3 + para=parse.quote('{Filter:"ortofototype in (1,2,3,4,8,9,12)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}') + inn = request.urlopen('https://tjenester.norgeibilder.no/rest/projectMetadata.ashx?request='+para).read() #list with of-projects + ut = inn.decode() # Convert from bytes to string + ut = ut.replace('"','') # Remove quotes, i.e. replace " with nothing + a = ut.split("[") # Split string in two at bracket [ + b = a[1].split("]") # Split 2nd. substring at each bracket [ gives 2 elements since there is only one occurence of [ (counting starts at 0) + nib_liste = b[0].split(",") + lyr = '' # Initiate + for nibprosjwms in nib_liste: # Loads WMS (raster layer) for each of-project within the bounding box in question + urlWithParams = 'contextualWMSLegend=0&crs=EPSG:'+str(crs_proj_int)+'&dpiMode=7&featureCount=10&format=image/png&layers='+nibprosjwms+'&styles&url=https://wms.geonorge.no/skwms1/wms.nib-prosjekter' + rlayer = QgsRasterLayer(urlWithParams, nibprosjwms, 'wms') # Get the raster layer + if rlayer.isValid(): # Valid raster layer + layers = QgsProject.instance().mapLayersByName(nibprosjwms) # Check if loaded layer already exists + if layers: + lyr = layers[0] + tree_layer = root.findLayer(lyr.id()) + if tree_layer: # True if layer exists, otherwise False + layer_parent = tree_layer.parent() + if str(layer_parent.name()) == 'Nib-prosjekt': # If layer exists in group Nib-prosjekt, it will not be added + self.iface.messageBar().pushMessage("Info", "WMS layer exists: "+ str(nibprosjwms),level=Qgis.Info, duration=1) + continue # Return the control to the beginning of the for loop + else: # The layer exists but not in the Nib-prsjekt group + QgsProject.instance().addMapLayer(rlayer, False) # Add the raster layer without showing it (False) + mygroup = root.findGroup("Nib-prosjekt") # Get the group named "Nib-prosjekt" + mygroup.addLayer(rlayer) # Add the layer to this group + # Uncheck the raster layer + QgsProject.instance().layerTreeRoot().findLayer(rlayer.id()).setItemVisibilityChecked(False) + self.iface.messageBar().pushMessage("Success", "WMS reloaded", level=Qgis.Success, duration=3) + else: # layer does not exist, thus it will be addedd + QgsProject.instance().addMapLayer(rlayer, False) # Add the raster layer without showing it (False) + mygroup = root.findGroup("Nib-prosjekt") # Get the group named "Nib-prosjekt" + mygroup.addLayer(rlayer) # Add the layer to this group + # Uncheck the raster layer + QgsProject.instance().layerTreeRoot().findLayer(rlayer.id()).setItemVisibilityChecked(False) + self.iface.messageBar().pushMessage("Success", "WMS added in Nib-prosjekt", level=Qgis.Success, duration=3) + else: # If not valid raster layer, an error message will appear + self.iface.messageBar().pushMessage("Warning", "Can't load: "+ str(nibprosjwms),level=Qgis.Warning, duration=3)