From 665b7355f1e864ca9ea3e48467bb30165f040200 Mon Sep 17 00:00:00 2001
From: "Benjamin Jakimow benjamin.jakimow@geo.hu-berlin.de"
 <benjamin.jakimow@geo.hu-berlin.de>
Date: Fri, 17 Jan 2020 13:43:17 +0100
Subject: [PATCH] pulled qps updates timeseries.py TimeSeriesSource changed
 staticmethods to classmethods which allows to inherit the TimeSeriesSource
 object

Signed-off-by: Benjamin Jakimow benjamin.jakimow@geo.hu-berlin.de <benjamin.jakimow@geo.hu-berlin.de>
---
 doc/source/installation.rst                   |   2 +-
 eotimeseriesviewer/dateparser.py              |  13 +-
 eotimeseriesviewer/externals/qps/__init__.py  |   3 +-
 .../externals/qps/make/deploy.py              |  94 +++++++
 .../externals/qps/qpsresources.qrc            |   1 +
 eotimeseriesviewer/externals/qps/testing.py   |   6 +-
 .../qps/ui/icons/raster_flagimage.svg         | 241 ++++++++++++++++++
 eotimeseriesviewer/timeseries.py              |  62 ++++-
 tests/test_main.py                            |   3 +-
 9 files changed, 410 insertions(+), 15 deletions(-)
 create mode 100644 eotimeseriesviewer/externals/qps/ui/icons/raster_flagimage.svg

diff --git a/doc/source/installation.rst b/doc/source/installation.rst
index ad55873b..2d15b107 100644
--- a/doc/source/installation.rst
+++ b/doc/source/installation.rst
@@ -5,7 +5,7 @@ Installation
 ============
 
 
-.. important:: The EO TSV plugin requires `QGIS Version 3.4 or higher <https://www.qgis.org/en/site/forusers/download.html>`_
+.. important:: The EO TSV plugin requires `QGIS Version 3.10 or higher <https://www.qgis.org/en/site/forusers/download.html>`_
 
 
 
diff --git a/eotimeseriesviewer/dateparser.py b/eotimeseriesviewer/dateparser.py
index 89c770f6..fca6c2ae 100644
--- a/eotimeseriesviewer/dateparser.py
+++ b/eotimeseriesviewer/dateparser.py
@@ -31,13 +31,20 @@ def matchOrNone(regex, text):
     else:
         return None
 
-def dateDOY(date):
+def dateDOY(date:datetime.date)->int:
+    """
+    Returns the DOY
+    :param date:
+    :type date:
+    :return:
+    :rtype:
+    """
     if isinstance(date, np.datetime64):
         date = date.astype(datetime.date)
     return date.timetuple().tm_yday
 
-def daysPerYear(year):
-
+def daysPerYear(year)->int:
+    """Returns the days per year"""
     if isinstance(year, np.datetime64):
         year = year.astype(datetime.date)
     if isinstance(year, datetime.date):
diff --git a/eotimeseriesviewer/externals/qps/__init__.py b/eotimeseriesviewer/externals/qps/__init__.py
index d87ea698..b2a7713a 100644
--- a/eotimeseriesviewer/externals/qps/__init__.py
+++ b/eotimeseriesviewer/externals/qps/__init__.py
@@ -1,6 +1,7 @@
 import sys, importlib, site, os
 from qgis.core import QgsApplication
