diff --git a/timeseriesviewer/main.py b/timeseriesviewer/main.py
index abe58628baa04472a1545bf8a5b3961f950015f0..c009cfbc10cee498884fee771346f70e54c7af63 100644
--- a/timeseriesviewer/main.py
+++ b/timeseriesviewer/main.py
@@ -59,6 +59,9 @@ import codecs
 from timeseriesviewer import jp, mkdir, DIR_SITE_PACKAGES, file_search
 
 site.addsitedir(DIR_SITE_PACKAGES)
+from timeseriesviewer.ui import widgets
+from timeseriesviewer.timeseries import TimeSeries, TimeSeriesDatum, SensorInstrument
+
 
 #I don't know why, but this is required to run this in QGIS
 #todo: still required?
@@ -70,9 +73,6 @@ if os.path.exists(path):
 #ensure that required non-standard modules are available
 
 import pyqtgraph as pg
-from timeseriesviewer.ui import widgets
-
-regLandsatSceneID = re.compile(r"L[EMCT][1234578]{1}[12]\d{12}[a-zA-Z]{3}\d{2}")
 
 
 class TimeSeriesTableModel(QAbstractTableModel):
@@ -109,8 +109,7 @@ class TimeSeriesTableModel(QAbstractTableModel):
         if index.isValid():
             i = index.row()
             if i >= 0 and i < len(self.TS):
-                date = self.TS.getTSDs()[i]
-                return self.TS.data[date]
+                return self.TS.data[i]
 
         return None
 
@@ -218,14 +217,6 @@ class TimeSeriesItemModel(QAbstractItemModel):
     def columnCount(self, index=QModelIndex()):
         return 1
 
-LUT_SENSORNAMES = {(6, 30., 30.): 'Landsat Legacy' \
-                  , (7,30.,30.): 'L8 OLI' \
-                  , (4,10.,10.): 'S2 MSI 10m' \
-                  , (6,20.,20.): 'S2 MSI 20m' \
-                  , (3,30.,30.): 'S2 MSI 60m' \
-                  , (3,30.,30.): 'S2 MSI 60m' \
-                  , (5,5.,5.): 'RE 5m' \
-                   }
 
 
 class BandView(QObject):
@@ -243,7 +234,7 @@ class BandView(QObject):
 
         import copy
         for sensor in self.Sensors:
-            self.initSensor(copy.deepcopy(sensor), recommended_bands=recommended_bands)
+            self.initSensor(copy.deepcopy(sensor))
 
 
 
@@ -269,16 +260,7 @@ class BandView(QObject):
 
         assert type(sensor) is SensorInstrument
         if sensor not in self.representation.keys():
-            #self.bandMappings[sensor] = ((0, 0, 5000), (1, 0, 5000), (2, 0, 5000))
-            #x = imagechipviewsettings_widget.ImageChipViewSettings(sensor)
-            #x = tsv_widgets.BandViewSettings(sensor)
             x = widgets.ImageChipViewSettings(sensor)
-            #x.removeView.connect(lambda : self.removeView.emit(self))
-
-            if recommended_bands is not None:
-                assert min(recommended_bands) > 0
-                if max(recommended_bands) < sensor.nb:
-                    x.setBands(recommended_bands)
             x.create()
             self.representation[sensor] = x
 
@@ -317,93 +299,6 @@ class BandView(QObject):
         return False
 
 
