From cca6c310993a70c8dbba8b47fb2edb4411038c77 Mon Sep 17 00:00:00 2001
From: "benjamin.jakimow" <benjamin.jakimow@geo.hu-berlin.de>
Date: Tue, 16 Feb 2016 16:02:03 +0100
Subject: [PATCH] different changes related to switch from date to TSD new
 capabilities: save and load text file based definitions of time series data
 files supports multiple sensors in a time series

---
 sensecarbon_tsv.py | 309 +++++++++++++++++++++++++++++++++------------
 1 file changed, 226 insertions(+), 83 deletions(-)

diff --git a/sensecarbon_tsv.py b/sensecarbon_tsv.py
index 33921586..ad2127b3 100644
--- a/sensecarbon_tsv.py
+++ b/sensecarbon_tsv.py
@@ -85,7 +85,7 @@ def file_search(rootdir, wildcard, recursive=False, ignoreCase=False):
 
 
 class TimeSeriesTableModel(QAbstractTableModel):
-    columnames = ['date','name','ns','nl','nb','image','mask']
+    columnames = ['date','sensor','ns','nl','nb','image','mask']
 
     def __init__(self, TS, parent=None, *args):
         super(QAbstractTableModel, self).__init__()
@@ -137,6 +137,11 @@ class TimeSeriesTableModel(QAbstractTableModel):
         if role == Qt.DisplayRole or role == Qt.ToolTipRole:
             if ic_name == 'name':
                 value = os.path.basename(TSD.pathImg)
+            elif ic_name == 'sensor':
+                if role == Qt.ToolTipRole:
+                    value = TSD.sensor.getDescription()
+                else:
+                    value = str(TSD.sensor)
             elif ic_name == 'date':
                 value = '{}'.format(TSD.date)
             elif ic_name == 'image':
@@ -280,7 +285,7 @@ class BandView(object):
         return self.bandMappings[sensor]
 
     def getBands(self, sensor):
-        return self.getWidget(sensor).Bands()
+        return self.getWidget(sensor).getBands()
 
 
     def useMaskValues(self):
@@ -339,13 +344,23 @@ class SensorConfiguration(object):
     def __repr__(self):
         return self.sensor_name
 
+    def getDescription(self):
+        info = []
+        info.append(self.sensor_name)
+        info.append('{} Bands'.format(self.nb))
+        info.append('Band\tName\tWavelength')
+        for b in range(self.nb):
+            if self.wavelengths:
+                wl = str(self.wavelengths[b])
+            else:
+                wl = 'unknown'
+            info.append('{}\t{}\t{}'.format(b+1, self.band_names[b],wl))
 
+        return '\n'.join(info)
 
-class TimeSeries(QObject):
 
-    #define signals
 
-    #fire when a new TSD is added
+class TimeSeries(QObject):
     datumAdded = pyqtSignal(name='datumAdded')
 
     #fire when a new sensor configuration is added
@@ -358,16 +373,21 @@ class TimeSeries(QObject):
     closed = pyqtSignal()
     error = pyqtSignal(object)
 
-    data = collections.OrderedDict()
+    def __init__(self, imageFiles=None, maskFiles=None):
+        QObject.__init__(self)
 
-    CHIP_BUFFER=dict()
+        #define signals
 
-    shape = None
+        #fire when a new TSD is added
 
-    Sensors = collections.OrderedDict()
 
-    def __init__(self, imageFiles=None, maskFiles=None):
-        QObject.__init__(self)
+        self.data = collections.OrderedDict()
+
+        self.CHIP_BUFFER=dict()
+
+        self.shape = None
+
+        self.Sensors = collections.OrderedDict()
 
         self.Pool = None
 
@@ -376,6 +396,52 @@ class TimeSeries(QObject):
         if maskFiles is not None:
             self.addMasks(maskFiles)
 