-
+from qgis.gui import QgisInterface
+__version__ = '0.2'
 
 def initResources():
     """
diff --git a/eotimeseriesviewer/externals/qps/make/deploy.py b/eotimeseriesviewer/externals/qps/make/deploy.py
index 245357a2..8e100a34 100644
--- a/eotimeseriesviewer/externals/qps/make/deploy.py
+++ b/eotimeseriesviewer/externals/qps/make/deploy.py
@@ -50,6 +50,100 @@ URL_WIKI = r'https://api.bitbucket.org/2.0/repositories/hu-geomatics/enmap-box/w
 
 
 
+class QGISMetadataFileWriter(object):
+
+    def __init__(self):
+        self.mName = None
+
+        self.mDescription = None
+        self.mVersion = None
+        self.mQgisMinimumVersion = '3.8'
+        self.mQgisMaximumVersion = '3.99'
+        self.mAuthor = None
+        self.mAbout = None
+        self.mEmail = None
+        self.mHomepage = None
+        self.mIcon = None
+        self.mTracker = None
+        self.mRepository = None
+        self.mIsExperimental = False
+        self.mTags = None
+        self.mCategory = None
+        self.mChangelog = ''
+
+    def validate(self)->bool:
+
+        return True
+
+    def metadataString(self)->str:
+        assert self.validate()
+
+        lines = ['[general]']
+        lines.append('name={}'.format(self.mName))
+        lines.append('author={}'.format(self.mAuthor))
+        if self.mEmail:
+            lines.append('email={}'.format(self.mEmail))
+
+        lines.append('description={}'.format(self.mDescription))
+        lines.append('version={}'.format(self.mVersion))
+        lines.append('qgisMinimumVersion={}'.format(self.mQgisMinimumVersion))
+        lines.append('qgisMaximumVersion={}'.format(self.mQgisMaximumVersion))
+        lines.append('about={}'.format(re.sub('\n', '', self.mAbout)))
+
+        lines.append('icon={}'.format(self.mIcon))
+
+        lines.append('tags={}'.format(', '.join(self.mTags)))
+        lines.append('category={}'.format(self.mRepository))
+
+        lines.append('homepage={}'.format(self.mHomepage))
+        if self.mTracker:
+            lines.append('tracker={}'.format(self.mTracker))
+        if self.mRepository:
+            lines.append('repository={}'.format(self.mRepository))
+        if isinstance(self.mIsExperimental, bool):
+            lines.append('experimental={}'.format(self.mIsExperimental))
+
+
+        #lines.append('deprecated={}'.format(self.mIsDeprecated))
+        lines.append('')
+        lines.append('changelog={}'.format(self.mChangelog))
+
+        return '\n'.join(lines)
+    """
+    [general]
+    name=dummy
+    description=dummy
+    version=dummy
+    qgisMinimumVersion=dummy
+    qgisMaximumVersion=dummy
+    author=dummy
+    about=dummy
+    email=dummy
+    icon=dummy
+    homepage=dummy
+    tracker=dummy
+    repository=dummy
+    experimental=False
+    deprecated=False
+    tags=remote sensing, raster, time series, data cube, landsat, sentinel
+    category=Raster
+    """
+
+    def writeMetadataTxt(self, path:str):
+        with open(path, 'w', encoding='utf-8') as f:
+            f.write(self.metadataString())
+        # read again and run checks
+        import pyplugin_installer.installer_data
+
+        # test if we could read the plugin
+        import pyplugin_installer.installer_data
+        P = pyplugin_installer.installer_data.Plugins()
+        plugin = P.getInstalledPlugin(self.mName, os.path.dirname(path), True)
+
+        #if hasattr(pyplugin_installer.installer_data, 'errorDetails'):
+        #    raise Exception('plugin structure/metadata error:\n{}'.format(pyplugin_installer.installer_data.errorDetails))
+        s = ""
+
 
 
 def buildId()->str:
diff --git a/eotimeseriesviewer/externals/qps/qpsresources.qrc b/eotimeseriesviewer/externals/qps/qpsresources.qrc
index 922bb104..1a39abfe 100644
--- a/eotimeseriesviewer/externals/qps/qpsresources.qrc
+++ b/eotimeseriesviewer/externals/qps/qpsresources.qrc
@@ -1,5 +1,6 @@
 <RCC>
   <qresource prefix="qps">
+    <file>ui/icons/raster_flagimage.svg</file>
     <file>ui/icons/poly2speclib.svg</file>
     <file>ui/icons/speclib_add.svg</file>
     <file>ui/icons/speclib_clear.svg</file>
diff --git a/eotimeseriesviewer/externals/qps/testing.py b/eotimeseriesviewer/externals/qps/testing.py
index c8c0ce69..baa39cb3 100644
--- a/eotimeseriesviewer/externals/qps/testing.py
+++ b/eotimeseriesviewer/externals/qps/testing.py
@@ -185,6 +185,9 @@ def initQgisApplication(*args, qgisResourceDir: str = None,
             qgsApp = qgis.testing.start_app()
             if not QgsProviderRegistry.instance().libraryDirectory().exists():
                 QgsProviderRegistry.instance().setLibraryDirectory(QDir(QApplication.instance().libraryPaths()[0]))
+                s = ""
+
+            qgsApp.setPkgDataPath(re.sub(r'(/envs/[^/]+)/\.$', r'\1/Library', qgsApp.pkgDataPath()))
 
         elif QOperatingSystemVersion.current().type() == QOperatingSystemVersion.Unknown:
 
@@ -407,12 +410,13 @@ class QgisMockup(QgisInterface):
     def layerTreeView(self) -> QgsLayerTreeView:
         return self.mLayerTreeView
 
-    def addRasterLayer(self, path, baseName=''):
+    def addRasterLayer(self, path, baseName:str='')->QgsRasterLayer:
         l = QgsRasterLayer(path, os.path.basename(path))
         self.lyrs.append(l)
         QgsProject.instance().addMapLayer(l, True)
         self.mRootNode.addLayer(l)
         # self.mCanvas.setLayers(self.mCanvas.layers() + l)
+        return l
 
     def createActions(self):
         m = self.ui.menuBar().addAction('Add Vector')
diff --git a/eotimeseriesviewer/externals/qps/ui/icons/raster_flagimage.svg b/eotimeseriesviewer/externals/qps/ui/icons/raster_flagimage.svg
new file mode 100644
index 00000000..363b707b
--- /dev/null
+++ b/eotimeseriesviewer/externals/qps/ui/icons/raster_flagimage.svg
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="svg2"
+   viewBox="0 0 120 120"
+   height="128"
+   width="128"
+   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
+   sodipodi:docname="raster_flagimage.svg"
+   inkscape:export-filename="C:\Users\geo_beja\Desktop\flag_renderer.png"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96">
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1137"
+     id="namedview6021"
+     showgrid="false"
+     inkscape:zoom="4"
+     inkscape:cx="17.923393"
+     inkscape:cy="99.828371"
+     inkscape:window-x="1912"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g4936"
+     units="px" />
+  <defs
+     id="defs4">
+    <inkscape:path-effect
+       effect="simplify"
+       id="path-effect5115"
+       is_visible="true"
+       steps="1"
+       threshold="0.000408163"
+       smooth_angles="360"
+       helper_size="0"
+       simplify_individual_paths="false"
+       simplify_just_coalesce="false"
+       simplifyindividualpaths="false"
+       simplifyJustCoalesce="false" />
+    <inkscape:path-effect
+       effect="simplify"
+       id="path-effect5111"
+       is_visible="true"
+       steps="1"
+       threshold="0.000408163"
+       smooth_angles="360"
+       helper_size="0"
+       simplify_individual_paths="false"
+       simplify_just_coalesce="false"
+       simplifyindividualpaths="false"
+       simplifyJustCoalesce="false" />
+    <inkscape:path-effect
+       effect="simplify"
+       id="path-effect5107"
+       is_visible="true"
+       steps="1"
+       threshold="0.000408163"
+       smooth_angles="360"
+       helper_size="0"
+       simplify_individual_paths="false"
+       simplify_just_coalesce="false"
+       simplifyindividualpaths="false"
+       simplifyJustCoalesce="false" />
+    <inkscape:path-effect
+       effect="simplify"
+       id="path-effect5103"
+       is_visible="true"
+       steps="1"
+       threshold="0.000408163"
+       smooth_angles="360"
+       helper_size="0"
+       simplify_individual_paths="false"
+       simplify_just_coalesce="false"
+       simplifyindividualpaths="false"
+       simplifyJustCoalesce="false" />
+    <inkscape:path-effect
+       effect="spiro"
+       id="path-effect5099"
+       is_visible="true" />
+    <inkscape:path-effect
+       effect="spiro"
+       id="path-effect5093"
+       is_visible="true" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect5089"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect5085"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect5081"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <inkscape:path-effect
+       effect="spiro"
+       id="path-effect4553"
+       is_visible="true" />
+    <inkscape:path-effect
+       effect="bspline"
+       id="path-effect4531"
+       is_visible="true"
+       weight="33.333333"
+       steps="2"
+       helper_size="0"
+       apply_no_weight="true"
+       apply_with_weight="true"
+       only_selected="false" />
+    <linearGradient
+       id="a">
+      <stop
+         id="stop4878"
+         offset="0"
+         stop-color="#aec7e2" />
+      <stop
+         id="stop4880"
+         offset="1"
+         stop-color="#6e97c4" />
+    </linearGradient>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath6909">
+      <path
+         id="path6911"
+         d="m 1.9298437,3.4084573 c 6.5392607,4.2773092 6.382906,-4.32324781 13.2315343,-7e-7 v 9.9001064 c -5.9513246,-6.1251931 -6.6101075,4.332478 -13.2315344,0 z"
+         inkscape:connector-curvature="0"
+         style="display:inline;fill:#00ff00;fill-opacity:1;stroke-width:2.64110875"
+         sodipodi:nodetypes="ccccc" />
+    </clipPath>
+  </defs>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="g1423"
+     transform="translate(-0.29108467,-0.14214042)">
+    <g
+       id="g6444"
+       transform="matrix(88.9034,-82.097222,82.126829,88.871349,-87385.716,-92384.398)">
+      <path
+         style="display:inline;fill:none;stroke:#2b3b4d;stroke-width:0.05750267;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 12.295807,1050.9981 -0.567748,0.6145"
+         id="path4559"
+         inkscape:connector-curvature="0" />
+      <g
+         id="g4936"
+         transform="matrix(0.03764367,0.03477435,-0.03478686,0.03765724,12.32214,1050.8888)">
+        <ellipse
+           style="opacity:1;fill:#2b3b4d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.11532462;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+           id="path5077"
+           cx="1.0853573"
+           cy="18.380617"
+           rx="1.1546977"
+           ry="0.89853734" />
+        <g
+           id="g6907"
+           clip-path="url(#clipPath6909)"
+           transform="translate(-8.1726307e-8,-1.2091792)">
+          <path
+             style="display:inline;fill:#aec7e2"
+             inkscape:connector-curvature="0"
+             d="m 6.333,2 h 4.334 V 6.333 H 6.333 Z"
+             id="path4918" />
+          <path
+             style="display:inline;fill:#2b3b4d"
+             inkscape:connector-curvature="0"
+             d="M 2,2 H 6.333 V 6.333 H 2 Z m 4.333,4.333 h 4.334 v 4.334 H 6.333 Z M 10.667,2 H 15 v 4.333 h -4.333 z"
+             id="path4920" />
+          <path
+             style="display:inline;fill:#aec7e2;stroke-width:1"
+             inkscape:connector-curvature="0"
+             d="m 10.667,6.333 h 4.334 v 4.333 h -4.334 z"
+             id="path4918-8" />
+          <path
+             style="display:inline;fill:#aec7e2;stroke-width:1"
+             inkscape:connector-curvature="0"
+             d="m 2,6.333 h 4.3339999 v 4.333 H 2 Z"
+             id="path4918-7" />
+          <path
+             style="display:inline;fill:#aec7e2;stroke-width:1"
+             inkscape:connector-curvature="0"
+             d="m 6.333,10.666 h 4.334 v 4.333 H 6.333 Z"
+             id="path4918-7-1" />
+          <path
+             style="display:inline;fill:#2b3b4d;fill-opacity:1;stroke-width:1"
+             inkscape:connector-curvature="0"
+             d="m 10.667,10.667 h 4.334 V 15 h -4.334 z"
+             id="path4918-7-8" />
+          <path
+             style="display:inline;fill:#2b3b4d;fill-opacity:1;stroke-width:1"
+             inkscape:connector-curvature="0"
+             d="M 1.9989997,10.666 H 6.333 v 4.333 H 1.9989997 Z"
+             id="path4918-7-3" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/eotimeseriesviewer/timeseries.py b/eotimeseriesviewer/timeseries.py
index b37f372c..f5b8e352 100644
--- a/eotimeseriesviewer/timeseries.py
+++ b/eotimeseriesviewer/timeseries.py
@@ -33,6 +33,8 @@ from qgis.PyQt.QtGui import *
 from qgis.PyQt.QtWidgets import *
 from qgis.PyQt.QtCore import *
 
+from osgeo import osr, ogr, gdal, gdal_array
+
 DEFAULT_WKT = QgsCoordinateReferenceSystem('EPSG:4326').toWkt()
 
 LUT_WAVELENGTH_UNITS = {}
@@ -386,20 +388,20 @@ def verifyInputImage(datasource):
 class TimeSeriesSource(object):
     """Provides some information on source images"""
 
-    @staticmethod
-    def fromJson(jsonData:str):
+    @classmethod
+    def fromJson(cls, jsonData:str):
         """
         Returs a TimeSeriesSource from its JSON representation
         :param json:
         :return:
         """
-        source = TimeSeriesSource(None)
+        source = cls(None)
         state = json.loads(jsonData)
         source.__setstatedictionary(state)
         return source
 
-    @staticmethod
-    def create(source):
+    @classmethod
+    def create(cls, source):
         """
         Reads the argument and returns a TimeSeriesSource
         :param source: gdal.Dataset, str, QgsRasterLayer
