# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Aderyn
                                 A QGIS plugin
 This plugin searches the LERC Wales Aderyn wildlife database.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2019-01-09
        git sha              : $Format:%H$
        copyright            : (C) 2019 by Biodiversity Information Service
        email                : enquiry@bis.org.uk
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
from PyQt5.QtCore import Qt, QSettings, QTranslator, qVersion, QCoreApplication, QVariant
from PyQt5.QtGui import *
from PyQt5.QtWidgets import QAction, QMessageBox, QFileDialog, QProgressBar
from PyQt5.QtSql import *  # http://pyqgis.org/blog/2013/04/11/creating-a-postgresql-connection-from-a-qgis-layer-datasource/
import qgis
from qgis.core import *
from qgis.gui import *
from qgis.utils import *
from . import osgr
import csv
import datetime
import subprocess
import sys

# Initialize Qt resources from file resources.py
from .resources import *
# Import the code for the dialog
from .aderyn_dialog import AderynDialog
from .settings_dialog import SettingsDialog
import os.path
import random
import json
from aderyn_query import AderynQuery
from aderyn_spreadsheet import AderynSpreadsheet

#Extra libraries.
from XlsxWriter import xlsxwriter

class Aderyn:
    """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
        self.canvas = iface.mapCanvas()
        # 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',
            'Aderyn_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Create the dialog (after translation) and keep reference
        self.dlg = AderynDialog()
        self.dlg_settings = SettingsDialog()

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Aderyn Data Search')
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar(u'Aderyn')
        self.toolbar.setObjectName(u'Aderyn')

        # Get the current database connection settings.
        s = QSettings()
        # s.remove("aderyn")
        self.dbhost = s.value("aderyn/dbhost", "")
        self.dbname = s.value("aderyn/dbname", "")
        self.dbuser = s.value("aderyn/dbuser", "")
        self.dbpassword = s.value("aderyn/dbpassword", "")
        self.dbport = s.value("aderyn/dbport", 0)

        # Get a reference to an osgr object and an osgrLayer object
        self.osgr = osgr.osgr()

        # Make a coordinate translator. Also need global references to OSGB and canvas CRSs since
        # they cannot be retrieved from a translator object.
        self.canvasCrs = iface.mapCanvas().mapSettings().destinationCrs().authid()
        self.osgbCrs = QgsCoordinateReferenceSystem("EPSG:27700").authid()
        self.transformCrs = QgsCoordinateTransform(self.canvas.mapSettings().destinationCrs(),
                                                   QgsCoordinateReferenceSystem("EPSG:27700"), QgsProject.instance())

        # Search location (gridref) and WKT (used for gridref and vector).
        self.SearchLocation = NULL
        self.SearchWKT = NULL
        self.SearchWKTCentre = NULL
        self.SearchGeometry = NULL # QgsGeometry instance.
        self.SearchGeometryType = NULL # 'Point','MultiPoint','Polyline','MultiPolyline','Polygon','MultiPolygon'

        # Store any gridsquare.
        self.gridSquareCentre = NULL  # QgsPointXY
        self.gridSquareRectangle = NULL  # QgsRectangle
        self.gridSquareRubberBand = NULL  # QgsRubberBand
        self.gridSquareMaxBuffer = 0  # QgsRubberBand - store the largest buffer.
        self.gridSquareMaxBufferBoundingBox = NULL

        # Vector.
        self.SearchVectorName = NULL
        self.SearchVectorLayer = NULL
        self.SearchVectorLayerFeatureCount = NULL
        self.SearchVectorLayerFeatureCountSelected = NULL # selectedFeatureCount()
        self.SearchVectorFeature = NULL

        self.SearchName = NULL
        self.SearchOutputFolder = NULL
        self.SearchCategories = []
        self.SearchCSVs = []
        self.Cat1Select = NULL
        self.Cat1Buffer = NULL
        self.Cat2Select = NULL
        self.Cat2Buffer = NULL
        self.Cat3Select = NULL
        self.Cat3Buffer = NULL
        self.Cat4Select = NULL
        self.Cat4Buffer = NULL
        self.BatsSelect = NULL
        self.BatsBuffer = NULL
        self.RnbSelect = NULL
        self.RnbBuffer = NULL
        self.CsvSelect = NULL

    # 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('Aderyn', 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:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToDatabaseMenu(
                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/aderyn/icon.png'
        self.add_action(
            icon_path,
            text=self.tr(u'Aderyn Data Search'),
            callback=self.run,
            parent=self.iface.mainWindow())

        # Settings option.
        icon_path_settings = ':/plugins/aderyn/icon_settings.png'
        self.add_action(
            icon_path_settings,
            text=self.tr(u'Settings'),
            callback=self.runSettings,
            add_to_menu=True,
            add_to_toolbar=False,
            parent=self.iface.mainWindow())

        # Add vector layer.
        self.dlg.tb_vector_layer.clicked.connect(self.addVector)

        # Database tester.
        self.dlg.pb_test_database.clicked.connect(self.testDatabaseConnection)

        # Locate GR.
        # self.dlg.pb_locate.clicked.connect(self.locateGridref)
        self.dlg.pb_locate.clicked.connect(self.displayGridref)

        # Open settings dialogue. Just open it - rather than call a function in this file.
        self.dlg.pb_settings.clicked.connect(self.runSettings)

        # Set output folder button.
        self.dlg.tb_output_folder.clicked.connect(self.outputFolder)

    def clearVariables(self):
        """Get all the variables from the GUI and load them into the class."""
        self.SearchLocation = NULL
        self.SearchWKT = NULL
        self.SearchWKTCentre = NULL
        self.SearchGeometry = NULL # QgsGeometry instance.

        # Store any gridsquare.
        self.gridSquareCentre = NULL  # QgsPointXY
        self.gridSquareRectangle = NULL  # QgsRectangle
        self.gridSquareRubberBand = NULL  # QgsRubberBand
        self.gridSquareMaxBuffer = 0  # QgsRubberBand - store the largest buffer.
        self.gridSquareMaxBufferBoundingBox = NULL

        # Vector.
        self.SearchVectorName = NULL
        self.SearchVectorLayer = NULL
        self.SearchVectorLayerFeatureCount = NULL
        self.SearchVectorLayerFeatureCountSelected = NULL # selectedFeatureCount()
        self.SearchVectorFeature = NULL

        self.SearchName = NULL
        self.SearchOutputFolder = NULL
        self.SearchCategories = []
        self.SearchCSVs = []
        self.Cat1Select = NULL
        self.Cat1Buffer = NULL
        self.Cat2Select = NULL
        self.Cat2Buffer = NULL
        self.Cat3Select = NULL
        self.Cat3Buffer = NULL
        self.Cat4Select = NULL
        self.Cat4Buffer = NULL
        self.BatsSelect = NULL
        self.BatsBuffer = NULL
        self.RnbSelect = NULL
        self.RnbBuffer = NULL
        self.CsvSelect = NULL

    def setVariables(self):
        """Get all the variables from the GUI and load them into the class."""
        self.SearchLocation = self.dlg.le_location.text()
        if self.dlg.cb_vector_layer.currentText() != 'Select...':
            self.getVectorLayer() # Get the vector layer and the feature count.
        self.SearchName = self.dlg.le_search_name.text()
        self.SearchOutputFolder = self.dlg.le_ouput_folder.text()
        self.Cat1Select = True if self.dlg.cb_cat1.isChecked() == True else False
        self.Cat1Buffer = self.dlg.sb_cat1_buffer.value()
        self.Cat2Select = True if self.dlg.cb_cat2.isChecked() == True else False
        self.Cat2Buffer = self.dlg.sb_cat2_buffer.value()
        self.Cat3Select = True if self.dlg.cb_cat3.isChecked() == True else False
        self.Cat3Buffer = self.dlg.sb_cat3_buffer.value()
        self.Cat4Select = True if self.dlg.cb_cat4.isChecked() == True else False
        self.Cat4Buffer = self.dlg.sb_cat4_buffer.value()
        self.BatsSelect = True if self.dlg.cb_bats.isChecked() == True else False
        self.BatsBuffer = self.dlg.sb_bats_buffer.value()
        self.RnbSelect = True if self.dlg.cb_rnb.isChecked() == True else False
        self.RnbBuffer = self.dlg.sb_rnb_buffer.value()
        self.CsvSelect = True if self.dlg.cb_csv.isChecked() == True else False

        # Output.
        # QMessageBox.information(None, "Success!", str(
        #     'Variables:\n' +
        #     'Location: ' + self.SearchLocation + '\n'
        #     'Search name: ' + self.SearchName + '\n'
        #     'Output folder: ' + self.SearchOutputFolder + '\n'
        #     'Cat1: ' + str(self.Cat1Select) + ' (' + str(self.Cat1Buffer) + ')' + '\n'
        #     'Cat2: ' + str(self.Cat2Select) + ' (' + str(self.Cat2Buffer) + ')' + '\n'
        #     'Cat3: ' + str(self.Cat3Select) + ' (' + str(self.Cat3Buffer) + ')' + '\n'
        #     'Cat4: ' + str(self.Cat4Select) + ' (' + str(self.Cat4Buffer) + ')' + '\n'
        #     'Bats: ' + str(self.BatsSelect) + ' (' + str(self.BatsBuffer) + ')' + '\n'
        #     'CSV: ' + str(self.CsvSelect) + '\n'
        #     ))

    def loadVectors(self):
        """ Load vectors from QGIS table of contents. """
        self.dlg.cb_vector_layer.clear()
        layers = [layer for layer in QgsProject.instance().mapLayers().values()]
        vector_layers = []
        for layer in layers:
            if layer.type() == QgsMapLayer.VectorLayer:
                vector_layers.append(layer.name())
        if len(vector_layers) > 0:
            # Add select to the start of the list.
            # vector_layers.append('Select...')
            vector_layers.insert(0, 'Select...')
        self.dlg.cb_vector_layer.addItems(vector_layers)

    def addVector(self):
        """ Open and add a vector layer from file dialogue. """
        inFile = str(QFileDialog.getOpenFileName(caption="Open Shapefile", filter="Shapefiles (*.shp)")[0])
        if inFile is not None:
            self.iface.addVectorLayer(inFile, str.split(os.path.basename(inFile), ".")[0], "ogr")
            self.loadVectors()

    def getVectorLayer(self):
        """ Get vector layer specified in combo box. """
        layer = None
        self.SearchVectorName = self.dlg.cb_vector_layer.currentText()
        for layer in QgsProject.instance().mapLayers().values():
            if layer.name() == self.SearchVectorName:
                self.SearchVectorLayer = layer
                self.SearchVectorLayerFeatureCount = layer.featureCount()
                self.SearchVectorLayerFeatureCountSelected = layer.selectedFeatureCount()
                break

        # What have we got?
        QgsMessageLog.logMessage('Layer feature count: ' + str(self.SearchVectorLayerFeatureCount) + ', selected feature count: ' + str(self.SearchVectorLayerFeatureCountSelected) + '.', 'Aderyn')

        # Get the feature (self.SearchVectorFeature)
        features = []
        if self.SearchVectorLayerFeatureCount == 1:
            QgsMessageLog.logMessage('Using single feature from vector layer.', 'Aderyn')
            features = layer.getFeatures()
        elif self.SearchVectorLayerFeatureCountSelected == 1:
            QgsMessageLog.logMessage('Using selected feature from vector layer.', 'Aderyn')
            features = layer.getSelectedFeatures()

        # Loop through - should only be 1! So no matter to loop.
        geom = NULL
        type = NULL
        wkt = NULL
        wkt_centre = NULL
        feature_length = 0
        feature_area = 0
        for feature in features:
            self.SearchVectorFeature = feature
            geom = feature.geometry()
            centroid = geom.centroid()
            geomSingleType = QgsWkbTypes.isSingleType(geom.wkbType())
            if geom.type() == QgsWkbTypes.PointGeometry:
                # the geometry type can be of single or multi type
                if geomSingleType:
                    x = geom.asPoint()
                    wkt = geom.asWkt()
                    wkt_centre = wkt # Point - centre is the same.
                    type = 'Point'
                else:
                    x = geom.asMultiPoint()
                    wkt = geom.asWkt()
                    wkt_centre = wkt  # Point - centre is the same.
                    type = 'MultiPoint'
            elif geom.type() == QgsWkbTypes.LineGeometry:
                if geomSingleType:
                    x = geom.asPolyline()
                    wkt = geom.asWkt()
                    wkt_centre = centroid.asWkt()
                    type = 'Polyline'
                    feature_length = geom.length()
                else:
                    x = geom.asMultiPolyline()
                    wkt = geom.asWkt()
                    wkt_centre = centroid.asWkt()
                    type = 'MultiPolyline'
                    feature_length = geom.length()
            elif geom.type() == QgsWkbTypes.PolygonGeometry:
                if geomSingleType:
                    x = geom.asPolygon()
                    wkt = geom.asWkt()
                    wkt_centre = centroid.asWkt()
                    type = 'Polygon'
                    feature_area = geom.area()
                else:
                    x = geom.asMultiPolygon()
                    wkt = geom.asWkt()
                    wkt_centre = centroid.asWkt()
                    type = 'MultiPolygon'
                    feature_area = geom.area()
            else:
                type = 'Unknown'

        # What did we get?
        self.SearchGeometry = geom
        self.SearchGeometryType = type
        self.SearchWKT = wkt
        self.SearchWKTCentre = wkt_centre
        QgsMessageLog.logMessage('Geom type: ' + str(type) + ', wkt: ' + str(wkt) + ', wkt centre: ' + str(wkt_centre) + ', length: ' + str(feature_length) + ', area: ' + str(feature_area) + '', 'Aderyn')

    def validateVariables(self):
        """ Validate that the categories - if a cat is selected then there should be a buffer. """
        if len(self.SearchName) < 2:
            # Search name - bit short!
            # QMessageBox.information(None, "Error!", str('Invalid invalid search name!'))
            self.iface.messageBar().pushMessage("Warning", "Invalid invalid search name!", level=Qgis.Warning)
            self.run()
        elif len(self.SearchOutputFolder) < 3:
            # Search output folder - bit short!
            # QMessageBox.information(None, "Error!", str('Invalid output folder!'))
            self.iface.messageBar().pushMessage("Warning", "Invalid output folder!", level=Qgis.Warning)
            self.run()
        elif self.Cat1Select is True and self.Cat1Buffer < 1:
            # QMessageBox.information(None, "Error!", str('No buffer specified for category 1!'))
            self.iface.messageBar().pushMessage("Warning", "No buffer specified for category 1!", level=Qgis.Warning)
            self.run()
        elif self.Cat2Select is True and self.Cat2Buffer < 1:
            # QMessageBox.information(None, "Error!", str('No buffer specified for category 2!'))
            self.iface.messageBar().pushMessage("Warning", "No buffer specified for category 2!", level=Qgis.Warning)
            self.run()
        elif self.Cat3Select is True and self.Cat3Buffer < 1:
            # QMessageBox.information(None, "Error!", str('No buffer specified for category 3!'))
            self.iface.messageBar().pushMessage("Warning", "No buffer specified for category 3!", level=Qgis.Warning)
            self.run()
        elif self.Cat4Select is True and self.Cat4Buffer < 1:
            # QMessageBox.information(None, "Error!", str('No buffer specified for category 4!'))
            self.iface.messageBar().pushMessage("Warning", "No buffer specified for category 4!", level=Qgis.Warning)
            self.run()
        elif self.BatsSelect is True and self.BatsBuffer < 1:
            # QMessageBox.information(None, "Error!", str('No buffer specified for bats!'))
            self.iface.messageBar().pushMessage("Warning", "No buffer specified for bats!", level=Qgis.Warning)
            self.run()
        elif self.RnbSelect is True and self.RnbBuffer < 1:
            # QMessageBox.information(None, "Error!", str('No buffer specified for RNB!'))
            self.iface.messageBar().pushMessage("Warning", "No buffer specified for RNB!", level=Qgis.Warning)
            self.run()
        else:
            return True

    def validateCategories(self):
        """ Add the categories to array if they are true and there is s buffer. """
        self.SearchCategories = []
        if self.Cat3Select is True and self.Cat3Buffer >= 1:
            fileName = 'cat3' + '_' + self.SearchName.replace(" ", "_").lower() + '_' + str(self.Cat3Buffer)
            cat3 = ['CAT3', self.Cat3Buffer, 'yellow', fileName, 'Locally Important Species', 'Loc. Important Spp.']
            self.SearchCategories.append(cat3)
        if self.Cat2Select is True and self.Cat2Buffer >= 1:
            fileName = 'cat2' + '_' + self.SearchName.replace(" ", "_").lower() + '_' + str(self.Cat2Buffer)
            cat2 = ['CAT2', self.Cat2Buffer, 'orange', fileName, 'Species of Conservation Concern', 'Spp. Cons. Concern']
            self.SearchCategories.append(cat2)
        if self.Cat1Select is True and self.Cat1Buffer >= 1:
            fileName = 'cat1' + '_' + self.SearchName.replace(" ", "_").lower() + '_' + str(self.Cat1Buffer)
            cat1 = ['CAT1', self.Cat1Buffer, 'red', fileName, 'Priority Species', 'Priority Spp.']
            self.SearchCategories.append(cat1)
        if self.Cat4Select is True and self.Cat4Buffer >= 1:
            fileName = 'cat4' + '_' + self.SearchName.replace(" ", "_").lower() + '_' + str(self.Cat4Buffer)
            cat4 = ['CAT4', self.Cat4Buffer, 'black', fileName, 'Other Species', 'Other Spp.']
            self.SearchCategories.append(cat4)
        if self.BatsSelect is True and self.BatsBuffer >= 1:
            fileName = 'bats' + '_' + self.SearchName.replace(" ", "_").lower() + '_' + str(self.BatsBuffer)
            bats = ['BATS', self.BatsBuffer, 'grey', fileName, 'Bats', 'Bats']
            self.SearchCategories.append(bats)
        if self.RnbSelect is True and self.RnbBuffer >= 1:
            fileName = 'rnb' + '_' + self.SearchName.replace(" ", "_").lower() + '_' + str(self.RnbBuffer)
            rnb = ['RNB', self.RnbBuffer, 'blue', fileName, 'Roof Nesting Birds', 'Roof Nest. Birds']
            self.SearchCategories.append(rnb)

        if len(self.SearchCategories) == 0:
            self.iface.messageBar().pushMessage("Warning", "No categories selected", level=Qgis.Warning)
            # self.run()
        else:
            return True

    def validateLocation(self):
        """Check vector layer, or if blank, validate that the search location (gridref) is a valid grid ref."""
        if self.SearchVectorName and self.SearchVectorLayer and self.SearchWKT == NULL:
            # We have a vector - but the feature count is not 1.
            self.iface.messageBar().pushMessage("Warning", "Error getting feature from vector layer. Vector layer has more than one (selected) feature?", level=Qgis.Warning)
        elif self.SearchLocation and len(self.SearchLocation) % 2 != 0:
            # Fine - number is even.
            # QMessageBox.information(None, "Error!", str('Invalid grid reference (not even)!'))
            self.iface.messageBar().pushMessage("Warning", "Invalid grid reference (not even)!", level=Qgis.Warning)
        elif self.SearchLocation and len(self.SearchLocation) < 4:
            # Min is 2 digits plus prefix.
            # QMessageBox.information(None, "Error!", str('Invalid grid reference (too short)!'))
            self.iface.messageBar().pushMessage("Warning", "Invalid grid reference (too short)!", level=Qgis.Warning)
        elif self.SearchLocation and len(self.SearchLocation) > 12:
            # Max is 10 nubmers plus prefix.
            # QMessageBox.information(None, "Error!", str('Invalid grid reference (too long)!'))
            self.iface.messageBar().pushMessage("Warning", "Invalid grid reference (too long)!", level=Qgis.Warning)
        else:
            return True

    def locateGridref(self):
        """Locate the gridref the user has specified. Pan and zoom the map. """
        # Get the variables.
        self.SearchLocation = self.dlg.le_location.text()
        if self.SearchLocation:

            res = self.osgr.enFromGR(self.SearchLocation)  # returns easting, northing, precision, retCheck[1]

            if res[0] == 0:
                # self.iface.messageBar().pushMessage("Warning", res[3], level=Qgis.Warning)  # Show verification warning if conversion has failed.
                self.iface.messageBar().pushMessage("Warning", "Grid Ref. Validation failed! " + res[3], level=Qgis.Warning)
                QMessageBox.information(None, "Error!", str("Grid Ref. Validation failed!"))
            else:
                QgsMessageLog.logMessage('GR converted successfully.', 'Aderyn')
                QgsMessageLog.logMessage('Res 0: ' + str(res[0]), 'Aderyn')  # Easting
                QgsMessageLog.logMessage('Res 1: ' + str(res[1]), 'Aderyn')  # Northing
                QgsMessageLog.logMessage('Res 2: ' + str(res[2]), 'Aderyn')  # Precision - i.e.100/1000
                QgsMessageLog.logMessage('Res 3: ' + str(res[3]), 'Aderyn')  # retCheck - verification??

                precision = res[2]  # i.e. 1000 for 1k square.
                x0 = (res[
                          0] - precision / 2)  # Easting and northing are the centre - so divide precision by 2 to move to the edge of the square.
                x1 = (res[0] + precision / 2)
                y0 = (res[1] - precision / 2)
                y1 = (res[1] + precision / 2)
                QgsMessageLog.logMessage('Grid ref located: ' + str(x0) + ', ' + str(x1) + ', ' + str(y0) + ', ' + str(y1) + '.', 'Aderyn')

                centre = QgsPointXY(res[0], res[1])
                ll = QgsPointXY(x0, y0)
                ur = QgsPointXY(x1, y1)
                rect = QgsRectangle(ll, ur)
                # centre = QgsPointXY(res[0], res[1])

                # Store the centre and square.
                self.gridSquareCentre = centre
                self.gridSquareRectangle = rect

                # Create and store the geometry.
                geometry = QgsGeometry()
                geometry = geometry.fromRect(rect)
                self.SearchGeometry = geometry
                self.SearchGeometryType = 'Polygon'

                # Build the centre WKT. self.gridSquareCentre = centre ~ centre = QgsPointXY(res[0], res[1])
                wkt = 'POINT(' \
                             + str(centre.x()) + ' ' + str(centre.y()) + ')'
                self.SearchWKT = wkt

                # Build the WKT.
                wkt_centre = 'POLYGON((' \
                      + str(rect.xMinimum()) + ' ' + str(rect.yMinimum()) + ',' \
                      + str(rect.xMinimum()) + ' ' + str(rect.yMaximum()) + ',' \
                      + str(rect.xMaximum()) + ' ' + str(rect.yMaximum()) + ',' \
                      + str(rect.xMaximum()) + ' ' + str(rect.yMinimum()) + ',' \
                      + str(rect.xMinimum()) + ' ' + str(rect.yMinimum()) + '))'
                self.SearchWKTCentre = wkt_centre

                QgsMessageLog.logMessage('WKT: ' + wkt + '.', 'Aderyn')
                QgsMessageLog.logMessage('WKT Centre: ' + wkt_centre + '.', 'Aderyn')

                # Display the gridref.
                # self.displayGridref()

        else:
            QgsMessageLog.logMessage('No grid ref specified.', 'Aderyn')

    def displayGridref(self):

        # Ensure the grid ref has been located and saved.
        self.locateGridref()

        # Do we have valid rect from locateGridref?
        if self.gridSquareRectangle:

            #Get the grid ref.
            rect = self.gridSquareRectangle
            geometry = QgsGeometry()
            geometry = geometry.fromRect(rect)

            # Get existing RubberBands
            rbs = [i for i in iface.mapCanvas().scene().items()
                   if issubclass(type(i), qgis._gui.QgsRubberBand)]

            # Remove existing RubberBands
            for rb in rbs:
                if rb in iface.mapCanvas().scene().items():
                    iface.mapCanvas().scene().removeItem(rb)

            # Only do this for canvas in OSGB or if there's a user-defined grid size. Firstly update the canvas CRS.
            self.canvasCrs = iface.mapCanvas().mapSettings().destinationCrs().authid()
            if self.canvasCrs == self.osgbCrs:

                r = QgsRubberBand(self.canvas, False)  # False = a polyline
                color = QColor(255, 0, 255)
                transparent = QColor(0, 0, 0, 0)
                # points = [QgsPoint(rect.xMinimum(), rect.yMinimum()),
                #           QgsPoint(rect.xMinimum(), rect.yMaximum()),
                #           QgsPoint(rect.xMaximum(), rect.yMaximum()),
                #           QgsPoint(rect.xMaximum(), rect.yMinimum()),
                #           QgsPoint(rect.xMinimum(), rect.yMinimum())]
                # r.setToGeometry(QgsGeometry.fromPolyline(points), None)
                r.setToGeometry(geometry, None)
                r.setColor(color)
                r.setFillColor(transparent)
                r.setWidth(2)

                # Store the Qgs RubberBand.
                self.gridSquareRubberBand = r

                #Pan and zoom to the gridref.
                # box = rect.boundingBoxOfSelected()
                self.canvas.setExtent(rect)
                self.canvas.refresh()
                self.canvas.zoomOut()  # Zoom out 1 level - to give a bit of context.

            else:
                self.iface.messageBar().pushMessage("Warning", "Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ").", level=Qgis.Warning)
                # QMessageBox.information(None, "Error!", str("Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ")."))

        else:
            QgsMessageLog.logMessage('No rectangle returned by locate grid ref.', 'Aderyn')

    def clearGridref(self):
        """ Clear all rubber bands. """

        # Get existing RubberBands
        rbs = [i for i in iface.mapCanvas().scene().items()
               if issubclass(type(i), qgis._gui.QgsRubberBand)]

        # Remove existing RubberBands
        for rb in rbs:
            if rb in iface.mapCanvas().scene().items():
                iface.mapCanvas().scene().removeItem(rb)

    # def displayBuffer(self, buffer, colour):
    #
    #     #Convert buffer to int.
    #     buffer = int(buffer)
    #
    #     # Buffer the rectangle. https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/geometry.html
    #     rect = self.gridSquareRectangle
    #     geometry = QgsGeometry()
    #     geometry = geometry.fromRect(rect)
    #     buffered = geometry.buffer(buffer, 10)
    #
    #     # Only do this for canvas in OSGB or if there's a user-defined grid size. Firstly update the canvas CRS.
    #     self.canvasCrs = iface.mapCanvas().mapSettings().destinationCrs().authid()
    #     if self.canvasCrs == self.osgbCrs:
    #
    #         r = QgsRubberBand(self.canvas, False)  # False = a polyline
    #         color = QColor(colour)
    #         transparent = QColor(0, 0, 0, 0)
    #         r.setToGeometry(buffered, None)
    #         r.setColor(color)
    #         r.setFillColor(transparent)
    #         r.setWidth(2)
    #
    #         # Store the buffer - if it's larger than the existing buffer.
    #         if buffer > self.gridSquareMaxBuffer:
    #             self.gridSquareMaxBuffer = buffer
    #             boundingbox = buffered.boundingBox()
    #             self.canvas.setExtent(boundingbox)
    #             # self.canvas.setExtent(buffered)
    #             self.canvas.refresh()
    #             self.canvas.zoomOut()  # Zoom out 1 level - to give a bit of context.
    #
    #     else:
    #         self.iface.messageBar().pushMessage("Warning", "Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ").", level=Qgis.Warning)
    #         # QMessageBox.information(None, "Error!", str("Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ")."))

    def createShapefileSearchLocation(self):
        """ Create (and add) shapefile for the search area (grid ref). """

        # Get the search geometry. https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/geometry.html
        # rect = self.gridSquareRectangle
        # geometry = QgsGeometry()
        # geometry = geometry.fromRect(rect)
        geometry = self.SearchGeometry # Just get the geometry object (QgsGeometry()) stored by the locate gridref or getVectorLayers function.


        # Only do this for canvas in OSGB or if there's a user-defined grid size. Firstly update the canvas CRS.
        self.canvasCrs = iface.mapCanvas().mapSettings().destinationCrs().authid()
        if self.canvasCrs == self.osgbCrs:

            # Create a writer.
            writerCrs = QgsCoordinateReferenceSystem(27700)

            # Set geom type - could be 'Point','MultiPoint','Polyline','MultiPolyline','Polygon','MultiPolygon'
            if self.SearchGeometryType == 'Point':
                shapefileType = QgsWkbTypes.Point
            elif self.SearchGeometryType == 'MultiPoint':
                shapefileType = QgsWkbTypes.Point
            elif self.SearchGeometryType == 'Polyline':
                shapefileType = QgsWkbTypes.LineString
            elif self.SearchGeometryType == 'MultiPolyline':
                shapefileType = QgsWkbTypes.LineString
            elif self.SearchGeometryType == 'Polygon':
                shapefileType = QgsWkbTypes.Polygon
            elif self.SearchGeometryType == 'MultiPolygon':
                shapefileType = QgsWkbTypes.Polygon

            QgsMessageLog.logMessage('Creating search location shapefile - geom type: ' + self.SearchGeometryType + '.', 'Aderyn')

            fileName = 'search_location'
            outputFile = os.path.join(self.SearchOutputFolder, fileName)
            QgsMessageLog.logMessage('Search location output file: ' + outputFile + '.', 'Aderyn')
            fields = QgsFields()
            fields.append(QgsField("id", QVariant.Int))
            fields.append(QgsField("type", QVariant.String))
            fields.append(QgsField("string", QVariant.String)) # Might be grid ref or blank.
            fields.append(QgsField("location", QVariant.String))
            writer = QgsVectorFileWriter(outputFile, "CP1250", fields, shapefileType, writerCrs, "ESRI Shapefile")

            # Errors?
            if writer.hasError() != QgsVectorFileWriter.NoError:
                # print("Error when creating shapefile: ", writer.errorMessage())
                QgsMessageLog.logMessage('Error when creating shapefile: ' + writer.errorMessage() + '.', 'Aderyn')

            # Write feature.
            fet = QgsFeature()
            fet.setGeometry(geometry)

            # self.SearchLocation = NULL
            # self.SearchName = NULL

            # Add in one line.
            fet.setAttributes([
                1,
                self.SearchGeometryType,
                self.SearchName,
                self.SearchLocation,
            ])

            # Add the feature.
            writer.addFeature(fet)

            # Delete the writer.
            del writer

            # Return the name of the shapefile.
            return fileName

        else:
            self.iface.messageBar().pushMessage("Warning", "Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ").", level=Qgis.Warning)
            # QMessageBox.information(None, "Error!", str("Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ")."))

    def styleShapefileSearchLocation(self, layer, fileName):
        """ Style the layer - and save the resulting QML file. """
        renderer = layer.renderer()

        # Set geom type - could be 'Point','MultiPoint','Polyline','MultiPolyline','Polygon','MultiPolygon'
        if self.SearchGeometryType == 'Point':
            symbol = QgsMarkerSymbol.createSimple({'name': 'triangle', 'color': '255, 0, 255', 'size': '3', })
        elif self.SearchGeometryType == 'MultiPoint':
            symbol = QgsMarkerSymbol.createSimple({'name': 'triangle', 'color': '255, 0, 255', 'size': '3', })
        elif self.SearchGeometryType == 'Polyline':
            symbol = QgsLineSymbol.createSimple({'color': '255, 0, 255', 'width': '0.66'})
        elif self.SearchGeometryType == 'MultiPolyline':
            symbol = QgsLineSymbol.createSimple({'color': '255, 0, 255', 'width': '0.66'})
        elif self.SearchGeometryType == 'Polygon':
            symbol = QgsFillSymbol.createSimple({'outline_color': '255, 0, 255', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
        elif self.SearchGeometryType == 'MultiPolygon':
            symbol = QgsFillSymbol.createSimple({'outline_color': '255, 0, 255', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
        # symbol = QgsFillSymbol.createSimple({'outline_color': '255, 0, 255', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
        renderer.setSymbol(symbol)

        # Update the symbolgy on the layer tree (refresh)
        iface.layerTreeView().refreshLayerSymbology(iface.activeLayer().id())

        # Save the style file for future use in QGIS. Get the layer source and change the file extension.
        outputFile = os.path.join(self.SearchOutputFolder, fileName) + '.qml'
        layer.saveNamedStyle(outputFile)

    def createShapefileBuffer(self, category):
        """ Create shapefile for search geometry and buffer. """

        # Break up the category. # category = ['CAT1', self.Cat1Buffer, 'red', fileName, 'Priority Species']
        searchCategory = category[0]
        searchCategoryBuffer = category[1]
        searchCategoryColour = category[2]
        searchCategoryFileName = category[3]
        searchCategoryFullName = category[4]
        searchCategoryFullNameAbbrev = category[5]

        #Convert buffer to int.
        buffer = int(searchCategoryBuffer)

        # Buffer the search geometry. https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/geometry.html
        geometry = self.SearchGeometry # Just get the geometry object (QgsGeometry()) stored by the locate gridref or getVectorLayers function.
        buffered = geometry.buffer(buffer, 10)

        # Only do this for canvas in OSGB or if there's a user-defined grid size. Firstly update the canvas CRS.
        self.canvasCrs = iface.mapCanvas().mapSettings().destinationCrs().authid()
        if self.canvasCrs == self.osgbCrs:

            # Create a writer.
            writerCrs = QgsCoordinateReferenceSystem(27700)
            shapefileType = QgsWkbTypes.Polygon # Buffer is always a polygon.
            fileName = searchCategory.lower() + '_buffer_' + str(buffer)
            outputFile = os.path.join(self.SearchOutputFolder, fileName)
            QgsMessageLog.logMessage('Buffer ' + searchCategory + ' output file: ' + outputFile + '.', 'Aderyn')
            fields = QgsFields()
            fields.append(QgsField("id", QVariant.Int))
            fields.append(QgsField("grid_ref", QVariant.String))
            fields.append(QgsField("location", QVariant.String))
            fields.append(QgsField("buffer", QVariant.Int))
            writer = QgsVectorFileWriter(outputFile, "CP1250", fields, shapefileType, writerCrs, "ESRI Shapefile")

            # Errors?
            if writer.hasError() != QgsVectorFileWriter.NoError:
                print("Error when creating shapefile: ", writer.errorMessage())

            # Write feature.
            fet = QgsFeature()
            fet.setGeometry(buffered)

            # self.SearchLocation = NULL
            # self.SearchName = NULL

            # Add in one line.
            fet.setAttributes([
                1,
                self.SearchName,
                self.SearchLocation,
                buffer,
            ])

            # Add the feature.
            writer.addFeature(fet)

            # Delete the writer.
            del writer

            # Store the buffer - if it's larger than the existing buffer - so we know the bounding box of the largest buffer.
            if buffer > self.gridSquareMaxBuffer:
                self.gridSquareMaxBuffer = buffer
                boundingbox = buffered.boundingBox()
                self.gridSquareMaxBufferBoundingBox = boundingbox

                # # Zoom.
                # self.canvas.setExtent(boundingbox)
                # self.canvas.refresh()
                # self.canvas.zoomOut()  # Zoom out 1 level - to give a bit of context.

            # Return the name of the shapefile.
            return fileName

        else:
            self.iface.messageBar().pushMessage("Warning", "Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ").", level=Qgis.Warning)
            # QMessageBox.information(None, "Error!", str("Incorrect map CRS (" + str(self.canvasCrs) + ")! Map view must be in OSGB36 (" + self.osgbCrs + ")."))

    def styleShapefileBuffer(self, layer, category, fileName):
        """ Style the layer - and save the resulting QML file. """
        renderer = layer.renderer()
        if category == 'CAT1':
            symbol = QgsFillSymbol.createSimple({'outline_color': 'red', 'outline_width': '0.66', 'outline_width_unit': 'MM', 'outline_style': 'solid', 'style':'no' })
            renderer.setSymbol(symbol)
        elif category == 'CAT2':
            symbol = QgsFillSymbol.createSimple({'outline_color': 'orange', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
            renderer.setSymbol(symbol)
        elif category == 'CAT3':
            symbol = QgsFillSymbol.createSimple({'outline_color': 'yellow', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
            renderer.setSymbol(symbol)
        elif category == 'CAT4':
            symbol = QgsFillSymbol.createSimple({'outline_color': 'black', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
            renderer.setSymbol(symbol)
        elif category == 'BATS':
            symbol = QgsFillSymbol.createSimple({'outline_color': 'black', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
            renderer.setSymbol(symbol)
        elif category == 'RNB':
            symbol = QgsFillSymbol.createSimple({'outline_color': 'blue', 'outline_width': '0.66', 'outline_style': 'solid', 'style':'no' })
            renderer.setSymbol(symbol)

        # Update the symbolgy on the layer tree (refresh)
        iface.layerTreeView().refreshLayerSymbology(iface.activeLayer().id())

        # Save the style file for future use in QGIS. Get the layer source and change the file extension.
        outputFile = os.path.join(self.SearchOutputFolder, fileName) + '.qml'
        layer.saveNamedStyle(outputFile)

    def loadSetings(self):
        """Get the current db settings and load them into the form fields."""
        self.dlg_settings.le_dbhost.setText(self.dbhost)
        self.dlg_settings.le_dbuser.setText(self.dbuser)
        self.dlg_settings.le_dbpassword.setText(self.dbpassword)

    def saveSettings(self):
        """Get the settings from the form fields and update the internal variables."""
        self.dbhost = self.dlg_settings.le_dbhost.text()
        self.dbname = "lrc_wales_data"
        self.dbuser = self.dlg_settings.le_dbuser.text()
        self.dbpassword = self.dlg_settings.le_dbpassword.text()
        # Save the supplied settings.
        s = QSettings()
        s.setValue("aderyn/dbhost", self.dbhost)
        s.setValue("aderyn/dbname", "lrc_wales_data")
        s.setValue("aderyn/dbuser", self.dbuser)
        s.setValue("aderyn/dbpassword", self.dbpassword)
        s.setValue("aderyn/dbport", 5432)

        QMessageBox.information(None, "Success!", str('Settings saved!'))

    def testDatabaseConnection(self):
        """Test the database connection to LERC Wales merged database."""
        # http://pyqgis.org/blog/2013/04/11/creating-a-postgresql-connection-from-a-qgis-layer-datasource/
        # https://gis.stackexchange.com/questions/29721/how-to-connect-a-pyqgis-plugin-with-postgres
        db = QSqlDatabase.addDatabase('QPSQL')
        if db.isValid():
            db.setHostName(self.dbhost)
            db.setDatabaseName(self.dbname)
            db.setUserName(self.dbuser)
            db.setPassword(self.dbpassword)
            db.setPort(int(self.dbport))
            if db.open():
                # QMessageBox.information(None, "Success!", str('Database opened!'))
                QgsMessageLog.logMessage('Database opened successfully.', 'Aderyn')
                offset = str(random.randint(1, 100000))
                # AderynQueryObj = aderyn_query.AderynQuery()
                AderynQueryObj = AderynQuery()
                queryString = AderynQueryObj.sqlQueryTest(offset)
                # query = db.exec_("SELECT * FROM lrc_wales_data.records LIMIT 1 OFFSET " + offset)
                query = db.exec_(queryString)
                # iterate over the rows
                while query.next():
                    record = query.record()
                    # print the value of the first column
                    # print record.value(0)
                    QMessageBox.information(None, "Success!", str(
                        'Random record: \nTOK: ' + record.field('tok').value() + '\nSpecies: ' + record.field(
                            'actual_name').value() + '\nLERC: ' + record.field('lrc').value()))
                # Close the connection.
                db.removeDatabase('QPSQL')
            else:
                QMessageBox.information(None, "Error!", str('Unable to open database!'))
        else:
            QMessageBox.information(None, "Error!", str('Invalid database specification!'))

    def outputFolder(self):
        """ Specify the output folder """
        outputFolder = str(
            QFileDialog.getExistingDirectory(caption="Set output folder", options=QFileDialog.ShowDirsOnly))
        self.setOutputFolderLine(outputFolder)

    def setOutputFolderLine(self, text):
        """ Ser the folder name in the output line edit."""
        self.dlg.le_ouput_folder.setText(text)

    def createShapefile(self, category, buffer, query, fileName):
        """ Take query object and create shapefile. """
        # Create a writer.
        writerCrs = QgsCoordinateReferenceSystem(27700)
        shapefileType = QgsWkbTypes.Point  # Point - could be polygon.
        #fileName = category.lower() + '_' + self.SearchName.replace(" ", "_").lower() + '_' + buffer
        outputFile = os.path.join(self.SearchOutputFolder, fileName)
        QgsMessageLog.logMessage('Output file: ' + outputFile + '.', 'Aderyn')
        fields = QgsFields()
        fields.append(QgsField("id", QVariant.Int))
        fields.append(QgsField("sensitive", QVariant.String))
        fields.append(QgsField("gr", QVariant.String))
        fields.append(QgsField("public_gr", QVariant.String))
        fields.append(QgsField("distance", QVariant.Int))
        fields.append(QgsField("actual_name", QVariant.String))
        fields.append(QgsField("common_name", QVariant.String))
        fields.append(QgsField("date_formatted", QVariant.String))
        fields.append(QgsField("recorder", QVariant.String))
        fields.append(QgsField("abundance", QVariant.String))
        fields.append(QgsField("location", QVariant.String))
        fields.append(QgsField("comments", QVariant.String))
        fields.append(QgsField("full_status", QVariant.String))
        fields.append(QgsField("source", QVariant.String))
        fields.append(QgsField("determiner", QVariant.String))
        fields.append(QgsField("verificat", QVariant.String))
        fields.append(QgsField("taxon_group", QVariant.String))  # name_taxon_nbn_group
        fields.append(QgsField("super_group", QVariant.String))  # name_taxon_super_group
        writer = QgsVectorFileWriter(outputFile, "CP1250", fields, shapefileType, writerCrs, "ESRI Shapefile")

        # Errors?
        if writer.hasError() != QgsVectorFileWriter.NoError:
            print("Error when creating shapefile: ", writer.errorMessage())

        # Loop through and write features.
        while query.next():
            record = query.record()
            geom = record.field('geom_point').value()
            # QgsMessageLog.logMessage('Geom point GeoJSON: ' + str(geom) + '.', 'Aderyn')
            geom_array = json.loads(str(geom))
            x = geom_array['coordinates'][0]  # Easting
            y = geom_array['coordinates'][1]  # Northing
            # QgsMessageLog.logMessage('Easting: ' + str(x) + ', northing: ' + str(y) + '.', 'Aderyn')

            #Set the verification level string.
            verification_code = record.field('verification_level').value()
            verification_string = 'Unassessed'
            if verification_code == 2:
                verification_string = 'Unconfirmed'
            elif verification_code == 3:
                verification_string = 'Correct'

            fet = QgsFeature()
            fet.setGeometry(QgsPoint(x, y))
            # Add in one line.
            fet.setAttributes([
                record.field('id').value(),
                record.field('sensitive').value(),
                record.field('gr').value(),
                record.field('public_gr').value(),
                record.field('distance').value(),
                record.field('actual_name').value(),
                record.field('common_name').value(),
                record.field('date_formatted').value(),
                record.field('recorder').value(),
                record.field('abundance').value(),
                record.field('location').value(),
                record.field('comments').value(),
                record.field('full_status').value(),
                record.field('source').value(),
                record.field('determiner').value(),
                # record.field('verification_level').value(),
                verification_string,
                record.field('name_taxon_nbn_group').value(),  # name_taxon_nbn_group
                record.field('name_taxon_super_group').value(),  # name_taxon_super_group
            ])

            # Add the feature.
            writer.addFeature(fet)

        # Delete the writer.
        del writer

        # Return the name of the shapefile.
        return fileName

    def styleShapefile(self, layer, category):
        """ Style the layer - and save the resulting QML file. """

        # Break up the category. # category = ['CAT1', self.Cat1Buffer, 'red', fileName, 'Priority Species']
        searchCategory = category[0]
        searchCategoryBuffer = category[1]
        searchCategoryColour = category[2]
        searchCategoryFileName = category[3]
        searchCategoryFullName = category[4]
        searchCategoryFullNameAbbrev = category[5]

        renderer = layer.renderer()
        if searchCategory == 'CAT1':
            symbol = QgsMarkerSymbol.createSimple({'name': 'circle', 'color': 'red', 'size': '2', })
        elif searchCategory == 'CAT2':
            symbol = QgsMarkerSymbol.createSimple({'name': 'circle', 'color': 'orange', 'size': '3', })
        elif searchCategory == 'CAT3':
            symbol = QgsMarkerSymbol.createSimple({'name': 'circle', 'color': 'yellow', 'size': '4', })
        elif searchCategory == 'CAT4':
            symbol = QgsMarkerSymbol.createSimple({'name': 'star', 'color': 'black', 'size': '2', })
        elif searchCategory == 'BATS':
            symbol = QgsMarkerSymbol.createSimple({'name': 'triangle', 'color': 'black', 'size': '2', })
        elif searchCategory == 'RNB':
            symbol = QgsMarkerSymbol.createSimple({'name': 'circle', 'color': 'blue', 'size': '2', })
        renderer.setSymbol(symbol)

        # Update the symbolgy on the layer tree (refresh)
        iface.layerTreeView().refreshLayerSymbology(iface.activeLayer().id())

        #Add labels.
        layer_settings = QgsPalLayerSettings()
        layer_settings.fieldName = "gr"
        # layer_settings.fieldName = "concat(grid_ref, ' (', actual_nam, ')')"
        layer_settings.isExpression = True

        # layer_settings.placement = QgsPalLayerSettings.AroundPoint
        layer_settings.placement = 1 # Placement: Cartographic = 6; Around point = 0; Offset from point = 1
        layer_settings.quadOffset = 2 # Quadrant position: QuadrantAboveLeft = 0; QuadrantAbove = 1,...
        layer_settings.xOffset = 2.0
        layer_settings.yOffset = -1.0
        # layer_settings.OffsetUnits = QgsUnitTypes.RenderUnit.RenderMillimeters
        # layer_settings.dist = 1
        layer_settings.enabled = True

        text_format = QgsTextFormat()
        text_format.setFont(QFont("Arial Black", 6))
        text_format.setSize(6)
        layer_settings.setFormat(text_format)

        labeling = QgsVectorLayerSimpleLabeling(layer_settings)
        layer.setLabelsEnabled(True)
        layer.setLabeling(labeling)
        layer.triggerRepaint()

        # Save the style file for future use in QGIS. Get the layer source and change the file extension.
        #base = os.path.splitext(layer.source())[0]
        #newFileQml = base + '.qml'
        outputFile = os.path.join(self.SearchOutputFolder, searchCategoryFileName) + '.qml'
        layer.saveNamedStyle(outputFile)

    def createCsv(self, layer, category):
        """ Create CSV file from the layer. """

        # Break up the category. # category = ['CAT1', self.Cat1Buffer, 'red', fileName, 'Priority Species']
        searchCategory = category[0]
        searchCategoryBuffer = category[1]
        searchCategoryColour = category[2]
        searchCategoryFileName = category[3]
        searchCategoryFullName = category[4]
        searchCategoryFullNameAbbrev = category[5]

        #fileName = layer.source()

        #Get filename for CSV.
        #base = os.path.splitext(fileName)[0]
        #fileNameCsv = base + '.csv'
        outputFile = os.path.join(self.SearchOutputFolder, searchCategoryFileName) + '.csv'

        #Add the CSV to the array of CSVs. We'll use this later to create the XLS.
        # self.SearchCSVs.append(outputFile)

        QgsMessageLog.logMessage('Creating CSV file: ' + outputFile, 'Aderyn')

        #Set the CSR.
        writerCrs = QgsCoordinateReferenceSystem(27700)  # Not sure if we need this.
        error = QgsVectorFileWriter.writeAsVectorFormat(layer, outputFile, "utf-8", writerCrs, "CSV", layerOptions=['GEOMETRY=AS_WKT'])
        if error == QgsVectorFileWriter.NoError:
            QgsMessageLog.logMessage('Successfully creating CSV file: ' + outputFile, 'Aderyn')

    def createXlsx(self):
        """ Create the XLSX file from any CSV files. """
        QgsMessageLog.logMessage('Creating XLSX file...', 'Aderyn')

        #Create the XLSX file.
        searchNameCleaned = self.SearchName.replace(" ", "_").lower()
        xlsxFile = os.path.join(self.SearchOutputFolder, searchNameCleaned) + '.xlsx'
        workbook = xlsxwriter.Workbook(xlsxFile)
        QgsMessageLog.logMessage('Created XLSX file ' + xlsxFile, 'Aderyn')

        #Add formats.
        cell_format_bold = workbook.add_format({'bold': True})
        cell_format_bold_medium = workbook.add_format({'bold': True, 'font_size': 14})
        cell_format_bold_large = workbook.add_format({'bold': True, 'font_size': 26})
        cell_format_bold_border = workbook.add_format({'bold': True})
        cell_format_bold_border.set_border()

        #Setup spreadhseet class.
        AderynSpreadsheetObj = AderynSpreadsheet(self.SearchName, self.SearchLocation)

        #Add the overview sheet.
        worksheet = workbook.add_worksheet('Information')
        worksheet.set_column('A:A', 3)  # Decrease the width.
        lines = AderynSpreadsheetObj.linesInformation()
        for line in lines:
            if line[0] == 'text':
                if len(line) == 4:
                    if line[3] == 'cell_format_bold':
                        worksheet.write(line[1], line[2], cell_format_bold)
                    elif line[3] == 'cell_format_bold_medium':
                        worksheet.write(line[1], line[2], cell_format_bold_medium)
                    elif line[3] == 'cell_format_bold_large':
                        worksheet.write(line[1], line[2], cell_format_bold_large)
                    elif line[3] == 'cell_format_bold_border':
                        worksheet.write(line[1], line[2], cell_format_bold_border)
                    else:
                        worksheet.write(line[1], line[2])
                else:
                    worksheet.write(line[1], line[2])
            elif line[0] == 'merge':
                if len(line) == 4:
                    if line[3] == 'cell_format_bold':
                        worksheet.merge_range(line[1], line[2], cell_format_bold)
                    elif line[3] == 'cell_format_bold_medium':
                        worksheet.merge_range(line[1], line[2], cell_format_bold_medium)
                    elif line[3] == 'cell_format_bold_large':
                        worksheet.merge_range(line[1], line[2], cell_format_bold_large)
                    elif line[3] == 'cell_format_bold_border':
                        worksheet.merge_range(line[1], line[2], cell_format_bold_border)
                    else:
                        worksheet.merge_range(line[1], line[2])
                else:
                    worksheet.merge_range(line[1], line[2])

        #Loop through categories and add the CSV files.
        categories = self.SearchCategories
        categories = categories[::-1] #Reverse the list - we want to add cat1 first etc.
        for category in categories:
            # cat1 = ['CAT1', self.Cat1Buffer, 'red', fileName, 'Priority Species']
            QgsMessageLog.logMessage('Processing CSV file ' + category[3], 'Aderyn')
            csvFile = os.path.join(self.SearchOutputFolder, category[3]) + '.csv'
            worksheet = workbook.add_worksheet(category[4])
            worksheet.write('A1', 'SENSITIVE SPECIES RECORDS ARE HIGHLIGHTED IN BOLD – do NOT release into public domain', cell_format_bold)
            with open(csvFile, 'rt', encoding='utf8') as f:
                reader = csv.reader(f)
                for r, row in enumerate(reader):
                    # QgsMessageLog.logMessage('Sensitive: ' + row[2], 'Aderyn')
                    if row[2] == 't':
                        for c, col in enumerate(row):
                            worksheet.write(r + 1, c, col, cell_format_bold) #We've added the first row already - so r + 1.
                    else:
                        for c, col in enumerate(row):
                            worksheet.write(r + 1, c, col) #We've added the first row already - so r + 1.

        #Add the T&C sheet.
        worksheet = workbook.add_worksheet('T&C')
        worksheet.set_column('A:A', 35) #Increase the width.
        worksheet.set_column('B:B', 35)
        worksheet.set_column('C:C', 35)
        lines = AderynSpreadsheetObj.linesTandC()
        for line in lines:
            if line[0] == 'text':
                if len(line) == 4:
                    if line[3] == 'cell_format_bold':
                        worksheet.write(line[1], line[2], cell_format_bold)
                    elif line[3] == 'cell_format_bold_medium':
                        worksheet.write(line[1], line[2], cell_format_bold_medium)
                    elif line[3] == 'cell_format_bold_large':
                        worksheet.write(line[1], line[2], cell_format_bold_large)
                    elif line[3] == 'cell_format_bold_border':
                        worksheet.write(line[1], line[2], cell_format_bold_border)
                    else:
                        worksheet.write(line[1], line[2])
                else:
                    worksheet.write(line[1], line[2])
            elif line[0] == 'merge':
                if len(line) == 4:
                    if line[3] == 'cell_format_bold':
                        worksheet.merge_range(line[1], line[2], cell_format_bold)
                    elif line[3] == 'cell_format_bold_medium':
                        worksheet.merge_range(line[1], line[2], cell_format_bold_medium)
                    elif line[3] == 'cell_format_bold_large':
                        worksheet.merge_range(line[1], line[2], cell_format_bold_large)
                    elif line[3] == 'cell_format_bold_border':
                        worksheet.merge_range(line[1], line[2], cell_format_bold_border)
                    else:
                        worksheet.merge_range(line[1], line[2])
                else:
                    worksheet.merge_range(line[1], line[2])

        #Close.
        workbook.close()

    def runSearch(self, category):
        """Run the search using the parameters suplied."""
        # QMessageBox.information(None, "Success!", str('You clicked ok - searching ' + category + '!'))

        # Break up the category. # category = ['CAT1', self.Cat1Buffer, 'red', fileName, 'Priority Species']
        searchCategory = category[0]
        searchCategoryBuffer = category[1]
        searchCategoryColour = category[2]
        searchCategoryFileName = category[3]
        searchCategoryFullName = category[4]
        searchCategoryFullNameAbbrev = category[5]

        db = QSqlDatabase.addDatabase('QPSQL')
        if db.isValid():
            db.setHostName(self.dbhost)
            db.setDatabaseName(self.dbname)
            db.setUserName(self.dbuser)
            db.setPassword(self.dbpassword)
            db.setPort(int(self.dbport))
            if db.open():
                # QMessageBox.information(None, "Success!", str('Database opened!'))
                QgsMessageLog.logMessage('Database opened successfully.', 'Aderyn')
                QgsMessageLog.logMessage('Searching ' + searchCategory + ', buffer ' + str(searchCategoryBuffer), 'Aderyn')

                # Run the search.
                # AderynQueryObj = aderyn_query.AderynQuery()
                AderynQueryObj = AderynQuery()
                queryString = AderynQueryObj.sqlQuery(searchCategory, self.SearchWKT, self.SearchWKTCentre, searchCategoryBuffer)
                # QgsMessageLog.logMessage('SQL: ' + queryString + '', 'Aderyn')
                query = db.exec_(queryString)
                QgsMessageLog.logMessage(searchCategory + ' query returned ' + str(query.size()) + ' rows.', 'Aderyn')

                # Did we get any data?
                if query.size() >= 0:

                    # Create the shapefile and get the name of the file.
                    shapeFile = self.createShapefile(searchCategory, searchCategoryBuffer, query, searchCategoryFileName)

                    # Get the file name - for adding it to the interface.
                    fileNameShp = shapeFile + '.shp';
                    newFile = os.path.join(self.SearchOutputFolder, fileNameShp)

                    # Add the shapefile to QGIS'
                    # layer = iface.addVectorLayer(newFile, category + ' - ' + self.SearchName, "ogr")
                    # layer = iface.addVectorLayer(newFile, searchCategory + ' - ' + self.SearchName + ' (' + str(searchCategoryBuffer) + 'm)', "ogr")
                    layer = iface.addVectorLayer(newFile, searchCategoryFullNameAbbrev + ' (' + str(searchCategoryBuffer) + 'm)', "ogr")

                    if not layer:
                        self.iface.messageBar().pushMessage("Warning", 'Failed to load layer into interface (' + newFile + ')!', level=Qgis.Warning)
                        # QMessageBox.information(None, "Error!", str('Failed to load layer into interface (' + newFile + ')!'))
                    else:
                        # Style the layer.
                        self.styleShapefile(layer, category)

                        # If selected, convert the layer to a CSV.
                        if self.CsvSelect:
                            self.createCsv(layer, category)

                else:
                    self.iface.messageBar().pushMessage("Warning", searchCategory + ' did not return any records! (' + str(query.size()) + ')', level=Qgis.Warning)

                # Close the connection.
                db.removeDatabase('QPSQL')

            else:
                QMessageBox.information(None, "Error!", str('Unable to open database!'))

        else:
            QMessageBox.information(None, "Error!", str('Invalid database specification!'))

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginDatabaseMenu(
                self.tr(u'&Aderyn Data Search'),
                action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar

    def run(self):
        """Run method that performs all the real work"""
        # show the dialog
        self.dlg.show()
        self.loadVectors() # Load any vector layers.
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            # Let's go!
            self.clearVariables() # Reset everything.
            self.setVariables()
            self.clearGridref() # Clear any grid refs elastic bands.
            self.displayGridref() # If there is a grid ref, display it.
            validateLocation = self.validateLocation()
            validateVariables = self.validateVariables()
            validateCategories = self.validateCategories()
            if validateVariables == True and validateLocation == True and validateCategories == True:

                #Set up progress bar. https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/communicating.html - showing progress??
                progressMessageBar = iface.messageBar().createMessage("Running search...")
                progress = QProgressBar()
                progress.setMaximum(len(self.SearchCategories))
                progress.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
                progressMessageBar.layout().addWidget(progress)
                iface.messageBar().pushWidget(progressMessageBar, Qgis.Info)
                couter = 0

                #Create shapefile of search location.
                QgsMessageLog.logMessage('Saving search location shapefile.', 'Aderyn')
                fileNameShpSearchLocationRaw = self.createShapefileSearchLocation()

                # Get the file name - for adding it to the interface.
                fileNameShpSearchLocation = fileNameShpSearchLocationRaw + '.shp';
                newFileSearchLocation = os.path.join(self.SearchOutputFolder, fileNameShpSearchLocation)

                # Add the shapefile to QGIS'
                layer = iface.addVectorLayer(newFileSearchLocation, 'Search Location', "ogr")

                # Style the layer.
                if not layer:
                    self.iface.messageBar().pushMessage("Warning", 'Failed to load layer into interface (' + newFileSearchLocation + ')!', level=Qgis.Warning)
                else:
                    self.styleShapefileSearchLocation(layer, fileNameShpSearchLocationRaw)

                #Add all the buffers.
                for category in self.SearchCategories:

                    searchCategory = category[0]
                    searchCategoryBuffer = category[1]
                    searchCategoryColour = category[2]
                    searchCategoryFileName = category[3]
                    searchCategoryFullName = category[4]
                    searchCategoryFullNameAbbrev = category[5]

                    QgsMessageLog.logMessage('Looping - display buffer ' + searchCategory + ', buffer ' + str(searchCategoryBuffer) + ', colour ' + searchCategoryColour, 'Aderyn')
                    fileNameShpBufferRaw = self.createShapefileBuffer(category)

                    # Get the file name - for adding it to the interface.
                    fileNameShpBuffer = fileNameShpBufferRaw + '.shp';
                    newFileBuffer = os.path.join(self.SearchOutputFolder, fileNameShpBuffer)

                    # Add the shapefile to QGIS'
                    # layer = iface.addVectorLayer(newFileBuffer, searchCategory + ' Buffer (' + str(searchCategoryBuffer) + 'm)', "ogr")
                    layer = iface.addVectorLayer(newFileBuffer,searchCategoryFullNameAbbrev + ' Buffer (' + str(searchCategoryBuffer) + 'm)', "ogr")

                    # Style the layer.
                    if not layer:
                        self.iface.messageBar().pushMessage("Warning", 'Failed to load layer into interface (' + newFileBuffer + ')!', level=Qgis.Warning)
                    else:
                        self.styleShapefileBuffer(layer, searchCategory, fileNameShpBufferRaw)

                # Zoom the map to the largest buffer.
                self.canvas.setExtent(self.gridSquareMaxBufferBoundingBox)
                self.canvas.refresh()
                # self.canvas.zoomOut()  # Zoom out 1 level - to give a bit of context.

                # Run each search in turn.
                for category in self.SearchCategories:

                    # cat1 = ['CAT1', self.Cat1Buffer, 'red', fileName, 'Priority Species']
                    searchCategory = category[0]
                    searchCategoryBuffer = category[1]
                    searchCategoryColour = category[2]
                    searchCategoryFileName = category[3]
                    searchCategoryFullName = category[4]
                    searchCategoryFullNameAbbrev = category[5]

                    QgsMessageLog.logMessage('Looping - search ' + searchCategory + ', buffer ' + str(searchCategoryBuffer), 'Aderyn')

                    # self.runSearch(searchCategory, str(searchCategoryBuffer), searchCategoryFileName)
                    self.runSearch(category)

                    couter += 1 # Increment.
                    progress.setValue(couter)

                # If CSV was selected, loop through the CSVs and add them to an XSLX file.
                if self.CsvSelect:
                    self.createXlsx()

                iface.messageBar().clearWidgets()

            else:
                self.run()

            # pass

    def runSettings(self):
        """Run method that performs all the real work"""
        self.loadSetings()
        # Load the current settings into the form.
        # self.dlg_settings.le_dbhost.setText(self.dbhost)
        # self.dlg_settings.le_dbuser.setText(self.dbuser)
        # self.dlg_settings.le_dbpassword.setText(self.dbpassword)
        # s = QSettings()
        # dbport_type_int = "int" if type(self.dbport) is int else "str?"
        # dbport_type_str = "str" if type(self.dbport) is str else "int?"
        # QMessageBox.information(None, "Current Settings", str(
        #     'dbhost: ' + s.value("aderyn/dbhost", "") + '\n'
        #     'dbhost: ' + self.dbhost + '\n'
        #     'dbname: ' + s.value("aderyn/dbname", "") + '\n'
        #     'dbname: ' + self.dbname + '\n'
        #     'dbuser: ' + s.value("aderyn/dbuser", "") + '\n'
        #     'dbuser: ' + self.dbuser + '\n'
        #     'dbpassword: ' + s.value("aderyn/dbpassword", "") + '\n'
        #     'dbpassword: ' + self.dbpassword + '\n'
        #     'dbport_type_int: ' + dbport_type_int + '\n'
        #     'dbport_type_str: ' + dbport_type_str + '\n'
        #     'dbport: ' + str(s.value("aderyn/dbport", 0)) + '\n'
        #     'dbport: ' + str(self.dbport) + '\n'
        # ))
        # show the dialog
        self.dlg_settings.show()
        # Run the dialog event loop
        result = self.dlg_settings.exec_()
        # See if OK was pressed
        if result:
            # Do something useful here - delete the line containing pass and
            # substitute with your code.
            self.saveSettings()
            pass