+    _sep = ';'
+
+    @staticmethod
+    def loadFromFile(path):
+
+        images = []
+        masks = []
+        with open(path, 'r') as f:
+            lines = f.readlines()
+            for l in lines:
+                if re.match('^[ ]*[;#&]', l):
+                    continue
+
+                parts = re.split('[\n'+TimeSeries._sep+']', l)
+                parts = [p for p in parts if p != '']
+                images.append(parts[0])
+                if len(parts) > 1:
+                    masks.append(parts[1])
+
+        TS = TimeSeries()
+        TS.addFiles(images)
+        TS.addMasks(masks)
+
+        return TS
+
+
+    def saveToFile(self, path):
+        import time
+        lines = []
+        lines.append('#Time series definition file: {}'.format(np.datetime64('now').astype(str)))
+        lines.append('#<image path>[;<mask path>]')
+        for TSD in self.data.values():
+
+            line = TSD.pathImg
+            if TSD.pathMsk is not None:
+                line += TimeSeries._sep + TSD.pathMsk
+
+            lines.append(line)
+
+        lines = [l+'\n' for l in lines]
+
+        print('Write {}'.format(path))
+        with open(path, 'w') as f:
+            f.writelines(lines)
+
+
     def getSRS(self):
         if len(self.data) == 0:
             return 0
@@ -387,6 +453,23 @@ class TimeSeries(QObject):
         srs = self.getSRS()
         return srs.ExportToWkt()
 
+    def getSceneCenter(self, srs=None):
+
+        if srs is None:
+            srs = self.getSRS()
+
+        bbs = list()
+        for S, TSDs in self.Sensors.items():
+            x = []
+            y = []
+            for TSD in TSDs:
+                bb = TSD.getBoundingBox(srs)
+                x.extend([c[0] for c in bb])
+                y.extend([c[1] for c in bb])
+
+        return None
+        pass
+
     def getMaxExtent(self, srs=None):
 
         x = []
@@ -477,7 +560,6 @@ class TimeSeries(QObject):
     def addMasks(self, files, raise_errors=True, mask_value=0, exclude_mask_value=True):
         assert isinstance(files, list)
         l = len(files)
-        assert l > 0
 
         self.progress.emit(0,0,l)
         for i, file in enumerate(files):
@@ -490,7 +572,7 @@ class TimeSeries(QObject):
             self.progress.emit(0,i+1,l)
 
         self.progress.emit(0,0,l)
-        self.changed()
+        self.changed.emit()
 
     def addMask(self, pathMsk, raise_errors=True, mask_value=0, exclude_mask_value=True, _quiet=False):
         print('Add mask {}...'.format(pathMsk))
@@ -516,26 +598,23 @@ class TimeSeries(QObject):
         self.clear()
 
     def clear(self):
-        dates = list(self.data.keys())
-        for date in dates:
-            self.removeDate(date, _quiet=True)
+        self.Sensors.clear()
+        self.data.clear()
         self.changed.emit()
 
 
-    def removeDates(self, dates):
-        for date in dates:
-            self.removeDate(date, _quiet=True)
+    def removeDates(self, TSDs):
+        for TSD in TSDs:
+            self.removeTSD(TSD, _quiet=True)
         self.changed.emit()
 
-    def removeDate(self, date, _quiet=False):
+    def removeTSD(self, TSD, _quiet=False):
 
-        assert type(date) is np.datetime64
+        assert type(TSD) is TimeSeriesDatum
+        S = TSD.sensor
+        self.Sensors[S].remove(TSD)
+        self.data.pop(TSD, None)
 
-        self.data.pop(date, None)
-        if len(self.data) == 0:
-            self.nb = None
-            self.bandnames = None
-            self.srs = None
         if not _quiet:
             self.changed.emit()
 
@@ -589,6 +668,7 @@ class TimeSeries(QObject):
         self._sortTimeSeriesData()
         self.progress.emit(0,0,l)
         self.datumAdded.emit()
+        self.changed.emit()
 
 
 
@@ -628,6 +708,19 @@ def coordinate2px(gt, cx, cy):
     py = int((cy - gt[3]) / gt[5])
     return px, py
 
+
+def getBoundingBoxPolygon(points, srs=None):
+    ring = ogr.Geometry(ogr.wkbLinearRing)
+    for point in points:
+        ring.AddPoint(point[0], point[1])
+    bb = ogr.Geometry(ogr.wkbPolygon)
+    bb.AddGeometry(ring)
+    if srs:
+        bb.AssignSpatialReference(srs)
+    return bb
+
+
+
 def getDS(ds):
     if type(ds) is not gdal.Dataset:
         ds = gdal.Open(ds)
@@ -729,9 +822,9 @@ class TimeSeriesDatum(object):
             my_srs = self.getSpatialReference()
             if not my_srs.IsSame(srs):
                 #todo: consider srs differences
