Skip to content
Snippets Groups Projects
get_nib.py 40.21 KiB
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 getnib
                                 A QGIS plugin

 Plugin "NIB-ortofoto-prosjekt"
 
 NB! The plugin will ONLY work for those with access to "Norge i bilder ortofoto-prosjekt WMS".
 NB! The project must be in EPSG 25832, 25833 or 25835.
 
 Get one, several or all orthophoto-projects (included "Satellitt" and "Infrarødt") in map canvas from "Norge i bilder prosjekt (WMS)" for one, several or all years available.
 The bounding box may be  
  - the visible map canvas (default)
  - a bouding box given by the user in integer km (width x height)
  - a layer in Layers panel
  - an uploaded vector og raster file

  The orthopohoto projectnames may be written to sorted/unsorted text file(s).
  The maximum size of the bounding box is set to 500 km x 500 km.


Resources used:
 - Plugin Builder to achieve correct configuration and neccessary files:
   https://www.qgistutorials.com/en/docs/3/building_a_python_plugin.html 
   NB! Rememeber to create and copy compile.bat (neccessary for compiling resources.py to make your own icon visible)
 - 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 (in Norwegian) to get the metadata and see the required format of input-values
 - Install QGIS plugin "Releod plugin" to update changes made in the plugin
 - QGIS installed through OSGeo4W-installation which includes Qt Designer for designing the plugin
 - Logo downloaded from 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.PyQt.QtGui import QRegExpValidator, QIntValidator
from qgis.PyQt.QtCore import QRegExp
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 parse  # to parse a string
import requests   # https://www.geeksforgeeks.org/response-json-python-requests/ https://realpython.com/python-requests/
import os  # get the current working directory, access to file / path
import datetime  # get the current year

