From 6e62d3d216937d987c6e605aebaddb43dbb2685e Mon Sep 17 00:00:00 2001
From: "benjamin.jakimow@geo.hu-berlin.de" <q8DTkxUg-BB>
Date: Tue, 21 Feb 2017 16:32:32 +0100
Subject: [PATCH] vector overlay using a vector layer controlled in QGIS faster
 TSD loading initialization removed "mask" column in TimeSeriesTable model
 added debugstatus.ui

---
 timeseriesviewer/timeseries.py     | 219 ++++++++++++++++-------------
 timeseriesviewer/ui/debugstatus.ui |  50 +++++++
 2 files changed, 173 insertions(+), 96 deletions(-)
 create mode 100644 timeseriesviewer/ui/debugstatus.ui

diff --git a/timeseriesviewer/timeseries.py b/timeseriesviewer/timeseries.py
index d2f5d795..de38fb07 100644
--- a/timeseriesviewer/timeseries.py
+++ b/timeseriesviewer/timeseries.py
@@ -56,6 +56,13 @@ def verifyVRT(pathVRT):
     return True
 
 
+def getDS(pathOrDataset):
+    if isinstance(pathOrDataset, gdal.Dataset):
+        return pathOrDataset
+    else:
+        ds = gdal.Open(pathOrDataset)
+        assert isinstance(ds, gdal.Dataset)
+        return ds
 
 class SensorInstrument(QObject):
 
@@ -72,41 +79,25 @@ class SensorInstrument(QObject):
     """
     Describes a Sensor Configuration
     """
-    def __init__(self, refLyr, sensor_name=None):
+    def __init__(self, pathImg, sensor_name=None):
         super(SensorInstrument, self).__init__()
-        assert isinstance(refLyr, QgsRasterLayer)
-        assert refLyr.isValid()
-        #QgsMapLayerRegistry.instance().addMapLayer(refLyr)
-        self.nb = refLyr.bandCount()
-        self.bandDataType = refLyr.dataProvider().dataType(1)
-        self.refUri = refLyr.dataProvider().dataSourceUri()
-        r = refLyr.renderer()
 
+        ds = getDS(pathImg)
+        self.nb, nl, ns, crs, self.px_size_x, self.px_size_y = getSpatialPropertiesFromDataset(ds)
 
-        """
-        dom = QDomDocument()
-        root = dom.createElement('root')
-        refLyr.renderer().writeXML(dom, root)
-        dom.appendChild(root)
-        self.renderXML = dom.toString()
-        """
+        self.bandDataType = ds.GetRasterBand(1).DataType
+        self.pathImg = ds.GetFileList()[0]
 
         #todo: better band names
-        self.bandNames = [refLyr.bandName(i) for i in range(1, self.nb + 1)]
-        #self.refLyr = refLyr
+        self.bandNames = [ds.GetRasterBand(b+1).GetDescription() for b in range(self.nb)]
 
         self.TS = None
-        px_size_x = refLyr.rasterUnitsPerPixelX()
-        px_size_y = refLyr.rasterUnitsPerPixelY()
-
-        self.px_size_x = float(abs(px_size_x))
-        self.px_size_y = float(abs(px_size_y))
 
         assert self.px_size_x > 0
         assert self.px_size_y > 0
 
         #find wavelength
-        wl, wlu = parseWavelength(refLyr)
+        wl, wlu = parseWavelengthFromDataSet(ds)
         self.wavelengths = np.asarray(wl)
         self.wavelengthUnits = wlu
 
@@ -187,41 +178,6 @@ class SensorInstrument(QObject):
         return '\n'.join(info)
 
 
