Skip to content
Snippets Groups Projects
get_nib.py 21.67 KiB
# -*- 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 QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction, QFileDialog

#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
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 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)  

        # 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)