# 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):
      """ To select a file which will serve as bounding box """
      filename, _filter = QFileDialog.getOpenFileName(
        self.dlg, "Select a file ","", 'geo-files (*.shp *.geojson *.gpkg *.gml *.jpg *.tif);; All files (*.*)')
      self.dlg.lineEdit_file.setText(filename)
      self.dlg.radioButton_file.setChecked(True)  # In case you choose a file directly without first chosing this option

    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 LAEA 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 500 km)
        xdist = (ur.x()-ll.x())/1000  # Get length in km (xmax-xmin)
        ydist = (ur.y()-ll.y())/1000  # Get height in km (ymax-ymin)
        if xdist > 500 or ydist > 500:
            self.iface.messageBar().pushMessage("Error", "Height or length of bounding box can't be > 500 km",level=Qgis.Critical, duration=3)
            ok = False
        else:
            ok = True  # Bounding box is small enough
        return ok

    ## Bounding box ##
    # When you change your mind during input (started input of one bb-option and then changes to another bb-option),
    # e.g. have entered a length or selected a file, but change your mind and click Use current map canvas instead.
    # Then length and the file should be wiped out.
    def notUdbb(self):
        self.dlg.lineEdit_lengthbb.clear()
        self.dlg.lineEdit_heightbb.clear()
        self.dlg.lineEdit_lengthbb.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")
        self.dlg.lineEdit_heightbb.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")

    def notFile(self):
        self.dlg.lineEdit_file.clear()
        self.dlg.lineEdit_file.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")

    # When "Use current map canvas"-option: length, height and file turns white (if you change your mind)
    def ucmcbbClicked(self):
        if self.dlg.radioButton_ucmcbb.isChecked():
            self.notUdbb()  # if you change option
            self.notFile()  # if you change option

    # When "User-defined"-option: length turns yellow and is focused
    def udbbClicked(self):
        if self.dlg.radioButton_udbb.isChecked():
            self.notFile()  # if you change option
            self.dlg.lineEdit_lengthbb.setFocus()
            self.dlg.lineEdit_lengthbb.setStyleSheet("QLineEdit"
                                "{"
                                "background-color : yellow"
                                "}")

    # When length entered: length turns white
    def lengthbbEdited(self):
        self.dlg.lineEdit_lengthbb.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")
        self.dlg.radioButton_udbb.setChecked(True)

    # When length finished entered: height turns yellow and is focused
    def lengthbbFinished(self):
        self.dlg.lineEdit_heightbb.setFocus()
        self.dlg.lineEdit_heightbb.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : yellow"
                            "}")

    # When height edited: height turns white
    def heightbbEdited(self):
        self.dlg.lineEdit_heightbb.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")

    # When "layer"-option:
    def lyrClicked(self):
        if self.dlg.radioButton_lyr.isChecked():
            self.notUdbb()  # if you change option
            self.notFile()  # if you change option

    # When selected lyr is changed
    def lyrActivated(self):
        self.dlg.radioButton_lyr.setChecked(True)

    # When "file"-option: file turns yellow and is focused
    def fileClicked(self):
        if self.dlg.radioButton_file.isChecked():
            if self.dlg.lineEdit_file.text() == '':
                self.dlg.lineEdit_file.setStyleSheet("QLineEdit"
                                "{"
                                "background-color : yellow"
                                "}")
            self.notUdbb()  # if you change option

    # When file selected: file lineedit turns white
    def fileSelected(self):
        self.dlg.lineEdit_file.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")

    ## Years ##
    # When "All years"-option: start-year turns white (if you change your mind)
    def allyearsClicked(self):
        if self.dlg.radioButton_allyears.isChecked():
            self.dlg.lineEdit_startyear.setStyleSheet("QLineEdit"
                                "{"
                                "background-color : white"
                                "}")
            self.dlg.lineEdit_startyear.clear()
            self.dlg.lineEdit_endyear.setStyleSheet("QLineEdit"
                                "{"
                                "background-color : white"
                                "}")
            self.dlg.lineEdit_endyear.clear()

    # When "Between years"-option: start-year turns yellow and is focused
    def btwyearsClicked(self):
        if self.dlg.radioButton_btwyears.isChecked():
            self.dlg.lineEdit_startyear.setFocus()
            self.dlg.lineEdit_startyear.setStyleSheet("QLineEdit"
                                "{"
                                "background-color : yellow"
                                "}")

    # When start-year entered: start-year turns white
    def startEdited(self):
        self.dlg.lineEdit_startyear.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")
        # Due to "All years"-option is set to default:
        self.dlg.radioButton_btwyears.setChecked(True)   # Check

    # When start-year finished entered: end-year turns yellow and is focused
    def startFinished(self):
        self.dlg.lineEdit_endyear.setFocus()
        self.dlg.lineEdit_endyear.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : yellow"
                            "}")

    # When end-year edited: end-year turns white
    def endEdited(self):
        self.dlg.lineEdit_endyear.setStyleSheet("QLineEdit"
                            "{"
                            "background-color : white"
                            "}")
    """ 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_file.clicked.connect(self.select_input_file)
            self.dlg.toolButton_help.clicked.connect(self.show_help)

            self.dlg.radioButton_ucmcbb.toggled.connect(self.ucmcbbClicked)            # When current map canvas bb is chosen

            self.dlg.radioButton_udbb.toggled.connect(self.udbbClicked)                # When User-defined bb is chosen
            self.dlg.lineEdit_lengthbb.textEdited.connect(self.lengthbbEdited)         # Whenever length is edited
            self.dlg.lineEdit_lengthbb.editingFinished.connect(self.lengthbbFinished)  # When you press ‘Enter’ or length loses focus
            self.dlg.lineEdit_heightbb.textEdited.connect(self.heightbbEdited)         # Whenever height is edited

            self.dlg.radioButton_lyr.toggled.connect(self.lyrClicked)                  # When lyr bb is chosen
            self.dlg.comboBox_lyr.activated.connect(self.lyrActivated)                 # Whenever a lyr item is chosen

            self.dlg.radioButton_file.toggled.connect(self.fileClicked)                # When file bb is chosen
            self.dlg.lineEdit_file.textChanged.connect(self.fileSelected)              # Whenever file is selected

            self.dlg.radioButton_allyears.toggled.connect(self.allyearsClicked)        # When All years is chosen
																					   
            self.dlg.radioButton_btwyears.toggled.connect(self.btwyearsClicked)        # When Between years is chosen
            self.dlg.lineEdit_startyear.textEdited.connect(self.startEdited)           # Whenever the start year is edited
            self.dlg.lineEdit_startyear.editingFinished.connect(self.startFinished)    # When you press ‘Enter’ or start loses focus
            self.dlg.lineEdit_endyear.textEdited.connect(self.endEdited)               # Whenever end year is edited

        # As default: Remember user input from previous run
        """ If this should not be the default, remove comment # from next line """
        # self.dlg.checkBox_resetNib.setChecked(False)  # Unchecked

        # As default: Set orthophoto-projects checkbox checked
        """ If this should not be the default, remove comment # from next line """
        self.dlg.checkBox_resetNib.setChecked(True)  # Unchecked

        # As default: Set map canvas checkbox to checked
        """ If this should not be the default, comment # next line """
        self.dlg.radioButton_ucmcbb.setChecked(True)  # Checked

        # Clear the contents of the lineEdit_lengthbb from previous runs
        self.dlg.lineEdit_lengthbb.clear()
        # Clear the contents of the lineEdit_heightbb from previous runs
        self.dlg.lineEdit_heightbb.clear()
        # Clear the contents of the lineEdit_start (year) from previous runs
        self.dlg.lineEdit_startyear.clear()
        # Clear the contents of the lineEdit_end (year) from previous runs
        self.dlg.lineEdit_endyear.clear()
        # Clear the contents of the lineEdit_file (select file) from previous runs
        self.dlg.lineEdit_file.clear()

        self.dlg.radioButton_file.setChecked(False)   # Unchecked

        # Fetch the currently loaded layers
        layers = QgsProject.instance().mapLayers()
        # Clear the contents of the combobox from previous runs
        self.dlg.comboBox_lyr.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_lyr.addItems(unique_lyr)  # Populate combobox
        self.dlg.comboBox_lyr.model().sort(0)       # Sort layer names (includes filepath) alphabetically

        # Length and height: allowing only 1-500 due to max. 500 km x 500 km
        l = self.dlg.lineEdit_lengthbb
        l.setValidator(QRegExpValidator(QRegExp("[1-9]|[1-9][0-9]|[1-4][0-9][0-9]|500")))
        h = self.dlg.lineEdit_heightbb
        h.setValidator(QRegExpValidator(QRegExp("[1-9]|[1-9][0-9]|[1-4][0-9][0-9]|500")))

        # As default: Set all years radiobutton to checked
        """ If this should not be the default, comment # next line """
        self.dlg.radioButton_allyears.setChecked(True)  # Checked

        # Start- and end-year: must be 1900-current year
        currentDate = datetime.date.today()
        cy = currentDate.year  # get current year, e.g. 2023 (position 0,1,2,3)
        # cy = 2042  # used for testing the regexp
        cy = str(cy)  # Convert number to string
        d3 = cy[2]    # Get the third digit (in position 2)
        d4 = cy[3]    # Get the fourth digit (in position 3)
        # # Debugging
        # self.iface.messageBar().pushMessage("Info", "Two last digits in year are: "+d3+', '+d4, level=Qgis.Info, duration=3)

        # Verifying the entered years
        start = self.dlg.lineEdit_startyear
        start.setValidator(QRegExpValidator(QRegExp("19[0-9][0-9]|20[0-"+str(int(d3)-1)+"][0-9]|20["+d3+"][0-"+d4+"]")))
        end = self.dlg.lineEdit_endyear
        end.setValidator(QRegExpValidator(QRegExp("19[0-9][0-9]|20[0-"+str(int(d3)-1)+"][0-9]|20["+d3+"][0-"+d4+"]")))

        # 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
        epsg_list = [25832, 25833, 25835]
        if not crs_proj_int in epsg_list:
            self.iface.messageBar().pushMessage("Error", "Project must be in UTM 32, 33 or 35", level=Qgis.Critical, duration=3)
            return  # Return from (end) plugin 
            
        """ 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 = ''  # Initialize
            param = ''
            reset = False
            of = False
            sat = False
            ir = False

            if self.dlg.checkBox_resetNib.isChecked():  # Empty the Nib-prosjekt group
                reset = True

            if self.dlg.checkBox_of_proj.isChecked():   # Orthphoto-projects is checked
                of = True

            if self.dlg.checkBox_sat_proj.isChecked():  # Satellite-projects is checked
                sat = True

            if self.dlg.checkBox_ir_proj.isChecked():   # Infrared-projects is checked
                ir = True

            # If checked, use current map canvas as bounding box (= extent of)
            if self.dlg.radioButton_ucmcbb.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
            # If checked, use the length and height from map canvas centre point
            elif self.dlg.radioButton_udbb.isChecked():
                l = self.dlg.lineEdit_lengthbb.text()
                h = self.dlg.lineEdit_heightbb.text()
                if l == "" or h == "":
                    self.iface.messageBar().pushMessage("Error", "Length and/or height are/is missing!", level=Qgis.Critical, duration=3)
                    return  # Return from (end) plugin 
                else:
                    # Length and height (in km) requires projected coordinated. Ensures this by converting the coordinates to LAEA
                    e = iface.mapCanvas().extent().center()
                    # Debugging
                    # self.iface.messageBar().pushMessage("Info", "Centre x, y: " + str(e.x()) +", "+ str(e.y())+", "+crs_proj_str, level=Qgis.Info, duration=5)
                    # Computes corner coordinates in LAEA regardless input crs
                    xmin = e.x() - int(l)*1000/2
                    xmax = e.x() + int(l)*1000/2
                    ymin = e.y() - int(h)*1000/2
                    ymax = e.y() + int(h)*1000/2
                    """ Check length and height of bounding box. Limit is set to 500 km """
                    ok = self.check_bbsize(crs_proj_str, xmin, xmax, ymin, ymax)  # Check if bb is small enough
            else:  # A layer or a file is selcted as bounding box
                if self.dlg.radioButton_lyr.isChecked():  # Layer-option checked
                    lyr_name = self.dlg.comboBox_lyr.currentText()  # Get the layer name
                    if lyr_name != "":  #  If a layer is present/chosen
                        # self.iface.messageBar().pushMessage("Info", "Chosen layer: "+lyr_name, level=Qgis.Info, duration=3)
                        selectedLayer = QgsProject.instance().mapLayersByName(lyr_name)[0]  # Get this vector layer
                        # Get the extent of the layer (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", "No layer selected!", level=Qgis.Critical, duration=3)
                        return  # Return from (end) plugin

                elif self.dlg.radioButton_file.isChecked():  # File-option checked
                    fname = self.dlg.lineEdit_file.text()  # Get the text (path and filename)
                    if fname != "":
                        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
                        # self.iface.messageBar().pushMessage("Info", "Chosen file: "+lname, level=Qgis.Info, duration=3)
                        file = r""+fname+""  # Reads the file
                        fLayer = QgsVectorLayer(file, lname, "ogr")  # If vector file
                        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()
                    else:
                        self.iface.messageBar().pushMessage("Error", "No file selected!", level=Qgis.Critical, duration=3)
                        return  # Return from (end) plugin
                # Neither layer nor file is selected
                else:
                    self.iface.messageBar().pushMessage("Error", "No extent", level=Qgis.Critical, duration=5)
               
                # 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()
                # coords = "%f,%f;%f,%f;%f,%f;%f,%f" %(xmin,ymin,xmin,ymax,xmax,ymax,xmax,ymin)
                # self.iface.messageBar().pushMessage("Info", "extent"+str(coords), level=Qgis.Info, duration=3)             
                # 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:  # Selected file or layer and project have NOT the same crs
                    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 bounding box
            if not root.findGroup("Nib-prosjekt"):     # If the group doesn'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 the bounding box (geojson-format x1,y1;x2,y2;x3,y3;...)
            # https://norgeibilder.no/dok/webtjenester.pdf
            # 1 = Ortofoto 10, 2 = Ortofoto 20, 3 = Ortofoto 50, 4 = Ortofoto N50, 5 = Ortofoto Skog, 6 = Satellittbilde, 7 = Infrarødt, 8 = Rektifiserte flybilder, 9 = Ortofoto
            if of and not sat and not ir:
                param ='{Filter:"ortofototype in (1,2,3,4,5,8,9)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}'
            if of and sat and not ir:
                param = '{Filter:"ortofototype in (1,2,3,4,5,6,8,9)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}'
            if of and sat and ir:
                param = '{Filter:"ortofototype in (1,2,3,4,5,6,7,8,9)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}'
            if sat and not of and not ir:
                param = '{Filter:"ortofototype in (6)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}'
            if sat and ir and not of:
                param ='{Filter:"ortofototype in (6,7)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}'
            if ir and not sat and not of:
                param ='{Filter:"ortofototype in (7)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}'
          
            # para=parse.quote('{Filter:"ortofototype in (1,2,3,4,5,6,7,8,9)",Coordinates:"'+coords+'",InputWKID:'+str(crs_proj_int)+',StopOnCover:false}')
            para=parse.quote(param)
            js = requests.get('https://tjenester.norgeibilder.no/rest/projectMetadata.ashx?request='+para).json()  #list with current of-project on json-format
            nib_liste = js['ProjectList']
            # Debugging
            # print(js)  # {Success:true,ErrorMessage:null,ProjectList:[Sør-Varanger 2019,Sør-Varanger veg 2016,Sør-Varanger 2013,Sør-Varanger 2011],ProjectMetadata:null}
            # print('nib_liste =',nib_liste)  # nib_liste = ['Sør-Varanger 2019', 'Sør-Varanger veg 2016', 'Sør-Varanger 2013', 'Sør-Varanger 2011']
            # self.iface.messageBar().pushMessage("Info", "All OF: "+ str(nib_liste),level=Qgis.Info, duration=3)

            nib_liste_years = []  # Initialize

            # Ensure 4 digits in start and end-year and start year < end year.
            if self.dlg.radioButton_btwyears.isChecked():
                # Get the extent of current map canvas (coordinates in the project's crs)
                start = self.dlg.lineEdit_startyear.text()
                end = self.dlg.lineEdit_endyear.text()
                if int(start) <= int(end):
                    if len(start) == 4 and len(end) == 4:
                        # Creates a new list with only the OF-proejcts from between the start- and end-year.
                        years = [str(y) for y in range(int(start),int(end)+1,1)]
                        # We have an original list with all nib-projects (nib_liste)
                        # We make a new list selecting projects from nib_liste from the wanted time period
                        for year in years:
                            for n in (nib_liste):
                                if year in n.split():
                                    nib_liste_years.append(n)
                        nib_liste = nib_liste_years
                    else:
                        self.iface.messageBar().pushMessage("Warning", "year must have 4 digits",level=Qgis.Warning, duration=3)
                        return
                else:
                    self.iface.messageBar().pushMessage("Warning", "end-year must be >= start-year",level=Qgis.Warning, duration=3)
                    return

            lyr = ''  # Initialize
            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-prosjekt 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:  # The 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)

            # Current working directory will be set as destination folder for project lists
            cwd = os.getcwd()  # Get current working directory
            # self.iface.messageBar().pushMessage("Info", "Folder is:" +str(cwd), level=Qgis.Info, duration=3)

            if self.dlg.checkBox_sort.isChecked():
                nib_liste = sorted(nib_liste)   # Sort the nib_liste alphabetically

            if self.dlg.checkBox_savelist.isChecked():
                if self.dlg.radioButton_btwyears.isChecked():
                    for y in years:  # Year-list is created above and contain all years from start to end year
                        # Delete files if existing
                        fn = str(cwd)+"\\OF_"+str(y)+".txt"
                        if os.path.isfile(fn):
                            os.remove(fn)
                        for n in nib_liste:
                            for year in years:
                                if year in n.split():
                                    if y == year:
                                        with open(str(cwd)+'\\OF_'+str(year)+'.txt','a',encoding='utf-8') as f:  # a = append to textfile
                                            f.write(str(n)+'\n')   # Write the OF-project(s) for the specific year to the textfile
                else:
                    # Delete file if existing
                    fn = str(cwd)+"\\OF_all.txt"
                    if os.path.isfile(fn):
                        os.remove(fn) 
                    with open(str(cwd)+'\\OF_all.txt','w',encoding='utf-8') as f:  # w = write - will overwrite any existing content https://www.w3schools.com/python/python_file_write.asp
                       for n in nib_liste:
                           f.write(str(n)+'\n')        # Write all OF-projects to the textfile
                # Show message
                self.iface.messageBar().pushMessage("Success", "List with Nib-projects saved to file", level=Qgis.Success, duration=3)