-class SensorInstrument(object):
-
-    INSTRUMENTS = dict()
-
-    @staticmethod
-    def fromRasterLayer(lyr):
-
-        assert isinstance(lyr, QgsRasterLayer)
-        nb = lyr.bandCount()
-        sx = lyr.rasterUnitsPerPixelX()
-        sy = lyr.rasterUnitsPerPixelY()
-
-        bandNames = [lyr.bandName(i) for i in range(1, nb+1)]
-
-        return SensorInstrument(nb, sx, sy,
-                                band_names=bandNames,
-                                wavelengths=None,
-                                sensor_name=None)
-
-    def fromGDALDataSet(self, ds):
-        assert isinstance(ds, gdal.Dataset)
-        nb = ds.RasterCount
-
-
-    """
-    Describes a Sensor Configuration
-    """
-    def __init__(self,nb, px_size_x,px_size_y, band_names=None, wavelengths=None, sensor_name=None):
-        assert nb >= 1
-
-        self.TS = None
-        self.nb = nb
-        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
-
-        if band_names is not None:
-            assert len(band_names) == nb
-        else:
-            band_names = ['Band {}'.format(b+1) for b in range(nb)]
-
-        self.band_names = band_names
-
-        if wavelengths is not None:
-            assert len(wavelengths) == nb
-
-        self.wavelengths = wavelengths
-
-        if sensor_name is None:
-            id = (self.nb, self.px_size_x, self.px_size_y)
-            if id in LUT_SENSORNAMES.keys():
-                sensor_name = LUT_SENSORNAMES[id]
-            else:
-                sensor_name = '{}band@{}m'.format(self.nb, self.px_size_x)
-
-
-        self.sensor_name = sensor_name
-
-        self.hashvalue = hash(','.join(self.band_names))
-
-    def __eq__(self, other):
-        return self.nb == other.nb and \
-               self.px_size_x == other.px_size_x and \
-               self.px_size_y == other.px_size_y
-
-    def __hash__(self):
-        return self.hashvalue
-
-    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 ImageChipLabel(QLabel):
@@ -451,353 +346,6 @@ class ImageChipLabel(QLabel):
 
 
 