-
-                raise Exception('differences SRS in bounding box request')
-                pass
+                trans = osr.CoordinateTransformation(my_srs, srs)
+                ext = trans.TransformPoints(ext)
+                ext = [(e[0], e[1]) for e in ext]
 
         return ext
 
@@ -780,8 +873,8 @@ class TimeSeriesDatum(object):
 
 
         if type(geometry) is ogr.Geometry:
-            g = geometry
-            srs_bb = g.GetSpatialReference()
+            g_bb = geometry
+            srs_bb = g_bb.GetSpatialReference()
         else:
             assert srs is not None and type(srs) in [str, osr.SpatialReference]
             if type(srs) is str:
@@ -789,15 +882,16 @@ class TimeSeriesDatum(object):
                 srs_bb.ImportFromWkt(srs)
             else:
                 srs_bb = srs.Clone()
-            g = ogr.CreateGeometryFromWkt(geometry, srs_bb)
+            g_bb = ogr.CreateGeometryFromWkt(geometry, srs_bb)
 
-        assert srs_bb is not None and g is not None
-        assert g.GetGeometryName() == 'POLYGON'
+        assert srs_bb is not None and g_bb is not None
+        assert g_bb.GetGeometryName() == 'POLYGON'
 
         if not srs_img.IsSame(srs_bb):
-            g = g.Clone()
-            g.TransformTo(srs_img)
-        cx0,cx1,cy0,cy1 = g.GetEnvelope()
+            g_bb = g_bb.Clone()
+            g_bb.TransformTo(srs_img)
+
+        cx0,cx1,cy0,cy1 = g_bb.GetEnvelope()
 
         ul_px = coordinate2px(self.gt, min([cx0, cx1]), max([cy0, cy1]))
         lr_px = coordinate2px(self.gt, max([cx0, cx1]), min([cy0, cy1]))
@@ -814,7 +908,8 @@ class TimeSeriesDatum(object):
 
         ns = px_x[1]-px_x[0]+1
         nl = px_y[1]-px_y[0]+1
-
+        assert ns >= 0
+        assert nl >= 0
 
         src_ns = ds.RasterXSize
         src_nl = ds.RasterYSize
@@ -850,7 +945,7 @@ class TimeSeriesDatum(object):
         templateMsk = np.ones((nl,ns), dtype='bool')
 
         if win_xsize < 1 or win_ysize < 1:
-            six.print_('Selected image chip is out of raster image', file=sys.stderr)
+            six.print_('Selected image chip is out of raster image {}'.format(self.pathImg), file=sys.stderr)
             for i, b in enumerate(bands):
                 chipdata[b] = np.copy(templateImg)
 
@@ -997,12 +1092,11 @@ class VerticalLabel(QLabel):
 
 class ImageChipBuffer(object):
 
-    data = dict()
-    BBox = None
-    SRS = None
 
     def __init__(self):
-
+        self.data = dict()
+        self.BBox = None
+        self.SRS = None
         pass
 
 
@@ -1013,7 +1107,7 @@ class ImageChipBuffer(object):
 
         missing = set(bands)
         if TSD in self.data.keys():
-            missing = missing - set(self.data[id].keys())
+            missing = missing - set(self.data[TSD].keys())
         return missing
 
     def addDataCube(self, TSD, chipData):
@@ -1121,17 +1215,10 @@ class SenseCarbon_TSV:
         # Create the dialog (after translation) and keep reference
         self.dlg = SenseCarbon_TSVGui()
         D = self.dlg
-        self.TS = TimeSeries()
-        TSM = TimeSeriesTableModel(self.TS)
-        D.tableView_TimeSeries.setModel(TSM)
-        D.tableView_TimeSeries.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents)
-        D.cb_centerdate.setModel(TSM)
-        D.cb_centerdate.setModelColumn(0)
-        D.cb_centerdate.currentIndexChanged.connect(self.scrollToDate)
 
-        self.TS.datumAdded.connect(self.ua_datumAdded)
-        self.TS.progress.connect(self.ua_TSprogress)
-        self.TS.chipLoaded.connect(self.ua_showPxCoordinate_addChips)
+        #init on empty time series
+        self.TS = None
+        self.init_TimeSeries()
 
         self.BAND_VIEWS = list()
         self.ImageChipBuffer = ImageChipBuffer()