@@ -441,7 +443,11 @@ class TimeSeriesSource(object):
         if not isinstance(ds, gdal.Dataset):
             raise Exception('Unsupported source: {}'.format(source))
 
-        return TimeSeriesSource(ds)
+        srs = osr.SpatialReference()
+        assert srs.ImportFromWkt(ds.GetProjection()) == ogr.OGRERR_NONE, 'Can not read spatial reference from {}'.format(ds.GetDescription())
+
+
+        return cls(ds)
 
     def __init__(self, dataset:gdal.Dataset=None):
 
@@ -543,7 +549,9 @@ class TimeSeriesSource(object):
         for k, v in state.items():
             self.__dict__[k] = v
         self.mCRS = QgsCoordinateReferenceSystem(self.mWKT)
-        assert self.mCRS.isValid()
+        if not self.mCRS.isValid():
+            srs = osr.SpatialReference()
+            assert srs.ImportFromWkt(self.mCRS.toWkt()) == ogr.OGRERR_NONE, 'Unable to import spatial reference of {}'.format(self.mUri)
         self.mUL = QgsPointXY(QgsGeometry.fromWkt(self.mUL).asPoint())
         self.mLR = QgsPointXY(QgsGeometry.fromWkt(self.mLR).asPoint())
         self.mDate = np.datetime64(self.mDate)