-class TimeSeries(QObject):
-    datumAdded = pyqtSignal(name='datumAdded')
-
-    #fire when a new sensor configuration is added
-    sensorAdded = pyqtSignal(object, name='sensorAdded')
-
-
-    changed = pyqtSignal()
-    progress = pyqtSignal(int,int,int, name='progress')
-    chipLoaded = pyqtSignal(object, name='chiploaded')
-    closed = pyqtSignal()
-    error = pyqtSignal(object)
-
-    def __init__(self, imageFiles=None, maskFiles=None):
-        QObject.__init__(self)
-
-        #define signals
-
-        #fire when a new TSD is added
-
-
-        self.data = collections.OrderedDict()
-
-        self.CHIP_BUFFER=dict()
-
-        self.shape = None
-
-        self.Sensors = collections.OrderedDict()
-
-        self.Pool = None
-
-        if imageFiles is not None:
-            self.addFiles(imageFiles)
-        if maskFiles is not None:
-            self.addMasks(maskFiles)
-
-    _sep = ';'
-
-
-    def loadFromFile(self, 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])
-
-        self.addFiles(images)
-        self.addMasks(masks)
-
-
-    def saveToFile(self, path):
-        if path is None or len(path) == 0:
-            return
-
-        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
-        else:
-            TSD = self.data[self.getTSDs()[0]]
-            return TSD.getSpatialReference()
-
-    def getWKT(self):
-        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 = []
-        y = []
-
-        if srs is None:
-            srs = self.getSRS()
-
-        for TSD in self.data.values():
-            bb = TSD.getBoundingBox(srs)
-            x.extend([c[0] for c in bb])
-            y.extend([c[1] for c in bb])
-
-        return (min(x), min(y), max(x), max(y))
-
-    def getObservationDates(self):
-        return [tsd.getDate() for tsd in self.data.keys()]
-
-    def getTSDs(self, date_of_interest=None):
-        if date_of_interest:
-            tsds = [tsd for tsd in self.data.keys() if tsd.getDate() == date_of_interest]
-        else:
-            tsds = list(self.data.keys())
-        return tsds
-
-    def _callback_error(self, error):
-        six.print_(error, file=sys.stderr)
-        self.error.emit(error)
-        self._callback_progress()
-
-    def _callback_spatialchips(self, results):
-        self.chipLoaded.emit(results)
-        self._callback_progress()
-
-    def _callback_progress(self):
-        self._callback_progress_done += 1
-        self.progress.emit(0, self._callback_progress_done, self._callback_progress_max)
-
-        if self._callback_progress_done >= self._callback_progress_max:
-            self._callback_progress_done = 0
-            self._callback_progress_max = 0
-            self.progress.emit(0,0,1)
-
-    def getSpatialChips_parallel(self, bbWkt, srsWkt, TSD_band_list, ncpu=1):
-        assert type(bbWkt) is str and type(srsWkt) is str
-
-        import multiprocessing
-        if self.Pool is not None:
-            self.Pool.terminate()
-
-        self.Pool = multiprocessing.Pool(processes=ncpu)
-
-
-        self._callback_progress_max = len(TSD_band_list)
-        self._callback_progress_done = 0
-
-        for T in TSD_band_list:
-
-            TSD = copy.deepcopy(T[0])
-            bands = T[1]
-            #TSD = pickle.dumps(self.data[date])
-            args = (TSD, bbWkt, srsWkt)
-            kwds = {'bands':bands}
-
-            if six.PY3:
-                self.Pool.apply_async(PFunc_TimeSeries_getSpatialChip, \
-                                 args=args, kwds=kwds, \
-                                 callback=self._callback_spatialchips, error_callback=self._callback_error)
-            else:
-                self.Pool.apply_async(PFunc_TimeSeries_getSpatialChip, \
-                                      args, kwds, self._callback_spatialchips)
-
-        s = ""
-
-        pass
-
-    def getImageChips(self, xy, size=50, bands=[4,5,6], dates=None):
-        chipCollection = collections.OrderedDict()
-
-        if dates is None:
-            dates = self.data.keys()
-        for date in dates:
-            TSD = self.data[date]
-            chipCollection[date] = TSD.readImageChip(xy, size=size, bands=bands)
-
-        return chipCollection
-
-    def addMasks(self, files, raise_errors=True, mask_value=0, exclude_mask_value=True):
-        assert isinstance(files, list)
-        l = len(files)
-
-        self.progress.emit(0,0,l)
-        for i, file in enumerate(files):
-
-            try:
-                self.addMask(file, raise_errors=raise_errors, mask_value=mask_value, exclude_mask_value=exclude_mask_value, _quiet=True)
-            except:
-                pass
-
-            self.progress.emit(0,i+1,l)
-
-        self.progress.emit(0,0,1)
-        self.changed.emit()
-
-    def addMask(self, pathMsk, raise_errors=True, mask_value=0, exclude_mask_value=True, _quiet=False):
-        print('Add mask {}...'.format(pathMsk))
-        ds = getDS(pathMsk)
-        date = getImageDate(ds)
-
-        if date in self.data.keys():
-            TSD = self.data[date]
-
-            if not _quiet:
-                self.changed.emit()
-
-            return TSD.setMask(pathMsk, raise_errors=raise_errors, mask_value=mask_value, exclude_mask_value=exclude_mask_value)
-        else:
-            info = 'TimeSeries does not contain date {} {}'.format(date, pathMsk)
-            if raise_errors:
-                raise Exception(info)
-            else:
-                six.print_(info, file=sys.stderr)
-            return False
-
-    def removeAll(self):
-        self.clear()
-
-    def clear(self):
-        self.Sensors.clear()
-        self.data.clear()
-        self.changed.emit()
-
-
-    def removeDates(self, TSDs):
-        for TSD in TSDs:
-            self.removeTSD(TSD, _quiet=True)
-        self.changed.emit()
-
-    def removeTSD(self, TSD, _quiet=False):
-
-        assert type(TSD) is TimeSeriesDatum
-        S = TSD.sensor
-        self.Sensors[S].remove(TSD)
-        self.data.pop(TSD, None)
-        if len(self.Sensors[S]) == 0:
-            self.Sensors.pop(S)
-        if not _quiet:
-            self.changed.emit()
-
-
-    def addFile(self, pathImg, pathMsk=None, _quiet=False):
-
-        six.print_(pathImg)
-        six.print_('Add image {}...'.format(pathImg))
-
-        try:
-            sensorAdded = False
-            TSD = TimeSeriesDatum(pathImg, pathMsk=pathMsk)
-            existingSensors = list(self.Sensors.keys())
-            if TSD.sensor not in existingSensors:
-                self.Sensors[TSD.sensor] = list()
-                sensorAdded = True
-            else:
-                TSD.sensor = existingSensors[existingSensors.index(TSD.sensor)]
-
-            if TSD in self.data.keys():
-                six.print_('Time series datum already added: {}'.format(str(TSD)), file=sys.stderr)
-            else:
-                self.Sensors[TSD.sensor].append(TSD)
-                self.data[TSD] = TSD
-
-            if sensorAdded:
-                self.sensorAdded.emit(TSD.sensor)
-
-            if not _quiet:
-                self._sortTimeSeriesData()
-                self.changed.emit()
-                self.datumAdded.emit()
-        except:
-            exc_type, exc_value, exc_traceback = sys.exc_info()
-            traceback.print_exception(exc_type, exc_value, exc_traceback, limit=2)
-            six.print_('Unable to add {}'.format(file), file=sys.stderr)
-            pass
-
-
-
-    def addFiles(self, files):
-        assert isinstance(files, list)
-        l = len(files)
-        assert l > 0
-
-        self.progress.emit(0,0,l)
-        for i, file in enumerate(files):
-            self.addFile(file, _quiet=True)
-            self.progress.emit(0,i+1,l)
-
-        self._sortTimeSeriesData()
-        self.progress.emit(0,0,1)
-        self.datumAdded.emit()
-        self.changed.emit()
-
-
-
-    def _sortTimeSeriesData(self):
-        self.data = collections.OrderedDict(sorted(self.data.items(), key=lambda t:t[0]))
-
-    def __len__(self):
-        return len(self.data)
-
-    def __repr__(self):
-        info = []
-        info.append('TimeSeries:')
-        l = len(self)
-        info.append('  Scenes: {}'.format(l))
-
-        if l > 0:
-            keys = list(self.data.keys())
-            info.append('  Range: {} to {}'.format(keys[0], keys[-1]))
-        return '\n'.join(info)
-
-def PFunc_TimeSeries_getSpatialChip(TSD, bbWkt, srsWkt , bands=[4,5,3]):
-
-    chipdata = TSD.readSpatialChip(bbWkt, srs=srsWkt, bands=bands)
-
-    return TSD, chipdata
-
-def px2Coordinate(gt, pxX, pxY, upper_left=True):
-    cx = gt[0] + pxX*gt[1] + pxY*gt[2]
-    cy = gt[3] + pxX*gt[4] + pxY*gt[5]
-    if not upper_left:
-        cx += gt[1]*0.5
-        cy += gt[5]*0.5
-    return cx, cy
-
-def coordinate2px(gt, cx, cy):
-    px = int((cx - gt[0]) / gt[1])
-    py = int((cy - gt[3]) / gt[5])
-    return px, py
 
 
 def getBoundingBoxPolygon(points, srs=None):