-class TSDLoaderSignals(QObject):
-
-    sigRasterLayerLoaded = pyqtSignal(QgsRasterLayer)
-    sigFinished = pyqtSignal()
-
-class TSDLoader(QRunnable):
-    """
-    Runnable to load QgsRasterLayers from a parallel thread
-    """
-    def __init__(self, tsd_paths):
-        super(TSDLoader, self).__init__()
-        self.signals = TSDLoaderSignals()
-
-        self.paths = list()
-        for p in tsd_paths:
-            if not (isinstance(p, tuple) or isinstance(p, list)):
-                p = [p]
-            self.paths.append(p)
-
-
-    def run(self):
-        lyrs = []
-        for path in self.paths:
-            TSD = TimeSeriesDatum(path)
-            lyr = QgsRasterLayer(path)
-            if lyr:
-                lyrs.append(lyr)
-                self.signals.sigRasterLayerLoaded.emit(lyr)
-                dprint('{} loaded'.format(path))
-            else:
-                dprint('Failed to load {}'.format(path))
-        self.signals.sigFinished.emit()
-        #return lyrs
-
-
 def verifyInputImage(path, vrtInspection=''):
 
     if not os.path.exists(path):
@@ -238,6 +194,13 @@ def verifyInputImage(path, vrtInspection=''):
     else:
         return True
 
+def pixel2coord(gt, x, y):
+    """Returns global coordinates from pixel x, y coords"""
+    """https://scriptndebug.wordpress.com/2014/11/24/latitudelongitude-of-each-pixel-using-python-and-gdal/"""
+    xoff, a, b, yoff, d, e = gt
+    xp = a * x + b * y + xoff
+    yp = d * x + e * y + yoff
+    return (xp, yp)
 
 class TimeSeriesDatum(QObject):
     @staticmethod
@@ -263,27 +226,32 @@ class TimeSeriesDatum(QObject):
 
 
 
-    def __init__(self, timeSeries, lyr, pathMsk=None):
+    def __init__(self, timeSeries, pathImg):
         super(TimeSeriesDatum,self).__init__()
-        assert isinstance(lyr, QgsRasterLayer)
-        assert lyr.isValid()
 
-        self.lyrImg = lyr
-        self.timeSeries = timeSeries
+        ds = getDS(pathImg)
+
+        self.pathImg = ds.GetFileList()[0]
 
-        self.pathImg = self.lyrImg.dataProvider().dataSourceUri()
+        self.timeSeries = timeSeries
+        self.nb, self.nl, self.ns, self.crs, px_x, px_y = getSpatialPropertiesFromDataset(ds)
 
-        self.crs = self.lyrImg.dataProvider().crs()
-        self.sensor = SensorInstrument(self.lyrImg)
+        self.sensor = SensorInstrument(ds)
 
-        self.date = getImageDate(self.lyr)
+        self.date = getImageDateFromDataset(ds)
         self.doy = getDOYfromDate(self.date)
         assert self.date is not None, 'Unable to find acquisition date of {}'.format(pathImg)
-        self.ns = self.lyrImg.width()
-        self.nl = self.lyrImg.height()
-        self.nb = self.lyrImg.bandCount()
+
+        gt = ds.GetGeoTransform()
+
+        UL = QgsPoint(*pixel2coord(gt, 0, 0))
+        LR = QgsPoint(*pixel2coord(gt, self.ns, self.nl))
+        from timeseriesviewer.main import SpatialExtent
+        self._spatialExtent = SpatialExtent(self.crs, UL, LR)
+
         self.srs_wkt = str(self.crs.toWkt())
 
+
         self.mVisibility = True
 
 
@@ -308,9 +276,7 @@ class TimeSeriesDatum(QObject):
         return self.crs
 
     def spatialExtent(self):
-        from timeseriesviewer.main import SpatialExtent
-        extent = SpatialExtent(self.lyrImg.crs(), self.lyrImg.extent())
-        return extent
+        return self._spatialExtent
 
     def __repr__(self):
         return 'TS Datum {} {}'.format(self.date, str(self.sensor))
@@ -335,9 +301,10 @@ class TimeSeries(QObject):
 
     sigTimeSeriesDatesAdded = pyqtSignal(list)
     sigTimeSeriesDatesRemoved = pyqtSignal(list)