@@ -1148,6 +1235,9 @@ class SenseCarbon_TSV:
         D.btn_removeTSD.clicked.connect(lambda : self.ua_removeTSD(None))
         D.btn_removeTS.clicked.connect(self.ua_removeTS)
 
+        D.btn_loadTSFile.clicked.connect(self.ua_loadTSFile)
+        D.btn_saveTSFile.clicked.connect(self.ua_saveTSFile)
+        D.btn_addTSExample.clicked.connect(self.ua_loadExampleTS)
         D.spinBox_ncpu.setRange(0, multiprocessing.cpu_count())
 
         # Declare instance attributes
@@ -1176,6 +1266,56 @@ class SenseCarbon_TSV:
         self.check_enabled()
         s = ""
 
+    def init_TimeSeries(self, TS=None):
+        if TS is None:
+            TS = TimeSeries()
+        assert type(TS) is TimeSeries
+
+        if self.TS is not None:
+            disconnect_signal(self.TS.datumAdded)
+            disconnect_signal(self.TS.progress)
+            disconnect_signal(self.TS.chipLoaded)
+
+        self.TS = TS
+        self.TS.datumAdded.connect(self.ua_datumAdded)
+        self.TS.progress.connect(self.ua_TSprogress)
+        self.TS.chipLoaded.connect(self.ua_showPxCoordinate_addChips)
+
+        TSM = TimeSeriesTableModel(self.TS)
+        D = self.dlg
+        D.tableView_TimeSeries.setModel(TSM)
+        D.tableView_TimeSeries.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents)
+        D.cb_centerdate.setModel(TSM)
+        D.cb_centerdate.setModelColumn(0)
+        D.cb_centerdate.currentIndexChanged.connect(self.scrollToDate)
+
+
+    def ua_loadTSFile(self, path=None):
+        if path is None or path is False:
+            path = QFileDialog.getOpenFileName(self.dlg, 'Open Time Series file')
+
+        if os.path.exists(path):
+            self.ua_removeTS()
+            TS = TimeSeries.loadFromFile(path)
+            self.init_TimeSeries(TS)
+            self.ua_datumAdded()
+        self.check_enabled()
+
+    def ua_saveTSFile(self):
+        path = QFileDialog.getSaveFileName(self.dlg, caption='Save Time Series file')
+        if path is not None:
+            self.TS.saveToFile(path)
+
+
+    def ua_loadExampleTS(self):
+        import sensecarbon_tsv
+        path_example = file_search(os.path.dirname(sensecarbon_tsv.__file__), 'testdata.txt', recursive=True)
+        if path_example is None or len(path_example) == 0:
+            print('Can not find testdata.txt')
+        else:
+            self.ua_loadTSFile(path=path_example[0])
+
+
     def ua_selectByRectangle(self):
         if self.RectangleMapTool is not None:
             self.canvas.setMapTool(self.RectangleMapTool)
@@ -1207,15 +1347,17 @@ class SenseCarbon_TSV:
             x = geometry.x()
             y = geometry.y()
 
-        if self.TS.srs is not None and not self.TS.srs.IsSame(canvas_srs):
+        """
+        ref_srs = self.TS.getSRS()
+        if ref_srs is not None and not ref_srs.IsSame(canvas_srs):
             print('Convert canvas coordinates to time series SRS')
             g = ogr.Geometry(ogr.wkbPoint)
             g.AddPoint(x,y)
             g.AssignSpatialReference(canvas_srs)
-            g.TransformTo(self.TS.srs)
+            g.TransformTo(ref_srs)
             x = g.GetX()
             y = g.GetY()
-
+        """
 
 
         D.doubleSpinBox_subset_size_x.setValue(dx)
@@ -1245,6 +1387,7 @@ class SenseCarbon_TSV:
                 xmin, ymin, xmax, ymax = self.TS.getMaxExtent()
                 self.dlg.spinBox_coordinate_x.setRange(xmin, xmax)
                 self.dlg.spinBox_coordinate_y.setRange(ymin, ymax)
+                #x, y = self.TS.getSceneCenter()
                 self.dlg.spinBox_coordinate_x.setValue(0.5*(xmin+xmax))
                 self.dlg.spinBox_coordinate_y.setValue(0.5*(ymin+ymax))
                 s = ""
@@ -1382,8 +1525,6 @@ class SenseCarbon_TSV:
         # show the GUI
         self.dlg.show()
 