@@ -806,21 +354,15 @@ def getBoundingBoxPolygon(points, srs=None):
         ring.AddPoint(point[0], point[1])
     bb = ogr.Geometry(ogr.wkbPolygon)
     bb.AddGeometry(ring)
-    if srs:
-        bb.AssignSpatialReference(srs)
+    if isinstance(srs, QgsCoordinateReferenceSystem):
+
+        _crs = osr.SpatialReference()
+        _crs.ImportFromWkt(srs.toWkt())
+        bb.AssignSpatialReference(_crs)
     return bb
 
 
 
-def transformGeometry(geom, crsSrc, crsDst, trans=None):
-    if trans is None:
-        assert isinstance(crsSrc, QgsCoordinateReferenceSystem)
-        assert isinstance(crsDst, QgsCoordinateReferenceSystem)
-        return transformGeometry(geom, None, None, trans=QgsCoordinateTransform(crsSrc, crsDst))
-    else:
-        assert isinstance(trans, QgsCoordinateTransform)
-        return trans.transform(geom)
-
 
 
 
@@ -829,25 +371,7 @@ def getDS(ds):
         ds = gdal.Open(ds)
     return ds
 
-regAcqDate = re.compile(r'acquisition[ ]*(time|date|day)', re.I)
-def getImageDate2(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)
-    if date: return date
-
-    #find date in file directory path
-    date = parseAcquisitionDate(date)
 
-    return date
 
 def getBandNames(lyr):
     assert isinstance(lyr, QgsRasterLayer)
@@ -878,239 +402,6 @@ def getImageDate(ds):
     raise Exception('Can not identify acquisition date of {}'.format(path))
 
 