-
+    sigLoadingProgress = pyqtSignal(int, int, str)
     sigSensorAdded = pyqtSignal(SensorInstrument)
     sigSensorRemoved = pyqtSignal(SensorInstrument)
+    sigRuntimeStats = pyqtSignal(dict)
 
     def __init__(self, imageFiles=None, maskFiles=None):
         QObject.__init__(self)
@@ -447,10 +414,7 @@ class TimeSeries(QObject):
         return tsds
 
     def clear(self):
-        self.Sensors.clear()
-        dates = self.data[:]
-        del self.data[:]
-        self.sigTimeSeriesDatesRemoved.emit(dates)
+        self.removeDates(self[:])
 
 
     def removeDates(self, TSDs):
@@ -462,10 +426,10 @@ class TimeSeries(QObject):
             removed.append(TSD)
 
             S = TSD.sensor
-            #self.Sensors[S].remove(TSD)
+            self.Sensors[S].remove(TSD)
             if len(self.Sensors[S]) == 0:
                 self.Sensors.pop(S)
-                self.sigSensorRemoved(S)
+                self.sigSensorRemoved.emit(S)
 
         self.sigTimeSeriesDatesRemoved.emit(removed)
 
@@ -484,7 +448,7 @@ class TimeSeries(QObject):
                     TSD.sensor = existingSensors[existingSensors.index(TSD.sensor)]
 
                 if TSD in self.data:
-                    six.print_('Time series datum already added: {}'.format(str(TSD)), file=sys.stderr)
+                    six.print_('Time series datum already added: {} {}'.format(str(TSD), TSD.pathImg), file=sys.stderr)
                 else:
                     self.Sensors[TSD.sensor].append(TSD)
                     #insert sorted
@@ -508,12 +472,23 @@ class TimeSeries(QObject):
 
     def addFiles(self, files):
         assert isinstance(files, list)
+        nMax = len(files)
+        nDone = 0
+        self.sigLoadingProgress.emit(0,nMax, 'Start loading {} files...'.format(nMax))
+
         for i, file in enumerate(files):
+            t0 = np.datetime64('now')
             tsd = TimeSeriesDatum.createFromPath(file)
             if tsd is None:
-                dprint('Unable to add: {}'.format(file), file=sys.stderr)
+                msg = 'Unable to add: {}'.format(os.path.basename(file))
+                dprint(msg, file=sys.stderr)
             else:
                 self.addTimeSeriesDates([tsd])
+                msg = 'Added {}'.format(os.path.basename(file))
+                self.sigRuntimeStats.emit({'dt_addTSD':np.datetime64('now')-t0})
+            nDone += 1
+            self.sigLoadingProgress.emit(nDone, nMax, msg)
+
 
     def __len__(self):
         return len(self.data)
@@ -542,23 +517,47 @@ class TimeSeries(QObject):
 regAcqDate = re.compile(r'acquisition[ ]*(time|date|day)', re.I)
 regLandsatSceneID = re.compile(r"L[EMCT][1234578]{1}[12]\d{12}[a-zA-Z]{3}\d{2}")
 