-        if DEBUG:
-            pass
 
 
     def scrollToDate(self, date):
@@ -1394,11 +1535,11 @@ class SenseCarbon_TSV:
             return
 
         #get date INDEX that is closest to requested date
+        if type(date) is str:
+            date = np.datetime64(date)
         assert type(date) is np.datetime64
-        #if isinstance(date, int):
+
         i_doi = TSDs.index(sorted(TSDs, key=lambda TSD: abs(date - TSD.getDate()))[0])
-        #else:
-        #    i_doi = min([date.astype(int), HBar.maximum()])
 
         scrollValue = int(float(i_doi+1) / len(TSDs) * HBar.maximum())
 
@@ -1417,18 +1558,13 @@ class SenseCarbon_TSV:
         cx = D.spinBox_coordinate_x.value()
         cy = D.spinBox_coordinate_y.value()
 
+        pts = [(cx - dx, cy + dy), \
+               (cx + dx, cy + dy), \
+               (cx + dx, cy - dy), \
+               (cx - dx, cy - dy)]
 
-
-        ring = ogr.Geometry(ogr.wkbLinearRing)
-        ring.AddPoint(cx - dx, cy + dy)
-        ring.AddPoint(cx + dx, cy + dy)
-        ring.AddPoint(cx + dx, cy - dy)
-        ring.AddPoint(cx - dx, cy - dy)
-
-        bb = ogr.Geometry(ogr.wkbPolygon)
-        bb.AddGeometry(ring)
+        bb = getBoundingBoxPolygon(pts, srs=self.TS.getSRS())
         bbWkt = bb.ExportToWkt()
-        bb.AssignSpatialReference(self.TS.getSRS())
         srsWkt = bb.GetSpatialReference().ExportToWkt()
         self.ImageChipBuffer.setBoundingBox(bb)
 
@@ -1623,6 +1759,7 @@ class SenseCarbon_TSV:
 
     def ua_addBandView(self, band_recommendation = [3, 2, 1]):
         self.BAND_VIEWS.append(BandView(self.TS))
+        self.refreshBandViews()
 
 
     def refreshBandViews(self):
@@ -1672,24 +1809,31 @@ class SenseCarbon_TSV:
         M.endResetModel()
         self.check_enabled()
 
-    def ua_removeTSD(self, dates):
-        if dates is None:
-            dates = self.getSelectedDates()
+    def ua_removeTSD(self, TSDs=None):
+        if TSDs is None:
+            TSDs = self.getSelectedTSDs()
+        assert isinstance(TSDs,list)
 
         M = self.dlg.tableView_TimeSeries.model()
         M.beginResetModel()
-        self.TS.removeDates(dates)
+        self.TS.removeDates(TSDs)
         M.endResetModel()
         self.check_enabled()
 
 
 
-    def getSelectedDates(self):
+    def getSelectedTSDs(self):
         TV = self.dlg.tableView_TimeSeries
         TVM = TV.model()
+        return [TVM.getTimeSeriesDatumFromIndex(idx) for idx in TV.selectionModel().selectedRows()]
 
-        return [TVM.getTimeSeriesDatumFromIndex(idx).getDate() for idx in TV.selectionModel().selectedRows()]
 
+def disconnect_signal(signal):
+    while True:
+        try:
+            signal.disconnect()
+        except TypeError:
+            break
 
 
 def showRGBData(data):
@@ -1783,9 +1927,9 @@ def run_tests():
             filesImgRE = file_search(dirSrcRE, '*_328202.tif', recursive=True)
             #filesMsk = file_search(dirSrc, '2014*_Msk.vrt')
             #S.ua_addTSImages(files=filesImg[0:1])
-            S.ua_addTSImages(files=filesImgLS)
-            S.ua_addTSImages(files=filesImgRE)
-
+            #S.ua_addTSImages(files=filesImgLS)
+            #S.ua_addTSImages(files=filesImgRE)
+            S.ua_loadExampleTS()
 
 
             #S.ua_addTSMasks(files=filesMsk)
@@ -1823,8 +1967,7 @@ def run_tests():
     print('Tests done')
     exit(0)
 
-if __name__ == '__main__':
 
+if __name__ == '__main__':
     run_tests()
-
     print('Done')
\ No newline at end of file
-- 
GitLab