-class TimeSeriesDatum(object):
-    """
-    Collects all data sets related to one sensor
-    """
-
-    def __init__(self, pathImg, pathMsk=None):
-        self.pathImg = pathImg
-        self.pathMsk = None
-
-        self.lyrImg = QgsRasterLayer(pathImg, os.path.basename(pathImg), False)
-        self.crs = self.lyrImg.dataProvider().crs()
-        self.sensor = SensorInstrument.fromRasterLayer(self.lyrImg)
-
-        self.date = getImageDate2(self.lyrImg)
-        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()
-        self.srs_wkt = str(self.crs.toWkt())
-
-
-        if pathMsk:
-            self.setMask(pathMsk)
-
-    def getdtype(self):
-        return gdal_array.GDALTypeCodeToNumericTypeCode(self.etype)
-
-    def getDate(self):
-        return np.datetime64(self.date)
-
-
-    def getSpatialReference(self):
-        return self.crs
-        srs = osr.SpatialReference()
-        srs.ImportFromWkt(self.srs_wkt)
-        return srs
-
-
-    def getBoundingBox(self, srs=None):
-
-        bbox = self.lyrImg.extent()
-        if srs:
-            assert isinstance(srs, QgsCoordinateReferenceSystem)
-            bbox = transformGeometry(bbox, self.crs, srs)
-        return bbox
-
-
-    def setMask(self, pathMsk, raise_errors=True, mask_value=0, exclude_mask_value=True):
-        dsMsk = gdal.Open(pathMsk)
-        mskDate = getImageDate(dsMsk)
-
-
-        errors = list()
-        if mskDate and mskDate != self.getDate():
-            errors.append('Mask date differs from image date')
-        if self.ns != dsMsk.RasterXSize or self.nl != dsMsk.RasterYSize:
-            errors.append('Spatial size differs')
-        if dsMsk.RasterCount != 1:
-            errors.append('Mask has > 1 bands')
-
-        projImg = self.getSpatialReference()
-        projMsk = osr.SpatialReference()
-        projMsk.ImportFromWkt(dsMsk.GetProjection())
-
-        if not projImg.IsSame(projMsk):
-            errors.append('Spatial Reference differs from image')
-        if self.gt != list(dsMsk.GetGeoTransform()):
-            errors.append('Geotransformation differs from image')
-
-        if len(errors) > 0:
-            errors.insert(0, 'pathImg:{} \npathMsk:{}'.format(self.pathImg, pathMsk))
-            errors = '\n'.join(errors)
-            if raise_errors:
-                raise Exception(errors)
-            else:
-                six.print_(errors, file=sys.stderr)
-                return False
-        else:
-            self.pathMsk = pathMsk
-            self.mask_value = mask_value
-            self.exclude_mask_value = exclude_mask_value
-
-            return True
-
-    def readSpatialChip(self, geometry, srs=None, bands=[4,5,3]):
-
-        srs_img = osr.SpatialReference()
-        srs_img.ImportFromWkt(self.srs_wkt)
-
-
-        if type(geometry) is ogr.Geometry:
-            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:
-                srs_bb = osr.SpatialReference()
-                srs_bb.ImportFromWkt(srs)
-            else:
-                srs_bb = srs.Clone()
-            g_bb = ogr.CreateGeometryFromWkt(geometry, srs_bb)
-
-        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_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]))
-        lr_px = [c+1 for c in lr_px] #+1
-
-        return self.readImageChip([ul_px[0], lr_px[0]], [ul_px[1], lr_px[1]], bands=bands)
-
-    def readImageChip(self, px_x, px_y, bands=[4,5,3]):
-
-        ds = gdal.Open(self.pathImg, gdal.GA_ReadOnly)
-
-        assert len(px_x) == 2 and px_x[0] <= px_x[1]
-        assert len(px_y) == 2 and px_y[0] <= px_y[1]
-
-        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
-
-
-        chipdata = dict()
-
-
-
-        #pixel indices in source image
-        x0 = max([0, px_x[0]])
-        y0 = max([0, px_y[0]])
-        x1 = min([src_ns, px_x[1]])
-        y1 = min([src_nl, px_y[1]])
-        win_xsize = x1-x0+1
-        win_ysize = y1-y0+1
-
-        #pixel indices in image chip (ideally 0 and ns-1 or nl-1)
-        i0 = x0 - px_x[0]
-        i1 = i0 + win_xsize
-
-        j0 = y0 - px_y[0]
-        j1 = j0+ win_ysize
-
-
-
-
-        templateImg = np.zeros((nl,ns))
-        if self.nodata:
-            templateImg *= self.nodata
-
-        templateImg = templateImg.astype(self.getdtype())
-        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 {}'.format(self.pathImg), file=sys.stderr)
-            for i, b in enumerate(bands):
-                chipdata[b] = np.copy(templateImg)
-
-        else:
-            for i, b in enumerate(bands):
-                band = ds.GetRasterBand(b)
-                data = np.copy(templateImg)
-                data[j0:j1,i0:i1] = band.ReadAsArray(xoff=x0, yoff=y0, win_xsize=win_xsize,win_ysize=win_ysize)
-                chipdata[b] = data
-                nodatavalue = band.GetNoDataValue()
-                if nodatavalue is not None:
-                    templateMsk[j0:j1,i0:i1] = np.logical_and(templateMsk[j0:j1,i0:i1], data[j0:j1,i0:i1] != nodatavalue)
-
-            if self.pathMsk:
-                ds = gdal.Open(self.pathMsk)
-                tmp = ds.GetRasterBand(1).ReadAsArray(xoff=x0, yoff=y0, \
-                            win_xsize=win_xsize,win_ysize=win_ysize) == 0
-
-                templateMsk[j0:j1,i0:i1] = np.logical_and(templateMsk[j0:j1,i0:i1], tmp)
-
-        chipdata['mask'] = templateMsk
-        return chipdata
-
-    def __repr__(self):
-        return 'TS Datum {} {}'.format(self.date, str(self.sensor))
-
-    def __cmp__(self, other):
-        return cmp(str((self.date, self.sensor)), str((other.date, other.sensor)))
-
-    def __eq__(self, other):
-        return self.date == other.date and self.sensor == other.sensor
-
-    def __hash__(self):
-        return hash((self.date,self.sensor.sensor_name))
-
-
-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 parseAcquisitionDate(text):
-    match = regLandsatSceneID.search(text)
-    if match:
-        id = match.group()
-        return getDateTime64FromYYYYDOY(id[9:16])
-    match = regYYYYMMDD.search(text)
-    if match:
-        return np.datetime64(match.group())
-    match = regYYYYDOY.search(text)
-    if match:
-        return getDateTime64FromYYYYDOY(match.group())
-    match = regYYYY.search(text)
-    if match:
-        return np.datetime64(match.group())
-    return None
-
-
-
-def getDateTime64FromYYYYDOY(yyyydoy):
-    return getDateTime64FromDOY(yyyydoy[0:4], yyyydoy[4:7])
-
-def getDateTime64FromDOY(year, doy):
-        if type(year) is str:
-            year = int(year)
-        if type(doy) is str:
-            doy = int(doy)
-        return np.datetime64('{:04d}-01-01'.format(year)) + np.timedelta64(doy-1, 'D')
-
 
 def getChip3d(chips, rgb_idx, ranges):
     assert len(rgb_idx) == 3 and len(rgb_idx) == len(ranges)