-def getImageDate(lyr):
-    assert isinstance(lyr, QgsRasterLayer)
-    mdLines = str(lyr.metadata()).splitlines()
-    date = None
-    #find date in metadata
-    for line in [l for l in mdLines if regAcqDate.search(l)]:
-        date = parseAcquisitionDate(line)
-        if date:
-            return date
-    #find date in filename
-    dn, fn = os.path.split(str(lyr.dataProvider().dataSourceUri()))
-    date = parseAcquisitionDate(fn)
+def getSpatialPropertiesFromDataset(ds):
+    assert isinstance(ds, gdal.Dataset)
+
+    nb = ds.RasterCount
+    nl = ds.RasterYSize
+    ns = ds.RasterXSize
+    proj = ds.GetGeoTransform()
+    px_x = float(abs(proj[1]))
+    px_y = float(abs(proj[5]))
+
+    crs = QgsCoordinateReferenceSystem(ds.GetProjection())
+
+    return nb, nl, ns, crs, px_x, px_y
+
+def getImageDateFromDataset(ds):
+    assert isinstance(ds, gdal.Dataset)
+
+
+
+    #search metadata for datetime information
+    # see http://www.harrisgeospatial.com/docs/ENVIHeaderFiles.html for datetime format
+
+    regDateKeys = re.compile('(acquisition[ ]*time|date)')
+
+    for domain in ds.GetMetadataDomainList():
+        md = ds.GetMetadata_Dict(domain)
+        for key, value in md.items():
+            if regDateKeys.search(key):
+                return np.datetime64(value)
+
+    #extract from file basename
+    path = ds.GetFileList()[0]
+    bn = os.path.basename(path)
+    path_dir = os.path.dirname(path)
+
+    #search in basename
+    date = parseAcquisitionDate(bn)
     if date: return date
 
     #find date in file directory path
-    date = parseAcquisitionDate(date)
-
+    date = parseAcquisitionDate(path_dir)
     return date
 
 
@@ -567,6 +566,34 @@ regYYYYDOY = re.compile(r'(19|20)\d{5}')
 regYYYYMMDD = re.compile(r'(19|20)\d{2}-\d{2}-\d{2}')
 regYYYY = re.compile(r'(19|20)\d{2}')
 
+def parseWavelengthFromDataSet(ds):
+    assert isinstance(ds, gdal.Dataset)
+    wl = None
+    wlu = None
+
+    #see http://www.harrisgeospatial.com/docs/ENVIHeaderFiles.html for supported wavelength units
+    regWLkey = re.compile('.*wavelength[_ ]*$')
+    regWLUkey = re.compile('.*wavelength[_ ]*units?$')
+    regNumeric = re.compile(r"([-+]?\d*\.\d+|[-+]?\d+)")
+    regWLU = re.compile('((micro|nano|centi)meters)|(um|nm|mm|cm|m|GHz|MHz)')
+    for domain in ds.GetMetadataDomainList():
+        md = ds.GetMetadata_Dict(domain)
+        for key, value in md.items():
+            if wl is None and regWLkey.search(key):
+                numbers = regNumeric.findall(value)
+                if len(numbers) == ds.RasterCount:
+                    wl = [float(n) for n in numbers]
+
+            if wlu is None and regWLUkey.search(key):
+                match = regWLU.search(value)
+                if match:
+                    wlu = match.group()
+                names = ['nanometers', 'micrometers', 'millimeters', 'centimeters', 'decimeters']
+                si = ['nm', 'um', 'mm', 'cm', 'dm']
+                if wlu in names:
+                    wlu = si[names.index(wlu)]
+
+    return wl, wlu
 
 
 
diff --git a/timeseriesviewer/ui/debugstatus.ui b/timeseriesviewer/ui/debugstatus.ui
new file mode 100644
index 00000000..c7b47b3d
--- /dev/null
+++ b/timeseriesviewer/ui/debugstatus.ui
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DockWidget</class>
+ <widget class="QDockWidget" name="DockWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>313</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>DockWidget</string>
+  </property>
+  <widget class="QFrame" name="dockWidgetContents2">
+   <property name="frameShape">
+    <enum>QFrame::NoFrame</enum>
+   </property>
+   <property name="frameShadow">
+    <enum>QFrame::Sunken</enum>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout">
+    <item>
+     <widget class="QGroupBox" name="groupBox">
+      <property name="title">
+       <string>MapLayerRegistry</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QGroupBox" name="groupBox_2">
+      <property name="title">
+       <string>Loading times</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QGroupBox" name="groupBox_3">
+      <property name="title">
+       <string>QGIS MapCanvas</string>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
-- 
GitLab