@@ -636,21 +644,59 @@ class TimeSeriesSource(object):
         self.mTimeSeriesDate = tsd
 
     def date(self)->np.datetime64:
+        """
+        Returns the date-time-group of the source image
+        :return:
+        :rtype:
+        """
         return self.mDate
 
     def crs(self)->QgsCoordinateReferenceSystem:
+        """
+        Returns the coordinate system as QgsCoordinateReferenceSystem
+        :return:
+        :rtype:
+        """
         return self.mCRS
 
     def spatialExtent(self)->SpatialExtent:
+        """
+        Returns the SpatialExtent
+        :return:
+        :rtype:
+        """
         if not isinstance(self.mSpatialExtent, SpatialExtent):
             self.mSpatialExtent = SpatialExtent(self.mCRS, self.mUL, self.mLR)
         return self.mSpatialExtent
 
+    def asDataset(self)->gdal.Dataset:
+        """
+        Returns the source as gdal.Dataset
+        :return:
+        :rtype:
+        """
+        return gdal.Open(self.uri())
+
+    def asArray(self)->np.ndarray:
+        """
+        Returns the entire image as numpy array
+        :return:
+        :rtype:
+        """
+        return gdal_array.LoadFile(self.uri())
+
     def __eq__(self, other):
         if not isinstance(other, TimeSeriesSource):
             return False
         return self.mUri == other.mUri
 