@@ -1138,6 +429,27 @@ def Array2Image(d3d):
 
     return QImage(d3d.data, ns, nl, QImage.Format_RGB888)
 
+
+class LayerLoader(QRunnable):
+
+    def __init__(self, paths):
+        super(LayerLoader, self).__init__()
+        self.signals = LoaderSignals()
+
+        self.paths = paths
+
+    def run(self):
+        lyrs = []
+        for path in self.paths:
+            lyr = QgsRasterLayer(path)
+            if lyr:
+                lyrs.append(lyr)
+                self.signals.sigRasterLayerLoaded.emit(lyr)
+                print('{} loaded'.format(path))
+            else:
+                print('Failed to load {}'.format(path))
+        self.signals.sigFinished.emit()
+
 class VerticalLabel(QLabel):
     def __init__(self, text):
         super(self.__class__, self).__init__()
@@ -1341,7 +653,7 @@ class TimeSeriesViewer:
         D.btn_removeTSD.clicked.connect(lambda : self.ua_removeTSD(None))
         D.btn_removeTS.clicked.connect(self.ua_clear_TS)
 
-
+        D.sliderDOI.sliderMoved.connect(self.setDOILabel)
         D.spinBox_ncpu.setRange(0, multiprocessing.cpu_count())
 
 
@@ -1366,6 +678,11 @@ class TimeSeriesViewer:
         self.check_enabled()
         s = ""
 
+    def setDOILabel(self, i):
+        TSD = self.TS.data[i-1]
+        self.dlg.labelDOI.setText(str(TSD.date))
+        s = ""
+
     @staticmethod
     def icon():
         return QIcon(':/plugins/SenseCarbon/icon.png')
@@ -1383,9 +700,11 @@ class TimeSeriesViewer:
             disconnect_signal(self.TS.datumAdded)
             disconnect_signal(self.TS.progress)
             disconnect_signal(self.TS.chipLoaded)
+            disconnect_signal(self.TS.changed)
 
         self.TS = TS
         self.TS.datumAdded.connect(self.ua_datumAdded)
+        self.TS.changed.connect(self.timeseriesChanged)
         self.TS.progress.connect(self.ua_TSprogress)
         self.TS.chipLoaded.connect(self.ua_showPxCoordinate_addChips)
 