+    def __lt__(self, other):
+        assert isinstance(other, TimeSeriesSource)
+        return self.date() < other.date()
+
+    def __hash__(self):
+        return hash(self.mUri)
+
 
 class TimeSeriesDate(QAbstractTableModel):
     """
@@ -1509,7 +1555,7 @@ class TimeSeries(QAbstractItemModel):
             self.checkSensorList()
             self.sigTimeSeriesDatesRemoved.emit(removed)
 
-    def tsds(self, date:np.datetime64=None, sensor:SensorInstrument=None)->list:
+    def tsds(self, date:np.datetime64=None, sensor:SensorInstrument=None)->typing.List[TimeSeriesDate]:
 
         """
         Returns a list of  TimeSeriesDate of the TimeSeries. By default all TimeSeriesDate will be returned.
diff --git a/tests/test_main.py b/tests/test_main.py
index 19005f7c..5725854c 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -32,8 +32,9 @@ import eotimeseriesviewer
 eotimeseriesviewer.initResources()
 from eotimeseriesviewer.mapcanvas import *
 from eotimeseriesviewer.tests import TestObjects
-
+from qgis.testing import start_app
 QGIS_APP = initQgisApplication()
+
 SHOW_GUI = True and os.environ.get('CI') is None
 
 
-- 
GitLab