@@ -1393,10 +712,15 @@ class TimeSeriesViewer:
         D = self.dlg
         D.tableView_TimeSeries.setModel(TSM)
         D.tableView_TimeSeries.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents)
-        D.cb_doi.setModel(TSM)
-        D.cb_doi.setModelColumn(0)
-        D.cb_doi.currentIndexChanged.connect(self.scrollToDate)
+        #D.cb_doi.setModel(TSM)
+        #D.cb_doi.setModelColumn(0)
+        #D.cb_doi.currentIndexChanged.connect(self.scrollToDate)
 
+    def timeseriesChanged(self):
+        D = self.dlg
+        D.sliderDOI.setMinimum(1)
+        D.sliderDOI.setMaximum(len(self.TS.data))
+        s = ""
 
     def ua_loadTSFile(self, path=None):
         if path is None or path is False:
@@ -1422,12 +746,11 @@ class TimeSeriesViewer:
 
 
     def ua_loadExampleTS(self):
-        from timeseriesviewer import DIR_EXAMPLE
-        path_example = jp(DIR_EXAMPLE, 'testdata.txt')
-        if not os.path.exists(path_example):
+        from timeseriesviewer import PATH_EXAMPLE_TIMESERIES
+        if not os.path.exists(PATH_EXAMPLE_TIMESERIES):
             QMessageBox.information(self.dlg, 'File not found', '{} - this file describes an exemplary time series.'.format(path_example))
         else:
-            self.ua_loadTSFile(path=path_example)
+            self.ua_loadTSFile(path=PATH_EXAMPLE_TIMESERIES)
 
 
 
@@ -1440,12 +763,10 @@ class TimeSeriesViewer:
             self.canvas.setMapTool(self.PointMapTool)
 
     def setCanvasSRS(self,srs):
-        if type(srs) is osr.SpatialReference:
-            self.canvas_srs = srs
-        else:
-            self.canvas_srs.ImportFromWkt(srs)
+        assert isinstance(srs, QgsCoordinateReferenceSystem)
+        self.canvas_srs = srs
 
-        self.dlg.tb_bb_srs.setPlainText(self.canvas_srs.ExportToProj4())
+        self.dlg.tb_bb_srs.setPlainText(self.canvas_srs.toWkt())
 
     def ua_selectBy_Response(self, geometry, srs_wkt):
         D = self.dlg
@@ -1512,14 +833,17 @@ class TimeSeriesViewer:
             self.setCanvasSRS(self.TS.getSRS())
             if self.dlg.spinBox_coordinate_x.value() == 0.0 and \
                self.dlg.spinBox_coordinate_y.value() == 0.0:
-                xmin, ymin, xmax, ymax = self.TS.getMaxExtent(srs=self.canvas_srs)
-                self.dlg.spinBox_coordinate_x.setRange(xmin, xmax)
-                self.dlg.spinBox_coordinate_y.setRange(ymin, ymax)
+                bbox = self.TS.getMaxExtent(srs=self.canvas_srs)
+
+                self.dlg.spinBox_coordinate_x.setRange(bbox.xMinimum(), bbox.xMaximum())
+                self.dlg.spinBox_coordinate_y.setRange(bbox.yMinimum(), bbox.yMaximum())
                 #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))
+                c = bbox.center()
+                self.dlg.spinBox_coordinate_x.setValue(c.x())
+                self.dlg.spinBox_coordinate_y.setValue(c.y())
                 s = ""
-        self.dlg.cb_doi.setCurrentIndex(int(len(self.TS) / 2))
+        #self.dlg.sliderDOI
+
         self.dlg.tableView_TimeSeries.resizeColumnsToContents()
 
     def check_enabled(self):
@@ -1628,11 +952,8 @@ class TimeSeriesViewer:
 
         #get the dates of interes
         dates_of_interest = list()
-        doiTSD = D.cb_doi.itemData(D.cb_doi.currentIndex())
-        if doiTSD is None:
-            idx = int(len(self.TS)/2)
-            doiTSD = D.cb_doi.itemData(idx)
-            D.cb_doi.setCurrentIndex(idx)
+
+        doiTSD = self.TS.data[int(D.sliderDOI.value())-1]
         centerDate = doiTSD.getDate()
         allDates = self.TS.getObservationDates()
         i_doi = allDates.index(centerDate)