diff --git a/eotimeseriesviewer/__init__.py b/eotimeseriesviewer/__init__.py
index 8e05bc99c718e3ae41f54aa64ed110b0ce796312..dc91c11e597194b65227d54e464eccdd09927a37 100644
--- a/eotimeseriesviewer/__init__.py
+++ b/eotimeseriesviewer/__init__.py
@@ -45,8 +45,6 @@ from qgis.PyQt.QtGui import QIcon
 
 mkdir = lambda p: os.makedirs(p, exist_ok=True)
 
-
-
 DIR = os.path.dirname(__file__)
 DIR_REPO = os.path.dirname(DIR)
 DIR_UI = jp(DIR, *['ui'])
diff --git a/eotimeseriesviewer/pixelloader.py b/eotimeseriesviewer/pixelloader.py
index 4eaaea80b7b6ec4280f546559a50e2e507c3b237..e75241b88a4704b0ff1869f60471adb6ccc5e7f3 100644
--- a/eotimeseriesviewer/pixelloader.py
+++ b/eotimeseriesviewer/pixelloader.py
@@ -199,7 +199,7 @@ def doLoaderTask(taskWrapper:QgsTask, dump):
     nb, ns, nl = ds.RasterCount, ds.RasterXSize, ds.RasterYSize
 
     bandIndices = list(range(nb)) if task.bandIndices is None else list(task.bandIndices)
-    #ensure to load valid indices only
+    # ensure to load valid indices only
     bandIndices = [i for i in bandIndices if i >= 0 and i < nb]
 
     task.bandIndices = bandIndices
diff --git a/eotimeseriesviewer/profilevisualization.py b/eotimeseriesviewer/profilevisualization.py
index eb3c906e8d03c8fb93b7b42f166f32f0b72bcf38..47640c9d376dd4edd0cb97eb399d4148b34fbd05 100644
--- a/eotimeseriesviewer/profilevisualization.py
+++ b/eotimeseriesviewer/profilevisualization.py
@@ -31,7 +31,7 @@ from qgis.PyQt.QtGui import *
 
 from .timeseries import *
 from .utils import SpatialExtent, SpatialPoint, px2geo, loadUI, nextColor
-from .externals.qps.plotstyling.plotstyling import PlotStyle, PlotStyleButton
+from .externals.qps.plotstyling.plotstyling import PlotStyle, PlotStyleButton, PlotStyleDialog
 from .externals import pyqtgraph as pg
 from .sensorvisualization import SensorListModel
 from .temporalprofiles2d import *
@@ -483,9 +483,17 @@ class PlotSettingsModel2D(QAbstractTableModel):
         self.columnNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression]
 
         self.mPlotSettings = []
+        self.mIconSize = QSize(25, 25)
 
         self.dataChanged.connect(self.signaler)
 
+    def setIconSize(self, size:QSize):
+        assert isinstance(size, QSize)
+        self.mIconSize = size
+        tl = self.createIndex(0, self.columnIndex(self.cnStyle))
+        lr = self.createIndex(self.rowCount(), self.columnIndex(self.cnStyle))
+        self.dataChanged.emit(tl, lr, [Qt.DecorationRole])
+
     def __len__(self):
         return len(self.mPlotSettings)
 
@@ -646,13 +654,13 @@ class PlotSettingsModel2D(QAbstractTableModel):
                 elif columnName == self.cnTemporalProfile:
                     tp = plotStyle.temporalProfile()
                     if isinstance(tp, TemporalProfile):
-                        value = tp.name()
+                        value = '{} {}'.format(tp.id(), tp.name())
                     else:
                         value = 'undefined'
 
             if role == Qt.DecorationRole:
                 if columnName == self.cnStyle:
-                    value = plotStyle.createIcon(QSize(25,25))
+                    value = plotStyle.createIcon(self.mIconSize)
 
             elif role == Qt.CheckStateRole:
                 if columnName == self.cnTemporalProfile:
@@ -753,1183 +761,1249 @@ class PlotSettingsModel2D(QAbstractTableModel):
 
 
 
-class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')):
 
+class PlotSettingsTableView(QTableView):
 
-    def __init__(self, parent=None):
-        super(ProfileViewDockUI, self).__init__(parent)
-        self.setupUi(self)
+    def __init__(self, *args, **kwds):
+        super(PlotSettingsTableView, self).__init__(*args, **kwds)
 
-        self.addActions(self.findChildren(QAction))
+    def contextMenuEvent(self, event: QContextMenuEvent):
+        """
+        Creates and shows the QMenu
+        :param event: QContextMenuEvent
+        """
 
-        self.mActions2D = [self.actionAddStyle2D, self.actionRemoveStyle2D, self.actionRefresh2D, self.actionReset2DPlot]
-        self.mActions3D = [self.actionAddStyle3D, self.actionRemoveStyle3D, self.actionRefresh3D,
-                           self.actionReset3DCamera]
-        self.mActionsTP = [self.actionLoadTPFromOgr, self.actionSaveTemporalProfiles, self.actionToggleEditing,
-                           self.actionRemoveTemporalProfile, self.actionLoadMissingValues]
-        #TBD.
-        #self.line.setVisible(False)
-        #self.listWidget.setVisible(False)
-        self.baseTitle = self.windowTitle()
-        self.stackedWidget.currentChanged.connect(self.onStackPageChanged)
+        indices = self.selectionModel().selectedIndexes()
 
+        indices2 = [self.model().mapToSource(idx) for idx in indices]
 
-        self.plotWidget3D = None
-        self.plotWidget3DMPL = None
+        if len(indices2) > 0:
+            menu = QMenu(self)
+            a = menu.addAction('Set Style')
+            a.triggered.connect(lambda *args, i=indices2: self.onSetStyle(i))
 
-        self.init3DWidgets('gl')
+            menu.popup(QCursor.pos())
 
+    def onSetStyle(self, indices):
 
-        #pi = self.plotWidget2D.plotItem
-        #ax = DateAxis(orientation='bottom', showValues=True)
-        #pi.layout.addItem(ax, 3,2)
+        if len(indices) > 0:
+            refStyle = None
 
+            model = self.model().sourceModel()
+            assert isinstance(model, PlotSettingsModel2D)
 
-        self.TS = None
+            colStyle = model.columnIndex(model.cnStyle)
 
-        self.progressBar.setMinimum(0)
-        self.progressBar.setMaximum(100)
-        self.progressBar.setValue(0)
-        self.progressInfo.setText('')
-        self.pxViewModel2D = None
-        self.pxViewModel3D = None
+            for idx in indices:
+                style = self.model().sourceModel().idx2plotStyle(idx)
+                if isinstance(style, PlotStyle):
+                    refStyle = style
+                    break
+            if isinstance(refStyle, PlotStyle):
+                newStyle = PlotStyleDialog.getPlotStyle(plotStyle=refStyle)
+                if isinstance(newStyle, PlotStyle):
+                    for idx in indices:
+                        assert isinstance(idx, QModelIndex)
+                        idxStyle = model.createIndex(idx.row(), colStyle)
+                        model.setData(idxStyle, newStyle, role=Qt.EditRole)
 
-        self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
 
-    def init3DWidgets(self, mode='gl'):
-        assert mode in ['gl']
-        l = self.frame3DPlot.layout()
 
-        if ENABLE_OPENGL and OPENGL_AVAILABLE and mode == 'gl':
+class PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate):
+    """
 
-            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
-            self.plotWidget3D = ViewWidget3D(parent=self.labelDummy3D.parent())
-            self.plotWidget3D.setObjectName('plotWidget3D')
+    """
+    def __init__(self, tableView, temporalProfileLayer, parent=None):
+        assert isinstance(tableView, QTableView)
+        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
+        super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent)
+        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
+        self.mTableView = tableView
+        self.mTemporalProfileLayer = temporalProfileLayer
+        self.mTimeSeries = temporalProfileLayer.timeSeries()
 
-            size = self.labelDummy3D.size()
-            l.addWidget(self.plotWidget3D)
-            self.plotWidget3D.setSizePolicy(self.labelDummy3D.sizePolicy())
-            self.labelDummy3D.setVisible(False)
-            l.removeWidget(self.labelDummy3D)
-            #self.plotWidget3D.setBaseSize(size)
-            self.splitter3D.setSizes([100, 100])
-            self.frameSettings3D.setEnabled(True)
-        else:
-            self.frameSettings3D.setEnabled(False)
+        self.mSensorLayers = {}
 
-    def onStackPageChanged(self, i):
-        w = self.stackedWidget.currentWidget()
-        title = self.baseTitle
-        if w == self.page2D:
-            title = '{} | 2D'.format(title)
-            for a in self.mActions2D:
-                a.setVisible(True)
-            for a in self.mActions3D + self.mActionsTP:
-                a.setVisible(False)
-        elif w == self.page3D:
-            title = '{} | 3D (experimental!)'.format(title)
-            for a in self.mActions2D + self.mActionsTP:
-                a.setVisible(False)
-            for a in self.mActions3D:
-                a.setVisible(True)
-        elif w == self.pagePixel:
-            title = '{} | Coordinates'.format(title)
-            for a in self.mActions2D + self.mActions3D:
-                a.setVisible(False)
-            for a in self.mActionsTP:
-                a.setVisible(True)
+    def paint(self, painter: QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex):
 
-        w.update()
-        self.setWindowTitle(title)
+        if index.column() == 2:
+            style = self.style(index)
+
+            h = self.mTableView.verticalHeader().defaultSectionSize()
+            w = self.mTableView.horizontalHeader().defaultSectionSize()
+            if h > 0 and w > 0:
+                px = style.createPixmap(size=QSize(w, h))
+                label = QLabel()
+                label.setPixmap(px)
+                painter.drawPixmap(option.rect, px)
+                #QApplication.style().drawControl(QStyle.CE_CustomBase, label, painter)
+            else:
+                super(PlotSettingsModel2DWidgetDelegate, self).paint(painter, option, index)
+        else:
+            super(PlotSettingsModel2DWidgetDelegate, self).paint(painter, option, index)
 
+    def sortFilterProxyModel(self)->QSortFilterProxyModel:
+        return self.mTableView.model()
 
-class SpectralTemporalVisualization(QObject):
+    def plotSettingsModel(self)->PlotSettingsModel2D:
+        return self.sortFilterProxyModel().sourceModel()
 
-    sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem)
+    def setItemDelegates(self, tableView):
+        assert isinstance(tableView, QTableView)
+        model = self.plotSettingsModel()
+
+        assert isinstance(model, PlotSettingsModel2D)
+        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
+            i = model.columnNames.index(c)
+            tableView.setItemDelegateForColumn(i, self)
 
+    def getColumnName(self, index):
+        assert index.isValid()
+        model = self.plotSettingsModel()
+        assert isinstance(model, PlotSettingsModel2D)
+        return model.columnNames[index.column()]
     """
-    Signalizes to move to specific date of interest
+    def sizeHint(self, options, index):
+        s = super(ExpressionDelegate, self).sizeHint(options, index)
+        exprString = self.tableView.model().data(index)
+        l = QLabel()
+        l.setText(exprString)
+        x = l.sizeHint().width() + 100
+        s = QSize(x, s.height())
+        return self._preferedSize
     """
-    sigMoveToDate = pyqtSignal(np.datetime64)
+    def exampleLyr(self, sensor):
+        # if isinstance(sensor, SensorInstrument):
+        if sensor not in self.mSensorLayers.keys():
 
+            crs = QgsCoordinateReferenceSystem('EPSG:4862')
+            uri = 'Point?crs={}'.format(crs.authid())
+            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
+            f = sensorExampleQgsFeature(sensor)
+            assert isinstance(f, QgsFeature)
+            assert lyr.startEditing()
+            for field in f.fields():
+                lyr.addAttribute(field)
+            lyr.addFeature(f)
+            lyr.commitChanges()
+            self.mSensorLayers[sensor] = lyr
+        return self.mSensorLayers[sensor]
 
-    def __init__(self, timeSeries, profileDock):
-        super(SpectralTemporalVisualization, self).__init__()
+    def createEditor(self, parent, option, index):
+        cname = self.getColumnName(index)
+        model = self.plotSettingsModel()
+        pmodel = self.sortFilterProxyModel()
 
-        assert isinstance(profileDock, ProfileViewDockUI)
-        self.ui = profileDock
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
 
-        import eotimeseriesviewer.pixelloader
-        if DEBUG:
-            eotimeseriesviewer.pixelloader.DEBUG = True
+            plotStyle = model.idx2plotStyle(pmodel.mapToSource(index))
 
-        #the timeseries. will be set later
-        assert isinstance(timeSeries, TimeSeries)
-        self.TS = timeSeries
-        self.plot_initialized = False
+            if isinstance(plotStyle, TemporalProfile2DPlotStyle):
+                if cname == model.cnExpression:
 
-        self.plot2D = self.ui.plotWidget2D
-        self.plot2D.getViewBox().sigMoveToDate.connect(self.sigMoveToDate)
-        self.plot3D = self.ui.plotWidget3D
+                    w = QgsFieldExpressionWidget(parent=parent)
+                    w.setExpressionDialogTitle('Values')
+                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
+                    w.fieldChanged[str, bool].connect(lambda n, b: self.checkData(index, w, w.expression()))
+                    w.setExpression(plotStyle.expression())
+                    plotStyle.sigSensorChanged.connect(lambda s: w.setLayer(self.exampleLyr(s)))
+                    if isinstance(plotStyle.sensor(), SensorInstrument):
+                        w.setLayer(self.exampleLyr(plotStyle.sensor()))
 
-        # temporal profile collection to store loaded values
-        self.mTemporalProfileLayer = TemporalProfileLayer(self.TS)
-        self.mTemporalProfileLayer.sigTemporalProfilesAdded.connect(self.onTemporalProfilesAdded)
-        #self.mTemporalProfileLayer.startEditing()
-        self.mTemporalProfileLayer.selectionChanged.connect(self.onTemporalProfileSelectionChanged)
+                elif cname == model.cnStyle:
+                    w = PlotStyleButton(parent=parent)
+                    w.setPlotStyle(plotStyle)
+                    w.setToolTip('Set style.')
+                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
 
-        # fix to not loose C++ reference on temporal profile layer in case it is removed from QGIS mapcanvas
-        self.mMapCanvas = QgsMapCanvas()
-        self.mMapCanvas.setVisible(False)
-        self.mMapCanvas.setLayers([self.mTemporalProfileLayer])
-        # self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection)
+                elif cname == model.cnSensor:
+                    w = QComboBox(parent=parent)
+                    m = SensorListModel(self.mTimeSeries)
+                    w.setModel(m)
 
-        assert isinstance(self.ui.mDualView, QgsDualView)
-        self.ui.mDualView.init(self.mTemporalProfileLayer, self.mMapCanvas)
-        self.ui.mDualView.setView(QgsDualView.AttributeTable)
-        # pixel loader to load pixel values in parallel
-        config = QgsAttributeTableConfig()
-        config.update(self.mTemporalProfileLayer.fields())
-        config.setActionWidgetVisible(True)
+                elif cname == model.cnTemporalProfile:
+                    w = QgsFeatureListComboBox(parent=parent)
+                    w.setSourceLayer(self.mTemporalProfileLayer)
+                    w.setIdentifierField(FN_ID)
+                    w.setDisplayExpression('to_string("{}")+\'  \'+"name"'.format(FN_ID))
+                    w.setAllowNull(False)
+                else:
+                    raise NotImplementedError()
+        return w
 
-        hidden = [FN_ID]
-        for i, columnConfig in enumerate(config.columns()):
+    def checkData(self, index, w, value):
+        assert isinstance(index, QModelIndex)
+        model = self.mTableView.model()
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
+            plotStyle = model.idx2plotStyle(index)
+            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
+            if isinstance(w, QgsFieldExpressionWidget):
+                assert value == w.expression()
+                assert w.isExpressionValid(value) == w.isValidExpression()
 
-            assert isinstance(columnConfig, QgsAttributeTableConfig.ColumnConfig)
-            config.setColumnHidden(i, columnConfig.name in hidden)
+                if w.isValidExpression():
+                    self.commitData.emit(w)
+                else:
+                    s = ""
+                    #print(('Delegate commit failed',w.asExpression()))
+            if isinstance(w, PlotStyleButton):
 
+                self.commitData.emit(w)
 
-        self.mTemporalProfilesTableConfig = config
-        self.mTemporalProfileLayer.setAttributeTableConfig(self.mTemporalProfilesTableConfig)
+    def style(self, proxyIndex:QModelIndex)->PlotStyle:
+        model = self.plotSettingsModel()
+        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
+        return model.data(index, role=Qt.UserRole)
 
-        self.pixelLoader = PixelLoader()
-        self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded)
-        self.pixelLoader.sigLoadingStarted.connect(lambda: self.ui.progressInfo.setText('Start loading...'))
-        # self.pixelLoader.sigLoadingStarted.connect(self.tpCollection.prune)
-        self.pixelLoader.sigLoadingFinished.connect(lambda: self.plot2D.enableAutoRange('x', False))
+    def setEditorData(self, editor, proxyIndex):
 
-        # set the plot models for 2D
-        self.plotSettingsModel2D = PlotSettingsModel2D()
-        self.plotSettingsModel2DProxy = QSortFilterProxyModel()
-        self.plotSettingsModel2DProxy.setSourceModel(self.plotSettingsModel2D)
-        self.ui.tableView2DProfiles.setModel(self.plotSettingsModel2DProxy)
-        self.ui.tableView2DProfiles.setSortingEnabled(True)
-        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged)
-        self.ui.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
-        self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate)
-        self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D)
-        self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.ui.tableView2DProfiles, self.mTemporalProfileLayer)
-        self.delegateTableView2D.setItemDelegates(self.ui.tableView2DProfiles)
+        model = self.plotSettingsModel()
+        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
+            cname = self.getColumnName(proxyIndex)
+            if cname == model.cnExpression:
+                lastExpr = model.data(index, Qt.DisplayRole)
+                assert isinstance(editor, QgsFieldExpressionWidget)
+                editor.setProperty('lastexpr', lastExpr)
+                editor.setField(lastExpr)
 
-        # set the plot models for 3D
-        self.plotSettingsModel3D = PlotSettingsModel3D()
-        self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D)
-        self.ui.tableView3DProfiles.setSortingEnabled(False)
-        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot3DSelectionChanged)
-        self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
-        self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D)
-        self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles, self.mTemporalProfileLayer)
-        self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles)
+            elif cname == model.cnStyle:
+                style = model.data(index, Qt.UserRole)
+                assert isinstance(editor, PlotStyleButton)
+                editor.setPlotStyle(style)
 
-        if not ENABLE_OPENGL:
-            self.ui.listWidget.item(1).setHidden(True)
-            self.ui.page3D.setHidden(True)
+            elif cname == model.cnSensor:
+                assert isinstance(editor, QComboBox)
+                m = editor.model()
+                assert isinstance(m, SensorListModel)
+                sensor = index.data(role=Qt.UserRole)
+                if isinstance(sensor, SensorInstrument):
+                    idx = m.sensor2idx(sensor)
+                    editor.setCurrentIndex(idx.row())
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(editor, QgsFeatureListComboBox)
+                value = editor.identifierValue()
+                if value != QVariant():
+                    plotStyle = index.data(role=Qt.UserRole)
+                    TP = plotStyle.temporalProfile()
+                    editor.setIdentifierValue(TP.id())
+                else:
+                    s  = ""
+            else:
+                raise NotImplementedError()
 
+    def setModelData(self, w, model, proxyIndex):
+        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
+        cname = self.getColumnName(proxyIndex)
+        model = self.plotSettingsModel()
 
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
+            if cname == model.cnExpression:
+                assert isinstance(w, QgsFieldExpressionWidget)
+                expr = w.asExpression()
+                exprLast = model.data(index, Qt.DisplayRole)
 
-        def onTemporalProfilesRemoved(removedProfiles):
-            #set to valid temporal profile
+                if w.isValidExpression() and expr != exprLast:
+                    model.setData(index, w.asExpression(), Qt.EditRole)
 
-            affectedStyles2D = [p for p in self.plotSettingsModel2D if p.temporalProfile() in removedProfiles]
-            affectedStyles3D = [p for p in self.plotSettingsModel3D if p.temporalProfile() in removedProfiles]
-            alternativeProfile = self.mTemporalProfileLayer[-1] if len(self.mTemporalProfileLayer) > 0 else None
-            for s in affectedStyles2D:
-                assert isinstance(s, TemporalProfile2DPlotStyle)
-                m = self.plotSettingsModel2D
-                idx = m.plotStyle2idx(s)
-                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
-                m.setData(idx, alternativeProfile, Qt.EditRole)
+            elif cname == model.cnStyle:
+                if isinstance(w, PlotStyleButton):
+                    style = w.plotStyle()
+                    model.setData(index, style, Qt.EditRole)
 
-            for s in affectedStyles3D:
-                assert isinstance(s, TemporalProfile3DPlotStyle)
-                m = self.plotSettingsModel3D
-                idx = m.plotStyle2idx(s)
-                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
-                m.setData(idx, alternativeProfile, Qt.EditRole)
+            elif cname == model.cnSensor:
+                assert isinstance(w, QComboBox)
+                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
+                if isinstance(sensor, SensorInstrument):
+                    model.setData(index, sensor, Qt.EditRole)
 
-        self.mTemporalProfileLayer.sigTemporalProfilesRemoved.connect(onTemporalProfilesRemoved)
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(w, QgsFeatureListComboBox)
+                fid = w.identifierValue()
+                if isinstance(fid, int):
+                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
+                    model.setData(index, TP, Qt.EditRole)
 
-        # remove / add plot style
-        def on2DPlotStyleRemoved(plotStyles):
-            for plotStyle in plotStyles:
-                assert isinstance(plotStyle, PlotStyle)
-                for pi in plotStyle.mPlotItems:
-                    self.plot2D.plotItem.removeItem(pi)
+            else:
+                raise NotImplementedError()
 
-        def on3DPlotStyleRemoved(plotStyles):
-            toRemove = []
-            for plotStyle in plotStyles:
-                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
-                toRemove.append(plotStyle.mPlotItems)
-            self.plot3D.removeItems(toRemove)
 
 
-        self.plotSettingsModel2D.sigPlotStylesRemoved.connect(on2DPlotStyleRemoved)
-        self.plotSettingsModel3D.sigPlotStylesRemoved.connect(on3DPlotStyleRemoved)
+class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate):
+    """
 
-        # initialize the update loop
-        self.updateRequested = True
-        self.updateTimer = QTimer(self)
-        self.updateTimer.timeout.connect(self.onDataUpdate)
-        self.updateTimer.start(2000)
+    """
+    def __init__(self, tableView, temporalProfileLayer, parent=None):
+        assert isinstance(tableView, QTableView)
+        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
+        super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent)
+        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
+        self.mTableView = tableView
+        self.mTimeSeries = temporalProfileLayer.timeSeries()
+        self.mTemporalProfileLayer = temporalProfileLayer
+        self.mSensorLayers = {}
 
-        self.sigMoveToDate.connect(self.onMoveToDate)
+    def setItemDelegates(self, tableView):
+        assert isinstance(tableView, QTableView)
+        model = tableView.model()
+        assert isinstance(model, PlotSettingsModel3D)
+        for c in [model.cnSensor,
+                  model.cnExpression,
+                  # model.cnStyle,
+                  model.cnTemporalProfile]:
+            i = model.columnNames.index(c)
+            tableView.setItemDelegateForColumn(i, self)
 
-        self.initActions()
-        #self.ui.stackedWidget.setCurrentPage(self.ui.pagePixel)
-        self.ui.onStackPageChanged(self.ui.stackedWidget.currentIndex())
+    def getColumnName(self, index):
+        assert index.isValid()
+        model = index.model()
+        assert isinstance(model, PlotSettingsModel3D)
+        return model.columnNames[index.column()]
+    """
+    def sizeHint(self, options, index):
+        s = super(ExpressionDelegate, self).sizeHint(options, index)
+        exprString = self.tableView.model().data(index)
+        l = QLabel()
+        l.setText(exprString)
+        x = l.sizeHint().width() + 100
+        s = QSize(x, s.height())
+        return self._preferedSize
+    """
+    def createEditor(self, parent, option, index):
+        cname = self.getColumnName(index)
+        model = self.mTableView.model()
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            plotStyle = model.idx2plotStyle(index)
+            if isinstance(plotStyle, TemporalProfile3DPlotStyle):
+                if cname == model.cnExpression:
+                    w = QgsFieldExpressionWidget(parent=parent)
+                    w.setExpression(plotStyle.expression())
+                    w.setLayer(self.exampleLyr(plotStyle.sensor()))
+                    def onSensorAdded(s):
+                        w.setLayer(self.exampleLyr(s))
+                    #plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s)))
+                    plotStyle.sigSensorChanged.connect(onSensorAdded)
+                    w.setExpressionDialogTitle('Values')
+                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
+                    w.fieldChanged[str, bool].connect(lambda n, b : self.checkData(index, w, w.expression()))
 
-    def temporalProfileLayer(self)->TemporalProfileLayer:
-        """
-        Returns a QgsVectorLayer that is used to store profile coordinates.
-        :return:
-        """
-        return self.mTemporalProfileLayer
+                elif cname == model.cnStyle:
+                    w = TemporalProfile3DPlotStyleButton(parent=parent)
+                    w.setPlotStyle(plotStyle)
+                    w.setToolTip('Set plot style')
+                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
 
-    def onTemporalProfilesContextMenu(self, event):
-        assert isinstance(event, QContextMenuEvent)
-        tableView = self.ui.tableViewTemporalProfiles
-        selectionModel = self.ui.tableViewTemporalProfiles.selectionModel()
-        assert isinstance(selectionModel, QItemSelectionModel)
+                elif cname == model.cnSensor:
+                    w = QComboBox(parent=parent)
+                    m = SensorListModel(self.mTimeSeries)
+                    w.setModel(m)
 
-        model = self.ui.tableViewTemporalProfiles.model()
-        assert isinstance(model, TemporalProfileLayer)
+                elif cname == model.cnTemporalProfile:
+                    w = QgsFeatureListComboBox(parent=parent)
+                    w.setSourceLayer(self.mTemporalProfileLayer)
+                    w.setIdentifierField('id')
+                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
+                    w.setAllowNull(False)
+                else:
+                    raise NotImplementedError()
+        return w
 
-        temporalProfiles = []
+    def exampleLyr(self, sensor):
 
-        if len(selectionModel.selectedIndexes()) > 0:
-            for idx in selectionModel.selectedIndexes():
-                tp = model.idx2tp(idx)
-                if isinstance(tp, TemporalProfile) and not tp in temporalProfiles:
-                    temporalProfiles.append(tp)
-        else:
-            temporalProfiles = model[:]
+        if sensor not in self.mSensorLayers.keys():
+            crs = QgsCoordinateReferenceSystem('EPSG:4862')
+            uri = 'Point?crs={}'.format(crs.authid())
+            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
+            assert sensor is None or isinstance(sensor, SensorInstrument)
 
-        spatialPoints = [tp.coordinate() for tp in temporalProfiles]
+            f = sensorExampleQgsFeature(sensor, singleBandOnly=True)
+            assert isinstance(f, QgsFeature)
+            assert lyr.startEditing()
+            for field in f.fields():
+                lyr.addAttribute(field)
+            lyr.addFeature(f)
+            lyr.commitChanges()
+            self.mSensorLayers[sensor] = lyr
+        return self.mSensorLayers[sensor]
 
+    def checkData(self, index, w, value):
+        assert isinstance(index, QModelIndex)
+        model = self.mTableView.model()
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            plotStyle = model.idx2plotStyle(index)
+            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+            if isinstance(w, QgsFieldExpressionWidget):
+                assert value == w.expression()
+                assert w.isExpressionValid(value) == w.isValidExpression()
 
-        menu = QMenu()
+                if w.isValidExpression():
+                    self.commitData.emit(w)
+                else:
+                    s = ""
+                    #print(('Delegate commit failed',w.asExpression()))
+            if isinstance(w, TemporalProfile3DPlotStyleButton):
 
-        a = menu.addAction('Load missing')
-        a.setToolTip('Loads missing band-pixels.')
-        a.triggered.connect(lambda : self.loadCoordinate(spatialPoints=spatialPoints, mode='all'))
-        s = ""
+                self.commitData.emit(w)
 
-        a = menu.addAction('Reload')
-        a.setToolTip('Reloads all band-pixels.')
-        a.triggered.connect(lambda: self.loadCoordinate(spatialPoints=spatialPoints, mode='reload'))
 
-        menu.popup(tableView.viewport().mapToGlobal(event.pos()))
-        self.menu = menu
+    def setEditorData(self, editor, index):
+        cname = self.getColumnName(index)
+        model = self.mTableView.model()
 
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            cname = self.getColumnName(index)
+            style = model.idx2plotStyle(index)
 
-    def selected2DPlotStyles(self):
-        result = []
+            if cname == model.cnExpression:
+                lastExpr = index.model().data(index, Qt.DisplayRole)
+                assert isinstance(editor, QgsFieldExpressionWidget)
+                editor.setProperty('lastexpr', lastExpr)
+                editor.setField(lastExpr)
 
-        m = self.ui.tableView2DProfiles.model()
-        for idx in selectedModelIndices(self.ui.tableView2DProfiles):
-            result.append(m.data(idx, Qt.UserRole))
-        return result
+            elif cname == model.cnStyle:
+                assert isinstance(editor, TemporalProfile3DPlotStyleButton)
+                editor.setPlotStyle(style)
 
+            elif cname == model.cnSensor:
+                assert isinstance(editor, QComboBox)
+                m = editor.model()
+                assert isinstance(m, SensorListModel)
+                sensor = index.data(role=Qt.UserRole)
+                if isinstance(sensor, SensorInstrument):
+                    idx = m.sensor2idx(sensor)
+                    editor.setCurrentIndex(idx.row())
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(editor, QgsFeatureListComboBox)
+                value = editor.identifierValue()
+                if value != QVariant():
+                    plotStyle = index.data(role=Qt.UserRole)
+                    TP = plotStyle.temporalProfile()
+                    editor.setIdentifierValue(TP.id())
+                else:
+                    s  = ""
 
+            else:
+                raise NotImplementedError()
 
-    def removePlotStyles2D(self, plotStyles):
-        m = self.ui.tableView2DProfiles.model()
-        if isinstance(m.sourceModel(), PlotSettingsModel2D):
-            m.sourceModel().removePlotStyles(plotStyles)
+    def setModelData(self, w, model, index):
+        cname = self.getColumnName(index)
+        model = self.mTableView.model()
+
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            if cname == model.cnExpression:
+                assert isinstance(w, QgsFieldExpressionWidget)
+                expr = w.asExpression()
+                exprLast = model.data(index, Qt.DisplayRole)
+
+                if w.isValidExpression() and expr != exprLast:
+                    model.setData(index, w.asExpression(), Qt.EditRole)
 
-    def removeTemporalProfiles(self, fids):
+            elif cname == model.cnStyle:
+                assert isinstance(w, TemporalProfile3DPlotStyleButton)
+                model.setData(index, w.plotStyle(), Qt.EditRole)
 
-        self.mTemporalProfileLayer.selectByIds(fids)
-        b = self.mTemporalProfileLayer.isEditable()
-        self.mTemporalProfileLayer.startEditing()
-        self.mTemporalProfileLayer.deleteSelectedFeatures()
-        self.mTemporalProfileLayer.saveEdits(leaveEditable=b)
+            elif cname == model.cnSensor:
+                assert isinstance(w, QComboBox)
+                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
+                assert isinstance(sensor, SensorInstrument)
+                model.setData(index, sensor, Qt.EditRole)
 
-    def createNewPlotStyle2D(self):
-        l = len(self.mTemporalProfileLayer)
+                s = ""
 
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(w, QgsFeatureListComboBox)
+                fid = w.identifierValue()
+                if isinstance(fid, int):
+                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
+                    model.setData(index, TP, Qt.EditRole)
 
-        plotStyle = TemporalProfile2DPlotStyle()
-        plotStyle.sigExpressionUpdated.connect(self.updatePlot2D)
+            else:
+                raise NotImplementedError()
 
-        sensors = self.TS.sensors()
-        if len(sensors) > 0:
-            plotStyle.setSensor(sensors[0])
 
-        if len(self.mTemporalProfileLayer) > 0:
-            temporalProfile = self.mTemporalProfileLayer[0]
-            plotStyle.setTemporalProfile(temporalProfile)
 
+class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')):
 
-        if len(self.plotSettingsModel2D) > 0:
-            lastStyle = self.plotSettingsModel2D[0] #top style in list is the most-recent
-            assert isinstance(lastStyle, TemporalProfile2DPlotStyle)
-            markerColor = nextColor(lastStyle.markerBrush.color())
-            plotStyle.markerBrush.setColor(markerColor)
-        self.plotSettingsModel2D.insertPlotStyles([plotStyle], i=0)
-        pdi = plotStyle.createPlotItem(self.plot2D)
 
-        assert isinstance(pdi, TemporalProfilePlotDataItem)
-        pdi.sigClicked.connect(self.onProfileClicked2D)
-        pdi.sigPointsClicked.connect(self.onPointsClicked2D)
-        self.plot2D.plotItem.addItem(pdi)
-        #self.plot2D.getPlotItem().addItem(pg.PlotDataItem(x=[1, 2, 3], y=[1, 2, 3]))
-        #plotItem.addDataItem(pdi)
-        #plotItem.plot().sigPlotChanged.emit(plotItem)
-        self.updatePlot2D()
-        return plotStyle
+    def __init__(self, parent=None):
+        super(ProfileViewDockUI, self).__init__(parent)
+        self.setupUi(self)
 
+        self.addActions(self.findChildren(QAction))
 
-    def createNewPlotStyle3D(self):
-        if not (ENABLE_OPENGL and OPENGL_AVAILABLE):
-            return
+        self.mActions2D = [self.actionAddStyle2D, self.actionRemoveStyle2D, self.actionRefresh2D, self.actionReset2DPlot]
+        self.mActions3D = [self.actionAddStyle3D, self.actionRemoveStyle3D, self.actionRefresh3D,
+                           self.actionReset3DCamera]
+        self.mActionsTP = [self.actionLoadTPFromOgr, self.actionSaveTemporalProfiles, self.actionToggleEditing,
+                           self.actionRemoveTemporalProfile, self.actionLoadMissingValues]
+        #TBD.
+        #self.line.setVisible(False)
+        #self.listWidget.setVisible(False)
+        self.baseTitle = self.windowTitle()
+        self.stackedWidget.currentChanged.connect(self.onStackPageChanged)
 
 
-        plotStyle = TemporalProfile3DPlotStyle()
-        plotStyle.sigExpressionUpdated.connect(self.updatePlot3D)
+        self.plotWidget3D = None
+        self.plotWidget3DMPL = None
 
-        if len(self.mTemporalProfileLayer) > 0:
-            temporalProfile = self.mTemporalProfileLayer[0]
-            plotStyle.setTemporalProfile(temporalProfile)
-            if len(self.plotSettingsModel3D) > 0:
-                color = self.plotSettingsModel3D[-1].color()
-                plotStyle.setColor(nextColor(color))
+        self.init3DWidgets('gl')
 
-        sensors = list(self.TS.mSensors2TSDs.keys())
-        if len(sensors) > 0:
-            plotStyle.setSensor(sensors[0])
 
+        #pi = self.plotWidget2D.plotItem
+        #ax = DateAxis(orientation='bottom', showValues=True)
+        #pi.layout.addItem(ax, 3,2)
 
-        self.plotSettingsModel3D.insertPlotStyles([plotStyle], i=0) # latest to the top
-        plotItems = plotStyle.createPlotItem()
-        self.plot3D.addItems(plotItems)
-        #self.updatePlot3D()
 
-    def onProfileClicked2D(self, pdi):
-        if isinstance(pdi, TemporalProfilePlotDataItem):
-            sensor = pdi.mPlotStyle.sensor()
-            tp = pdi.mPlotStyle.temporalProfile()
-            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
-                c = tp.coordinate()
-                info = ['Sensor:{}'.format(sensor.name()),
-                        'Coordinate:{}, {}'.format(c.x(), c.y())]
-                self.ui.tbInfo2D.setPlainText('\n'.join(info))
+        self.TS = None
 
+        self.progressBar.setMinimum(0)
+        self.progressBar.setMaximum(100)
+        self.progressBar.setValue(0)
+        self.progressInfo.setText('')
+        self.pxViewModel2D = None
+        self.pxViewModel3D = None
 
-    def onPointsClicked2D(self, pdi, spottedItems):
-        if isinstance(pdi, TemporalProfilePlotDataItem) and isinstance(spottedItems, list):
-            sensor = pdi.mPlotStyle.sensor()
-            tp = pdi.mPlotStyle.temporalProfile()
-            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
-                c = tp.coordinate()
-                info = ['Sensor: {}'.format(sensor.name()),
-                        'Coordinate: {}, {}'.format(c.x(), c.y())]
+        self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
 
-                for item in spottedItems:
-                    pos = item.pos()
-                    x = pos.x()
-                    y = pos.y()
-                    date = num2date(x)
-                    info.append('Date: {}\nValue: {}'.format(date, y))
+    def init3DWidgets(self, mode='gl'):
+        assert mode in ['gl']
+        l = self.frame3DPlot.layout()
 
-                self.ui.tbInfo2D.setPlainText('\n'.join(info))
+        if ENABLE_OPENGL and OPENGL_AVAILABLE and mode == 'gl':
 
-    def onTemporalProfilesAdded(self, profiles):
-        # self.mTemporalProfileLayer.prune()
-        for plotStyle in self.plotSettingsModel3D:
-            assert isinstance(plotStyle, TemporalProfilePlotStyleBase)
-            if not isinstance(plotStyle.temporalProfile(), TemporalProfile):
+            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
+            self.plotWidget3D = ViewWidget3D(parent=self.labelDummy3D.parent())
+            self.plotWidget3D.setObjectName('plotWidget3D')
 
-                r = self.plotSettingsModel3D.plotStyle2idx(plotStyle).row()
-                c = self.plotSettingsModel3D.columnIndex(self.plotSettingsModel3D.cnTemporalProfile)
-                idx = self.plotSettingsModel3D.createIndex(r, c)
-                self.plotSettingsModel3D.setData(idx, self.mTemporalProfileLayer[0])
+            size = self.labelDummy3D.size()
+            l.addWidget(self.plotWidget3D)
+            self.plotWidget3D.setSizePolicy(self.labelDummy3D.sizePolicy())
+            self.labelDummy3D.setVisible(False)
+            l.removeWidget(self.labelDummy3D)
+            #self.plotWidget3D.setBaseSize(size)
+            self.splitter3D.setSizes([100, 100])
+            self.frameSettings3D.setEnabled(True)
+        else:
+            self.frameSettings3D.setEnabled(False)
 
-    def onTemporalProfileSelectionChanged(self, selectedFIDs, deselectedFIDs):
-        nSelected = len(selectedFIDs)
+    def onStackPageChanged(self, i):
+        w = self.stackedWidget.currentWidget()
+        title = self.baseTitle
+        if w == self.page2D:
+            title = '{} | 2D'.format(title)
+            for a in self.mActions2D:
+                a.setVisible(True)
+            for a in self.mActions3D + self.mActionsTP:
+                a.setVisible(False)
+        elif w == self.page3D:
+            title = '{} | 3D (experimental!)'.format(title)
+            for a in self.mActions2D + self.mActionsTP:
+                a.setVisible(False)
+            for a in self.mActions3D:
+                a.setVisible(True)
+        elif w == self.pagePixel:
+            title = '{} | Coordinates'.format(title)
+            for a in self.mActions2D + self.mActions3D:
+                a.setVisible(False)
+            for a in self.mActionsTP:
+                a.setVisible(True)
 
-        self.ui.actionRemoveTemporalProfile.setEnabled(nSelected > 0)
+        w.update()
+        self.setWindowTitle(title)
 
 
-    def onPlot2DSelectionChanged(self, selected, deselected):
 
-        self.ui.actionRemoveStyle2D.setEnabled(len(selected) > 0)
 
-    def onPlot3DSelectionChanged(self, selected, deselected):
+class SpectralTemporalVisualization(QObject):
 
-        self.ui.actionRemoveStyle3D.setEnabled(len(selected) > 0)
+    sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem)
 
-    def initActions(self):
+    """
+    Signalizes to move to specific date of interest
+    """
+    sigMoveToDate = pyqtSignal(np.datetime64)
 
-        self.ui.actionRemoveStyle2D.setEnabled(False)
-        self.ui.actionRemoveTemporalProfile.setEnabled(False)
-        self.ui.actionAddStyle2D.triggered.connect(self.createNewPlotStyle2D)
-        self.ui.actionAddStyle3D.triggered.connect(self.createNewPlotStyle3D)
-        self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D)
-        self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D)
-        self.ui.actionRemoveStyle2D.triggered.connect(lambda:self.removePlotStyles2D(self.selected2DPlotStyles()))
-        self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.mTemporalProfileLayer.selectedFeatureIds()))
-        self.ui.actionToggleEditing.triggered.connect(self.onToggleEditing)
-        self.ui.actionReset2DPlot.triggered.connect(self.plot2D.resetViewBox)
-        self.plot2D.resetTransform()
-        self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera)
-        self.ui.actionLoadTPFromOgr.triggered.connect(lambda : self.mTemporalProfileLayer.loadCoordinatesFromOgr(None))
-        self.ui.actionLoadMissingValues.triggered.connect(lambda: self.loadMissingData())
-        self.ui.actionSaveTemporalProfiles.triggered.connect(lambda *args : self.mTemporalProfileLayer.saveTemporalProfiles)
-        #set actions to be shown in the TemporalProfileTableView context menu
-        ma = [self.ui.actionSaveTemporalProfiles, self.ui.actionLoadMissingValues]
 
+    def __init__(self, timeSeries, profileDock):
+        super(SpectralTemporalVisualization, self).__init__()
 
-        self.onEditingToggled()
+        assert isinstance(profileDock, ProfileViewDockUI)
+        self.ui = profileDock
 
-    def onSaveTemporalProfiles(self):
+        import eotimeseriesviewer.pixelloader
+        if DEBUG:
+            eotimeseriesviewer.pixelloader.DEBUG = True
 
-        s = ""
+        #the timeseries. will be set later
+        assert isinstance(timeSeries, TimeSeries)
+        self.TS = timeSeries
+        self.plot_initialized = False
 
-    def onToggleEditing(self, b):
+        self.plot2D = self.ui.plotWidget2D
+        self.plot2D.getViewBox().sigMoveToDate.connect(self.sigMoveToDate)
+        self.plot3D = self.ui.plotWidget3D
 
-        if self.mTemporalProfileLayer.isEditable():
-            self.mTemporalProfileLayer.saveEdits(leaveEditable=False)
-        else:
-            self.mTemporalProfileLayer.startEditing()
-        self.onEditingToggled()
+        # temporal profile collection to store loaded values
+        self.mTemporalProfileLayer = TemporalProfileLayer(self.TS)
+        self.mTemporalProfileLayer.sigTemporalProfilesAdded.connect(self.onTemporalProfilesAdded)
+        #self.mTemporalProfileLayer.startEditing()
+        self.mTemporalProfileLayer.selectionChanged.connect(self.onTemporalProfileSelectionChanged)
 
-    def onEditingToggled(self):
-        lyr = self.mTemporalProfileLayer
+        # fix to not loose C++ reference on temporal profile layer in case it is removed from QGIS mapcanvas
+        self.mMapCanvas = QgsMapCanvas()
+        self.mMapCanvas.setVisible(False)
+        self.mMapCanvas.setLayers([self.mTemporalProfileLayer])
+        # self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection)
 
-        hasSelectedFeatures = lyr.selectedFeatureCount() > 0
-        isEditable = lyr.isEditable()
-        self.ui.actionToggleEditing.blockSignals(True)
-        self.ui.actionToggleEditing.setChecked(isEditable)
-        #self.actionSaveTemporalProfiles.setEnabled(isEditable)
-        #self.actionReload.setEnabled(not isEditable)
-        self.ui.actionToggleEditing.blockSignals(False)
+        assert isinstance(self.ui.mDualView, QgsDualView)
+        self.ui.mDualView.init(self.mTemporalProfileLayer, self.mMapCanvas)
+        self.ui.mDualView.setView(QgsDualView.AttributeTable)
+        # pixel loader to load pixel values in parallel
+        config = QgsAttributeTableConfig()
+        config.update(self.mTemporalProfileLayer.fields())
+        config.setActionWidgetVisible(False)
 
-        #self.actionAddAttribute.setEnabled(isEditable)
-        #self.actionRemoveAttribute.setEnabled(isEditable)
-        self.ui.actionRemoveTemporalProfile.setEnabled(isEditable and hasSelectedFeatures)
-        #self.actionPasteFeatures.setEnabled(isEditable)
-        self.ui.actionToggleEditing.setEnabled(not lyr.readOnly())
+        hidden = []
+        for i, columnConfig in enumerate(config.columns()):
+
+            assert isinstance(columnConfig, QgsAttributeTableConfig.ColumnConfig)
+            config.setColumnHidden(i, columnConfig.name in hidden)
 
 
+        self.mTemporalProfilesTableConfig = config
+        self.mTemporalProfileLayer.setAttributeTableConfig(self.mTemporalProfilesTableConfig)
 
+        self.pixelLoader = PixelLoader()
+        self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded)
+        self.pixelLoader.sigLoadingStarted.connect(lambda: self.ui.progressInfo.setText('Start loading...'))
+        # self.pixelLoader.sigLoadingStarted.connect(self.tpCollection.prune)
+        self.pixelLoader.sigLoadingFinished.connect(lambda: self.plot2D.enableAutoRange('x', False))
 
-    def reset3DCamera(self, *args):
+        # set the plot models for 2D
+        self.plotSettingsModel2D = PlotSettingsModel2D()
+        self.plotSettingsModel2DProxy = QSortFilterProxyModel()
+        self.plotSettingsModel2DProxy.setSourceModel(self.plotSettingsModel2D)
+        self.ui.tableView2DProfiles.setModel(self.plotSettingsModel2DProxy)
+        self.ui.tableView2DProfiles.setSortingEnabled(True)
+        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged)
+        self.ui.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
+        self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate)
+        self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D)
+        self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.ui.tableView2DProfiles, self.mTemporalProfileLayer)
+        self.delegateTableView2D.setItemDelegates(self.ui.tableView2DProfiles)
+        #self.ui.tableView2DProfiles.horizontalHeader().sectionResized.connect(self.on2DSettingsTableColumnResize)
 
-        if ENABLE_OPENGL and OPENGL_AVAILABLE:
-            self.ui.actionReset3DCamera.trigger()
+        # set the plot models for 3D
+        self.plotSettingsModel3D = PlotSettingsModel3D()
+        self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D)
+        self.ui.tableView3DProfiles.setSortingEnabled(False)
+        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot3DSelectionChanged)
+        self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
+        self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D)
+        self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles, self.mTemporalProfileLayer)
+        self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles)
 
+        if not ENABLE_OPENGL:
+            self.ui.listWidget.item(1).setHidden(True)
+            self.ui.page3D.setHidden(True)
 
 
-    sigMoveToTSD = pyqtSignal(TimeSeriesDatum)
 
-    def onMoveToDate(self, date):
-        dt = np.asarray([np.abs(tsd.date() - date) for tsd in self.TS])
-        i = np.argmin(dt)
-        self.sigMoveToTSD.emit(self.TS[i])
+        def onTemporalProfilesRemoved(removedProfiles):
+            #set to valid temporal profile
 
+            affectedStyles2D = [p for p in self.plotSettingsModel2D if p.temporalProfile() in removedProfiles]
+            affectedStyles3D = [p for p in self.plotSettingsModel3D if p.temporalProfile() in removedProfiles]
+            alternativeProfile = self.mTemporalProfileLayer[-1] if len(self.mTemporalProfileLayer) > 0 else None
+            for s in affectedStyles2D:
+                assert isinstance(s, TemporalProfile2DPlotStyle)
+                m = self.plotSettingsModel2D
+                idx = m.plotStyle2idx(s)
+                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
+                m.setData(idx, alternativeProfile, Qt.EditRole)
 
-    def onPixelLoaded(self, d):
+            for s in affectedStyles3D:
+                assert isinstance(s, TemporalProfile3DPlotStyle)
+                m = self.plotSettingsModel3D
+                idx = m.plotStyle2idx(s)
+                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
+                m.setData(idx, alternativeProfile, Qt.EditRole)
 
-        if isinstance(d, PixelLoaderTask):
+        self.mTemporalProfileLayer.sigTemporalProfilesRemoved.connect(onTemporalProfilesRemoved)
 
-            bn = os.path.basename(d.sourcePath)
-            if d.success():
+        # remove / add plot style
+        def on2DPlotStyleRemoved(plotStyles):
+            for plotStyle in plotStyles:
+                assert isinstance(plotStyle, PlotStyle)
+                for pi in plotStyle.mPlotItems:
+                    self.plot2D.plotItem.removeItem(pi)
 
-                t = 'Loaded {} pixel from {}.'.format(len(d.resProfiles), bn)
-                self.mTemporalProfileLayer.addPixelLoaderResult(d)
-                self.updateRequested = True
-            else:
-                t = 'Failed loading from {}.'.format(bn)
-                if d.info and d.info != '':
-                    t += '({})'.format(d.info)
+        def on3DPlotStyleRemoved(plotStyles):
+            toRemove = []
+            for plotStyle in plotStyles:
+                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+                toRemove.append(plotStyle.mPlotItems)
+            self.plot3D.removeItems(toRemove)
 
-            # QgsApplication.processEvents()
-            self.ui.progressInfo.setText(t)
 
-    def requestUpdate(self, *args):
+        self.plotSettingsModel2D.sigPlotStylesRemoved.connect(on2DPlotStyleRemoved)
+        self.plotSettingsModel3D.sigPlotStylesRemoved.connect(on3DPlotStyleRemoved)
+
+        # initialize the update loop
         self.updateRequested = True
-        #next time
+        self.updateTimer = QTimer(self)
+        self.updateTimer.timeout.connect(self.onDataUpdate)
+        self.updateTimer.start(2000)
 
-    def onRowsInserted2D(self, parent, start, end):
-        model = self.ui.tableView2DProfiles.model().sourceModel()
-        if isinstance(model, PlotSettingsModel2D):
-            colExpression = model.columnIndex(model.cnExpression)
-            colStyle = model.columnIndex(model.cnStyle)
-            while start <= end:
-                idxExpr = model.createIndex(start, colExpression)
-                idxStyle = model.createIndex(start, colStyle)
-                self.ui.tableView2DProfiles.openPersistentEditor(idxExpr)
-                self.ui.tableView2DProfiles.openPersistentEditor(idxStyle)
-                start += 1
+        self.sigMoveToDate.connect(self.onMoveToDate)
 
-    def onRowsInserted3D(self, parent, start, end):
-        model = self.ui.tableView3DProfiles.model()
-        if isinstance(model, PlotSettingsModel3D):
-            colExpression = model.columnIndex(model.cnExpression)
-            colStyle = model.columnIndex(model.cnStyle)
-            while start <= end:
-                idxStyle = model.createIndex(start, colStyle)
-                idxExpr = model.createIndex(start, colExpression)
-                self.ui.tableView3DProfiles.openPersistentEditor(idxStyle)
-                self.ui.tableView3DProfiles.openPersistentEditor(idxExpr)
-                start += 1
+        self.initActions()
+        #self.ui.stackedWidget.setCurrentPage(self.ui.pagePixel)
+        self.ui.onStackPageChanged(self.ui.stackedWidget.currentIndex())
 
-    def onObservationClicked(self, plotDataItem, points):
-        for p in points:
-            tsd = p.data()
-            #print(tsd)
 
+    def plotStyles(self):
+        return self.plotSettingsModel2D[:]
 
-    def loadMissingData(self, backgroundProcess=False):
+    def temporalProfileLayer(self)->TemporalProfileLayer:
         """
-        Loads all band values of collected locations that have not been loaded until now
+        Returns a QgsVectorLayer that is used to store profile coordinates.
+        :return:
         """
+        return self.mTemporalProfileLayer
 
-        fids = self.mTemporalProfileLayer.selectedFeatureIds()
-        if len(fids) == 0:
-            fids = [f.id() for f in self.mTemporalProfileLayer.getFeatures()]
+    def onTemporalProfilesContextMenu(self, event):
+        assert isinstance(event, QContextMenuEvent)
+        tableView = self.ui.tableViewTemporalProfiles
+        selectionModel = self.ui.tableViewTemporalProfiles.selectionModel()
+        assert isinstance(selectionModel, QItemSelectionModel)
 
-        tps = [self.mTemporalProfileLayer.mProfiles.get(fid) for fid in fids]
-        spatialPoints = [tp.coordinate() for tp in tps if isinstance(tp, TemporalProfile)]
-        self.loadCoordinate(spatialPoints=spatialPoints, mode='all', backgroundProcess=backgroundProcess)
+        model = self.ui.tableViewTemporalProfiles.model()
+        assert isinstance(model, TemporalProfileLayer)
 
-    LOADING_MODES = ['missing', 'reload', 'all']
-    def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None, mode='missing', backgroundProcess = True):
-        """
-        :param spatialPoints: [list-of-geometries] to load pixel values from
-        :param LUT_bandIndices: dictionary {sensor:[indices]} with band indices to be loaded per sensor
-        :param mode:
-        :return:
-        """
-        """
-        Loads a temporal profile for a single or multiple geometries.
-        :param spatialPoints: SpatialPoint | [list-of-SpatialPoints]
-        """
-        assert mode in SpectralTemporalVisualization.LOADING_MODES
+        temporalProfiles = []
 
-        if isinstance(spatialPoints, SpatialPoint):
-            spatialPoints = [spatialPoints]
+        if len(selectionModel.selectedIndexes()) > 0:
+            for idx in selectionModel.selectedIndexes():
+                tp = model.idx2tp(idx)
+                if isinstance(tp, TemporalProfile) and not tp in temporalProfiles:
+                    temporalProfiles.append(tp)
+        else:
+            temporalProfiles = model[:]
 
-        assert isinstance(spatialPoints, list)
+        spatialPoints = [tp.coordinate() for tp in temporalProfiles]
 
-        if not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
-            return False
 
-        # if not self.pixelLoader.isReadyToLoad():
-        #    return False
+        menu = QMenu()
 
-        assert isinstance(self.TS, TimeSeries)
+        a = menu.addAction('Load missing')
+        a.setToolTip('Loads missing band-pixels.')
+        a.triggered.connect(lambda : self.loadCoordinate(spatialPoints=spatialPoints, mode='all'))
+        s = ""
 
-        # Get or create the TimeSeriesProfiles which will store the loaded values
+        a = menu.addAction('Reload')
+        a.setToolTip('Reloads all band-pixels.')
+        a.triggered.connect(lambda: self.loadCoordinate(spatialPoints=spatialPoints, mode='reload'))
 
-        tasks = []
-        TPs = []
-        theGeometries = []
+        menu.popup(tableView.viewport().mapToGlobal(event.pos()))
+        self.menu = menu
 
-        # Define which (new) bands need to be loaded for each sensor
-        if LUT_bandIndices is None:
-            LUT_bandIndices = dict()
-            for sensor in self.TS.sensors():
-                if mode in ['all','reload']:
-                    LUT_bandIndices[sensor] = list(range(sensor.nb))
-                else:
-                    LUT_bandIndices[sensor] = self.plotSettingsModel2D.requiredBandsIndices(sensor)
 
-        assert isinstance(LUT_bandIndices, dict)
-        for sensor in self.TS.sensors():
-            assert sensor in LUT_bandIndices.keys()
+    def selected2DPlotStyles(self):
+        result = []
 
-        # update new / existing points
+        m = self.ui.tableView2DProfiles.model()
+        for idx in selectedModelIndices(self.ui.tableView2DProfiles):
+            result.append(m.data(idx, Qt.UserRole))
+        return result
 
-        for spatialPoint in spatialPoints:
-            assert isinstance(spatialPoint, SpatialPoint)
-            TP = self.mTemporalProfileLayer.fromSpatialPoint(spatialPoint)
 
-            # if not TP exists for this point, create an empty one
-            if not isinstance(TP, TemporalProfile):
-                TP = self.mTemporalProfileLayer.createTemporalProfiles(spatialPoint)[0]
 
-                if len(self.mTemporalProfileLayer) == 1:
-                    if len(self.plotSettingsModel2D) == 0:
-                        self.createNewPlotStyle2D()
+    def removePlotStyles2D(self, plotStyles):
+        m = self.ui.tableView2DProfiles.model()
+        if isinstance(m.sourceModel(), PlotSettingsModel2D):
+            m.sourceModel().removePlotStyles(plotStyles)
 
-                    if len(self.plotSettingsModel3D) == 0:
-                        self.createNewPlotStyle3D()
+    def removeTemporalProfiles(self, fids):
 
-            TPs.append(TP)
-            theGeometries.append(TP.coordinate())
+        self.mTemporalProfileLayer.selectByIds(fids)
+        b = self.mTemporalProfileLayer.isEditable()
+        self.mTemporalProfileLayer.startEditing()
+        self.mTemporalProfileLayer.deleteSelectedFeatures()
+        self.mTemporalProfileLayer.saveEdits(leaveEditable=b)
 
+    def createNewPlotStyle2D(self):
+        l = len(self.mTemporalProfileLayer)
 
-        TP_ids = [TP.id() for TP in TPs]
-        # each TSD is a Task
-        s = ""
-        # a Task defines which bands are to be loaded
-        for tsd in self.TS:
-            assert isinstance(tsd, TimeSeriesDatum)
 
-            # do not load from invisible TSDs
-            if not tsd.isVisible():
-                continue
+        plotStyle = TemporalProfile2DPlotStyle()
+        plotStyle.sigExpressionUpdated.connect(self.updatePlot2D)
 
-            # which bands do we need to load?
-            requiredIndices = set(LUT_bandIndices[tsd.sensor()])
-            if len(requiredIndices) == 0:
-                continue
+        sensors = self.TS.sensors()
+        if len(sensors) > 0:
+            plotStyle.setSensor(sensors[0])
 
-            if mode == 'missing':
-                missingIndices = set()
+        if len(self.mTemporalProfileLayer) > 0:
+            temporalProfile = self.mTemporalProfileLayer[0]
+            plotStyle.setTemporalProfile(temporalProfile)
 
-                for TP in TPs:
-                    assert isinstance(TP, TemporalProfile)
-                    need2load = TP.missingBandIndices(tsd, requiredIndices=requiredIndices)
-                    missingIndices = missingIndices.union(need2load)
+        if len(self.plotSettingsModel2D) > 0:
+            lastStyle = self.plotSettingsModel2D[0] #top style in list is the most-recent
+            assert isinstance(lastStyle, TemporalProfile2DPlotStyle)
+            markerColor = nextColor(lastStyle.markerBrush.color())
+            plotStyle.markerBrush.setColor(markerColor)
+        self.plotSettingsModel2D.insertPlotStyles([plotStyle], i=0)
+        pdi = plotStyle.createPlotItem(self.plot2D)
 
-                missingIndices = sorted(list(missingIndices))
-            else:
-                missingIndices = requiredIndices
+        assert isinstance(pdi, TemporalProfilePlotDataItem)
+        pdi.sigClicked.connect(self.onProfileClicked2D)
+        pdi.sigPointsClicked.connect(self.onPointsClicked2D)
+        self.plot2D.plotItem.addItem(pdi)
+        #self.plot2D.getPlotItem().addItem(pg.PlotDataItem(x=[1, 2, 3], y=[1, 2, 3]))
+        #plotItem.addDataItem(pdi)
+        #plotItem.plot().sigPlotChanged.emit(plotItem)
+        self.updatePlot2D()
+        return plotStyle
 
-            if len(missingIndices) > 0:
-                for pathImg in tsd.sourceUris():
-                    task = PixelLoaderTask(pathImg, theGeometries,
-                                       bandIndices=missingIndices,
-                                       temporalProfileIDs=TP_ids)
-                tasks.append(task)
 
-        if len(tasks) > 0:
-            aGoodDefault = 2 if len(self.TS) > 25 else 1
+    def createNewPlotStyle3D(self):
+        if not (ENABLE_OPENGL and OPENGL_AVAILABLE):
+            return
 
 
-            if DEBUG:
-                print('Start loading for {} geometries from {} sources...'.format(
-                    len(theGeometries), len(tasks)
-                ))
-            if backgroundProcess:
-                self.pixelLoader.startLoading(tasks)
-            else:
-                import eotimeseriesviewer.pixelloader
-                tasks = [PixelLoaderTask.fromDump(eotimeseriesviewer.pixelloader.doLoaderTask(None, task.toDump())) for task in tasks]
-                l = len(tasks)
-                for i, task in enumerate(tasks):
-                    self.pixelLoader.sigPixelLoaded.emit(task)
+        plotStyle = TemporalProfile3DPlotStyle()
+        plotStyle.sigExpressionUpdated.connect(self.updatePlot3D)
 
-        else:
-            if DEBUG:
-                print('Data for geometries already loaded')
+        if len(self.mTemporalProfileLayer) > 0:
+            temporalProfile = self.mTemporalProfileLayer[0]
+            plotStyle.setTemporalProfile(temporalProfile)
+            if len(self.plotSettingsModel3D) > 0:
+                color = self.plotSettingsModel3D[-1].color()
+                plotStyle.setColor(nextColor(color))
 
+        sensors = list(self.TS.mSensors2TSDs.keys())
+        if len(sensors) > 0:
+            plotStyle.setSensor(sensors[0])
 
 
-    @QtCore.pyqtSlot()
-    def onDataUpdate(self):
+        self.plotSettingsModel3D.insertPlotStyles([plotStyle], i=0) # latest to the top
+        plotItems = plotStyle.createPlotItem()
+        self.plot3D.addItems(plotItems)
+        #self.updatePlot3D()
 
-        # self.mTemporalProfileLayer.prune()
+    def onProfileClicked2D(self, pdi):
+        if isinstance(pdi, TemporalProfilePlotDataItem):
+            sensor = pdi.mPlotStyle.sensor()
+            tp = pdi.mPlotStyle.temporalProfile()
+            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
+                c = tp.coordinate()
+                info = ['Sensor:{}'.format(sensor.name()),
+                        'Coordinate:{}, {}'.format(c.x(), c.y())]
+                self.ui.tbInfo2D.setPlainText('\n'.join(info))
 
-        for plotSetting in self.plotSettingsModel2D:
-            assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
-            tp = plotSetting.temporalProfile()
-            for pdi in plotSetting.mPlotItems:
-                assert isinstance(pdi, TemporalProfilePlotDataItem)
-                pdi.updateDataAndStyle()
-            if isinstance(tp, TemporalProfile) and plotSetting.temporalProfile().updated():
-                plotSetting.temporalProfile().resetUpdatedFlag()
 
+    def onPointsClicked2D(self, pdi, spottedItems):
+        if isinstance(pdi, TemporalProfilePlotDataItem) and isinstance(spottedItems, list):
+            sensor = pdi.mPlotStyle.sensor()
+            tp = pdi.mPlotStyle.temporalProfile()
+            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
+                c = tp.coordinate()
+                info = ['Sensor: {}'.format(sensor.name()),
+                        'Coordinate: {}, {}'.format(c.x(), c.y())]
 
-        for i in self.plot2D.plotItem.dataItems:
-            i.updateItems()
+                for item in spottedItems:
+                    pos = item.pos()
+                    x = pos.x()
+                    y = pos.y()
+                    date = num2date(x)
+                    info.append('Date: {}\nValue: {}'.format(date, y))
 
-        notInit = [0, 1] == self.plot2D.plotItem.getAxis('bottom').range
-        if notInit:
-            x0 = x1 = None
-            for plotSetting in self.plotSettingsModel2D:
-                assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
-                for pdi in plotSetting.mPlotItems:
-                    assert isinstance(pdi, TemporalProfilePlotDataItem)
-                    if pdi.xData.ndim == 0 or pdi.xData.shape[0] == 0:
-                        continue
-                    if x0 is None:
-                        x0 = pdi.xData.min()
-                        x1 = pdi.xData.max()
-                    else:
-                        x0 = min(pdi.xData.min(), x0)
-                        x1 = max(pdi.xData.max(), x1)
+                self.ui.tbInfo2D.setPlainText('\n'.join(info))
 
-            if x0 is not None:
-                self.plot2D.plotItem.setXRange(x0, x1)
+    def onTemporalProfilesAdded(self, profiles):
+        # self.mTemporalProfileLayer.prune()
+        for plotStyle in self.plotSettingsModel3D:
+            assert isinstance(plotStyle, TemporalProfilePlotStyleBase)
+            if not isinstance(plotStyle.temporalProfile(), TemporalProfile):
 
-                # self.plot2D.xAxisInitialized = True
+                r = self.plotSettingsModel3D.plotStyle2idx(plotStyle).row()
+                c = self.plotSettingsModel3D.columnIndex(self.plotSettingsModel3D.cnTemporalProfile)
+                idx = self.plotSettingsModel3D.createIndex(r, c)
+                self.plotSettingsModel3D.setData(idx, self.mTemporalProfileLayer[0])
 
-    @QtCore.pyqtSlot()
-    def updatePlot3D(self):
-        if ENABLE_OPENGL and OPENGL_AVAILABLE:
+    def onTemporalProfileSelectionChanged(self, selectedFIDs, deselectedFIDs):
+        nSelected = len(selectedFIDs)
 
-            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
-            assert isinstance(self.plot3D, ViewWidget3D)
+        self.ui.actionRemoveTemporalProfile.setEnabled(nSelected > 0)
 
-            # 1. ensure that data from all bands will be loaded
-            #    new loaded values will be shown in the next updatePlot3D call
-            coordinates = []
-            allPlotItems = []
-            for plotStyle3D in self.plotSettingsModel3D:
-                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
-                if plotStyle3D.isPlotable():
-                    coordinates.append(plotStyle3D.temporalProfile().coordinate())
 
-            if len(coordinates) > 0:
-                self.loadCoordinate(coordinates, mode='all')
+    def onPlot2DSelectionChanged(self, selected, deselected):
 
-            toRemove = [item for item in self.plot3D.items if item not in allPlotItems]
-            self.plot3D.removeItems(toRemove)
-            toAdd = [item for item in allPlotItems if item not in self.plot3D.items]
-            self.plot3D.addItems(toAdd)
+        self.ui.actionRemoveStyle2D.setEnabled(len(selected) > 0)
 
+    def onPlot3DSelectionChanged(self, selected, deselected):
 
-            """
-            # 3. add new plot items
-            plotItems = []
-            for plotStyle3D in self.plotSettingsModel3D:
-                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
-                if plotStyle3D.isPlotable():
-                    items = plotStyle3D.createPlotItem(None)
-                    plotItems.extend(items)
+        self.ui.actionRemoveStyle3D.setEnabled(len(selected) > 0)
 
-            self.plot3D.addItems(plotItems)
-            self.plot3D.updateDataRanges()
-            self.plot3D.resetScaling()
-            """
+    def initActions(self):
 
-    @QtCore.pyqtSlot()
-    def updatePlot2D(self):
-        if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
+        self.ui.actionRemoveStyle2D.setEnabled(False)
+        self.ui.actionRemoveTemporalProfile.setEnabled(False)
+        self.ui.actionAddStyle2D.triggered.connect(self.createNewPlotStyle2D)
+        self.ui.actionAddStyle3D.triggered.connect(self.createNewPlotStyle3D)
+        self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D)
+        self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D)
+        self.ui.actionRemoveStyle2D.triggered.connect(lambda:self.removePlotStyles2D(self.selected2DPlotStyles()))
+        self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.mTemporalProfileLayer.selectedFeatureIds()))
+        self.ui.actionToggleEditing.triggered.connect(self.onToggleEditing)
+        self.ui.actionReset2DPlot.triggered.connect(self.plot2D.resetViewBox)
+        self.plot2D.resetTransform()
+        self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera)
+        self.ui.actionLoadTPFromOgr.triggered.connect(lambda : self.mTemporalProfileLayer.loadCoordinatesFromOgr(None))
+        self.ui.actionLoadMissingValues.triggered.connect(lambda: self.loadMissingData())
+        self.ui.actionSaveTemporalProfiles.triggered.connect(lambda *args : self.mTemporalProfileLayer.saveTemporalProfiles)
+        #set actions to be shown in the TemporalProfileTableView context menu
+        ma = [self.ui.actionSaveTemporalProfiles, self.ui.actionLoadMissingValues]
 
-            locations = set()
-            for plotStyle in self.plotSettingsModel2D:
-                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
-                if plotStyle.isPlotable():
-                    locations.add(plotStyle.temporalProfile().coordinate())
 
-                    for pdi in plotStyle.mPlotItems:
-                        assert isinstance(pdi, TemporalProfilePlotDataItem)
-                        pdi.updateDataAndStyle()
+        self.onEditingToggled()
 
-            #2. load pixel data
-            self.loadCoordinate(list(locations))
+    def onToggleEditing(self, b):
 
-            # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py
-            return
+        if self.mTemporalProfileLayer.isEditable():
+            self.mTemporalProfileLayer.saveEdits(leaveEditable=False)
+        else:
+            self.mTemporalProfileLayer.startEditing()
+        self.onEditingToggled()
 
+    def onEditingToggled(self):
+        lyr = self.mTemporalProfileLayer
 
+        hasSelectedFeatures = lyr.selectedFeatureCount() > 0
+        isEditable = lyr.isEditable()
+        self.ui.actionToggleEditing.blockSignals(True)
+        self.ui.actionToggleEditing.setChecked(isEditable)
+        #self.actionSaveTemporalProfiles.setEnabled(isEditable)
+        #self.actionReload.setEnabled(not isEditable)
+        self.ui.actionToggleEditing.blockSignals(False)
 
-class PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate):
-    """
+        #self.actionAddAttribute.setEnabled(isEditable)
+        #self.actionRemoveAttribute.setEnabled(isEditable)
+        self.ui.actionRemoveTemporalProfile.setEnabled(isEditable and hasSelectedFeatures)
+        #self.actionPasteFeatures.setEnabled(isEditable)
+        self.ui.actionToggleEditing.setEnabled(not lyr.readOnly())
 
-    """
-    def __init__(self, tableView, temporalProfileLayer, parent=None):
-        assert isinstance(tableView, QTableView)
-        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
-        super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent)
-        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
-        self.mTableView = tableView
-        self.mTemporalProfileLayer = temporalProfileLayer
-        self.mTimeSeries = temporalProfileLayer.timeSeries()
 
-        self.mSensorLayers = {}
 
 
-    def sortFilterProxyModel(self)->QSortFilterProxyModel:
-        return self.mTableView.model()
+    def reset3DCamera(self, *args):
 
-    def plotSettingsModel(self)->PlotSettingsModel2D:
-        return self.sortFilterProxyModel().sourceModel()
+        if ENABLE_OPENGL and OPENGL_AVAILABLE:
+            self.ui.actionReset3DCamera.trigger()
 
-    def setItemDelegates(self, tableView):
-        assert isinstance(tableView, QTableView)
-        model = self.plotSettingsModel()
 
-        assert isinstance(model, PlotSettingsModel2D)
-        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
-            i = model.columnNames.index(c)
-            tableView.setItemDelegateForColumn(i, self)
 
-    def getColumnName(self, index):
-        assert index.isValid()
-        model = self.plotSettingsModel()
-        assert isinstance(model, PlotSettingsModel2D)
-        return model.columnNames[index.column()]
-    """
-    def sizeHint(self, options, index):
-        s = super(ExpressionDelegate, self).sizeHint(options, index)
-        exprString = self.tableView.model().data(index)
-        l = QLabel()
-        l.setText(exprString)
-        x = l.sizeHint().width() + 100
-        s = QSize(x, s.height())
-        return self._preferedSize
-    """
-    def exampleLyr(self, sensor):
-        # if isinstance(sensor, SensorInstrument):
-        if sensor not in self.mSensorLayers.keys():
+    sigMoveToTSD = pyqtSignal(TimeSeriesDatum)
 
-            crs = QgsCoordinateReferenceSystem('EPSG:4862')
-            uri = 'Point?crs={}'.format(crs.authid())
-            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
-            f = sensorExampleQgsFeature(sensor)
-            assert isinstance(f, QgsFeature)
-            assert lyr.startEditing()
-            for field in f.fields():
-                lyr.addAttribute(field)
-            lyr.addFeature(f)
-            lyr.commitChanges()
-            self.mSensorLayers[sensor] = lyr
-        return self.mSensorLayers[sensor]
+    def onMoveToDate(self, date):
+        dt = np.asarray([np.abs(tsd.date() - date) for tsd in self.TS])
+        i = np.argmin(dt)
+        self.sigMoveToTSD.emit(self.TS[i])
 
-    def createEditor(self, parent, option, index):
-        cname = self.getColumnName(index)
-        model = self.plotSettingsModel()
-        pmodel = self.sortFilterProxyModel()
 
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
+    def onPixelLoaded(self, d):
 
-            plotStyle = model.idx2plotStyle(pmodel.mapToSource(index))
+        if isinstance(d, PixelLoaderTask):
 
-            if isinstance(plotStyle, TemporalProfile2DPlotStyle):
-                if cname == model.cnExpression:
+            bn = os.path.basename(d.sourcePath)
+            if d.success():
 
-                    w = QgsFieldExpressionWidget(parent=parent)
-                    w.setExpressionDialogTitle('Values')
-                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
-                    w.fieldChanged[str, bool].connect(lambda n, b: self.checkData(index, w, w.expression()))
-                    w.setExpression(plotStyle.expression())
-                    plotStyle.sigSensorChanged.connect(lambda s: w.setLayer(self.exampleLyr(s)))
-                    if isinstance(plotStyle.sensor(), SensorInstrument):
-                        w.setLayer(self.exampleLyr(plotStyle.sensor()))
+                t = 'Loaded {} pixel from {}.'.format(len(d.resProfiles), bn)
+                self.mTemporalProfileLayer.addPixelLoaderResult(d)
+                self.updateRequested = True
+            else:
+                t = 'Failed loading from {}.'.format(bn)
+                if d.info and d.info != '':
+                    t += '({})'.format(d.info)
 
+            # QgsApplication.processEvents()
+            self.ui.progressInfo.setText(t)
 
+    def requestUpdate(self, *args):
+        self.updateRequested = True
+        #next time
 
+    def onRowsInserted2D(self, parent, start, end):
+        model = self.ui.tableView2DProfiles.model().sourceModel()
+        if isinstance(model, PlotSettingsModel2D):
+            colExpression = model.columnIndex(model.cnExpression)
+            colStyle = model.columnIndex(model.cnStyle)
+            while start <= end:
+                idxExpr = model.createIndex(start, colExpression)
+                idxStyle = model.createIndex(start, colStyle)
+                self.ui.tableView2DProfiles.openPersistentEditor(idxExpr)
+                self.ui.tableView2DProfiles.openPersistentEditor(idxStyle)
+                start += 1
 
-                elif cname == model.cnStyle:
-                    w = PlotStyleButton(parent=parent)
-                    w.setPlotStyle(plotStyle)
-                    w.setToolTip('Set style.')
-                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
+    def onRowsInserted3D(self, parent, start, end):
+        model = self.ui.tableView3DProfiles.model()
+        if isinstance(model, PlotSettingsModel3D):
+            colExpression = model.columnIndex(model.cnExpression)
+            colStyle = model.columnIndex(model.cnStyle)
+            while start <= end:
+                idxStyle = model.createIndex(start, colStyle)
+                idxExpr = model.createIndex(start, colExpression)
+                self.ui.tableView3DProfiles.openPersistentEditor(idxStyle)
+                self.ui.tableView3DProfiles.openPersistentEditor(idxExpr)
+                start += 1
 
-                elif cname == model.cnSensor:
-                    w = QComboBox(parent=parent)
-                    m = SensorListModel(self.mTimeSeries)
-                    w.setModel(m)
+    def onObservationClicked(self, plotDataItem, points):
+        for p in points:
+            tsd = p.data()
+            #print(tsd)
 
-                elif cname == model.cnTemporalProfile:
-                    w = QgsFeatureListComboBox(parent=parent)
-                    w.setSourceLayer(self.mTemporalProfileLayer)
-                    w.setIdentifierField('id')
-                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
-                    w.setAllowNull(False)
-                else:
-                    raise NotImplementedError()
-        return w
 
-    def checkData(self, index, w, value):
-        assert isinstance(index, QModelIndex)
-        model = self.mTableView.model()
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
-            plotStyle = model.idx2plotStyle(index)
-            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
-            if isinstance(w, QgsFieldExpressionWidget):
-                assert value == w.expression()
-                assert w.isExpressionValid(value) == w.isValidExpression()
+    def loadMissingData(self, backgroundProcess=False):
+        """
+        Loads all band values of collected locations that have not been loaded until now
+        """
 
-                if w.isValidExpression():
-                    self.commitData.emit(w)
-                else:
-                    s = ""
-                    #print(('Delegate commit failed',w.asExpression()))
-            if isinstance(w, PlotStyleButton):
+        fids = self.mTemporalProfileLayer.selectedFeatureIds()
+        if len(fids) == 0:
+            fids = [f.id() for f in self.mTemporalProfileLayer.getFeatures()]
 
-                self.commitData.emit(w)
+        tps = [self.mTemporalProfileLayer.mProfiles.get(fid) for fid in fids]
+        spatialPoints = [tp.coordinate() for tp in tps if isinstance(tp, TemporalProfile)]
+        self.loadCoordinate(spatialPoints=spatialPoints, mode='all', backgroundProcess=backgroundProcess)
 
-    def setEditorData(self, editor, proxyIndex):
+    LOADING_MODES = ['missing', 'reload', 'all']
+    def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None, mode='missing', backgroundProcess = True):
+        """
+        :param spatialPoints: [list-of-geometries] to load pixel values from
+        :param LUT_bandIndices: dictionary {sensor:[indices]} with band indices to be loaded per sensor
+        :param mode:
+        :return:
+        """
+        """
+        Loads a temporal profile for a single or multiple geometries.
+        :param spatialPoints: SpatialPoint | [list-of-SpatialPoints]
+        """
+        assert mode in SpectralTemporalVisualization.LOADING_MODES
 
-        model = self.plotSettingsModel()
-        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
-            cname = self.getColumnName(proxyIndex)
-            if cname == model.cnExpression:
-                lastExpr = model.data(index, Qt.DisplayRole)
-                assert isinstance(editor, QgsFieldExpressionWidget)
-                editor.setProperty('lastexpr', lastExpr)
-                editor.setField(lastExpr)
 
-            elif cname == model.cnStyle:
-                style = index.data()
-                assert isinstance(editor, PlotStyleButton)
-                editor.setPlotStyle(style)
 
-            elif cname == model.cnSensor:
-                assert isinstance(editor, QComboBox)
-                m = editor.model()
-                assert isinstance(m, SensorListModel)
-                sensor = index.data(role=Qt.UserRole)
-                if isinstance(sensor, SensorInstrument):
-                    idx = m.sensor2idx(sensor)
-                    editor.setCurrentIndex(idx.row())
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(editor, QgsFeatureListComboBox)
-                value = editor.identifierValue()
-                if value != QVariant():
-                    plotStyle = index.data(role=Qt.UserRole)
-                    TP = plotStyle.temporalProfile()
-                    editor.setIdentifierValue(TP.id())
-                else:
-                    s  = ""
-            else:
-                raise NotImplementedError()
+        if isinstance(spatialPoints, SpatialPoint):
+            spatialPoints = [spatialPoints]
 
-    def setModelData(self, w, model, proxyIndex):
-        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
-        cname = self.getColumnName(proxyIndex)
-        model = self.plotSettingsModel()
+        assert isinstance(spatialPoints, list)
 
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
-            if cname == model.cnExpression:
-                assert isinstance(w, QgsFieldExpressionWidget)
-                expr = w.asExpression()
-                exprLast = model.data(index, Qt.DisplayRole)
+        if not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
+            return False
 
-                if w.isValidExpression() and expr != exprLast:
-                    model.setData(index, w.asExpression(), Qt.EditRole)
+        # if not self.pixelLoader.isReadyToLoad():
+        #    return False
 
-            elif cname == model.cnStyle:
-                if isinstance(w, PlotStyleButton):
-                    model.setData(index, w.plotStyle(), Qt.EditRole)
+        assert isinstance(self.TS, TimeSeries)
 
-            elif cname == model.cnSensor:
-                assert isinstance(w, QComboBox)
-                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
-                if isinstance(sensor, SensorInstrument):
-                    model.setData(index, sensor, Qt.EditRole)
+        # Get or create the TimeSeriesProfiles which will store the loaded values
 
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(w, QgsFeatureListComboBox)
-                fid = w.identifierValue()
-                if isinstance(fid, int):
-                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
-                    model.setData(index, TP, Qt.EditRole)
+        tasks = []
+        TPs = []
+        theGeometries = []
 
-            else:
-                raise NotImplementedError()
+        # Define which (new) bands need to be loaded for each sensor
+        if LUT_bandIndices is None:
+            LUT_bandIndices = dict()
+            for sensor in self.TS.sensors():
+                if mode in ['all', 'reload']:
+                    LUT_bandIndices[sensor] = list(range(sensor.nb))
+                else:
+                    LUT_bandIndices[sensor] = self.plotSettingsModel2D.requiredBandsIndices(sensor)
 
+        assert isinstance(LUT_bandIndices, dict)
+        for sensor in self.TS.sensors():
+            assert sensor in LUT_bandIndices.keys()
 
+        # update new / existing points
 
-class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate):
-    """
 
-    """
-    def __init__(self, tableView, temporalProfileLayer, parent=None):
-        assert isinstance(tableView, QTableView)
-        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
-        super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent)
-        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
-        self.mTableView = tableView
-        self.mTimeSeries = temporalProfileLayer.timeSeries()
-        self.mTemporalProfileLayer = temporalProfileLayer
-        self.mSensorLayers = {}
 
+        for spatialPoint in spatialPoints:
+            assert isinstance(spatialPoint, SpatialPoint)
+            TP = self.mTemporalProfileLayer.fromSpatialPoint(spatialPoint)
+
+            # if no TemporaProfile existed before, create an empty one
+            if not isinstance(TP, TemporalProfile):
+                TP = self.mTemporalProfileLayer.createTemporalProfiles(spatialPoint)[0]
+
+                if len(self.mTemporalProfileLayer) == 1:
+                    if len(self.plotSettingsModel2D) == 0:
+                        self.createNewPlotStyle2D()
+
+                    if len(self.plotSettingsModel3D) == 0:
+                        self.createNewPlotStyle3D()
+
+            TPs.append(TP)
+            theGeometries.append(TP.coordinate())
 
 
+        TP_ids = [TP.id() for TP in TPs]
+        # each TSD is a Task
+        s = ""
+        # a Task defines which bands are to be loaded
+        for tsd in self.TS:
+            assert isinstance(tsd, TimeSeriesDatum)
 
+            # do not load from invisible TSDs
+            if not tsd.isVisible():
+                continue
 
+            # which bands do we need to load?
+            requiredIndices = set(LUT_bandIndices[tsd.sensor()])
+            if len(requiredIndices) == 0:
+                continue
 
+            if mode == 'missing':
+                missingIndices = set()
 
-    def setItemDelegates(self, tableView):
-        assert isinstance(tableView, QTableView)
-        model = tableView.model()
-        assert isinstance(model, PlotSettingsModel3D)
-        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
-            i = model.columnNames.index(c)
-            tableView.setItemDelegateForColumn(i, self)
+                for TP in TPs:
+                    assert isinstance(TP, TemporalProfile)
+                    need2load = TP.missingBandIndices(tsd, requiredIndices=requiredIndices)
+                    missingIndices = missingIndices.union(need2load)
 
-    def getColumnName(self, index):
-        assert index.isValid()
-        model = index.model()
-        assert isinstance(model, PlotSettingsModel3D)
-        return model.columnNames[index.column()]
-    """
-    def sizeHint(self, options, index):
-        s = super(ExpressionDelegate, self).sizeHint(options, index)
-        exprString = self.tableView.model().data(index)
-        l = QLabel()
-        l.setText(exprString)
-        x = l.sizeHint().width() + 100
-        s = QSize(x, s.height())
-        return self._preferedSize
-    """
-    def createEditor(self, parent, option, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            plotStyle = model.idx2plotStyle(index)
-            if isinstance(plotStyle, TemporalProfile3DPlotStyle):
-                if cname == model.cnExpression:
-                    w = QgsFieldExpressionWidget(parent=parent)
-                    w.setExpression(plotStyle.expression())
-                    w.setLayer(self.exampleLyr(plotStyle.sensor()))
-                    def onSensorAdded(s):
-                        w.setLayer(self.exampleLyr(s))
-                    #plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s)))
-                    plotStyle.sigSensorChanged.connect(onSensorAdded)
-                    w.setExpressionDialogTitle('Values')
-                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
-                    w.fieldChanged[str,bool].connect(lambda n, b : self.checkData(index, w, w.expression()))
+                missingIndices = sorted(list(missingIndices))
+            else:
+                missingIndices = requiredIndices
 
-                elif cname == model.cnStyle:
-                    w = TemporalProfile3DPlotStyleButton(parent=parent)
-                    w.setPlotStyle(plotStyle)
-                    w.setToolTip('Set plot style')
-                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
+            if len(missingIndices) > 0:
+                for pathImg in tsd.sourceUris():
+                    task = PixelLoaderTask(pathImg, theGeometries,
+                                       bandIndices=missingIndices,
+                                       temporalProfileIDs=TP_ids)
+                tasks.append(task)
 
-                elif cname == model.cnSensor:
-                    w = QComboBox(parent=parent)
-                    m = SensorListModel(self.mTimeSeries)
-                    w.setModel(m)
+        if len(tasks) > 0:
 
-                elif cname == model.cnTemporalProfile:
-                    w = QgsFeatureListComboBox(parent=parent)
-                    w.setSourceLayer(self.mTemporalProfileLayer)
-                    w.setIdentifierField('id')
-                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
-                    w.setAllowNull(False)
-                else:
-                    raise NotImplementedError()
-        return w
+            if DEBUG:
+                print('Start loading for {} geometries from {} sources...'.format(
+                    len(theGeometries), len(tasks)
+                ))
+            if backgroundProcess:
+                self.pixelLoader.startLoading(tasks)
+            else:
+                import eotimeseriesviewer.pixelloader
+                tasks = [PixelLoaderTask.fromDump(eotimeseriesviewer.pixelloader.doLoaderTask(None, task.toDump())) for task in tasks]
+                l = len(tasks)
+                for i, task in enumerate(tasks):
+                    self.pixelLoader.sigPixelLoaded.emit(task)
 
-    def exampleLyr(self, sensor):
+        else:
+            if DEBUG:
+                print('Data for geometries already loaded')
 
-        if sensor not in self.mSensorLayers.keys():
-            crs = QgsCoordinateReferenceSystem('EPSG:4862')
-            uri = 'Point?crs={}'.format(crs.authid())
-            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
-            assert sensor is None or isinstance(sensor, SensorInstrument)
 
-            f = sensorExampleQgsFeature(sensor, singleBandOnly=True)
-            assert isinstance(f, QgsFeature)
-            assert lyr.startEditing()
-            for field in f.fields():
-                lyr.addAttribute(field)
-            lyr.addFeature(f)
-            lyr.commitChanges()
-            self.mSensorLayers[sensor] = lyr
-        return self.mSensorLayers[sensor]
 
-    def checkData(self, index, w, value):
-        assert isinstance(index, QModelIndex)
-        model = self.mTableView.model()
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            plotStyle = model.idx2plotStyle(index)
-            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
-            if isinstance(w, QgsFieldExpressionWidget):
-                assert value == w.expression()
-                assert w.isExpressionValid(value) == w.isValidExpression()
+    @QtCore.pyqtSlot()
+    def onDataUpdate(self):
 
-                if w.isValidExpression():
-                    self.commitData.emit(w)
-                else:
-                    s = ""
-                    #print(('Delegate commit failed',w.asExpression()))
-            if isinstance(w, TemporalProfile3DPlotStyleButton):
+        # self.mTemporalProfileLayer.prune()
 
-                self.commitData.emit(w)
+        for plotSetting in self.plotSettingsModel2D:
+            assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
+            tp = plotSetting.temporalProfile()
+            for pdi in plotSetting.mPlotItems:
+                assert isinstance(pdi, TemporalProfilePlotDataItem)
+                pdi.updateDataAndStyle()
+            if isinstance(tp, TemporalProfile) and plotSetting.temporalProfile().updated():
+                plotSetting.temporalProfile().resetUpdatedFlag()
 
 
-    def setEditorData(self, editor, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
+        for i in self.plot2D.plotItem.dataItems:
+            i.updateItems()
 
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            cname = self.getColumnName(index)
-            style = model.idx2plotStyle(index)
+        notInit = [0, 1] == self.plot2D.plotItem.getAxis('bottom').range
+        if notInit:
+            x0 = x1 = None
+            for plotSetting in self.plotSettingsModel2D:
+                assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
+                for pdi in plotSetting.mPlotItems:
+                    assert isinstance(pdi, TemporalProfilePlotDataItem)
+                    if pdi.xData.ndim == 0 or pdi.xData.shape[0] == 0:
+                        continue
+                    if x0 is None:
+                        x0 = pdi.xData.min()
+                        x1 = pdi.xData.max()
+                    else:
+                        x0 = min(pdi.xData.min(), x0)
+                        x1 = max(pdi.xData.max(), x1)
 
-            if cname == model.cnExpression:
-                lastExpr = index.model().data(index, Qt.DisplayRole)
-                assert isinstance(editor, QgsFieldExpressionWidget)
-                editor.setProperty('lastexpr', lastExpr)
-                editor.setField(lastExpr)
+            if x0 is not None:
+                self.plot2D.plotItem.setXRange(x0, x1)
 
-            elif cname == model.cnStyle:
-                assert isinstance(editor, TemporalProfile3DPlotStyleButton)
-                editor.setPlotStyle(style)
+                # self.plot2D.xAxisInitialized = True
 
-            elif cname == model.cnSensor:
-                assert isinstance(editor, QComboBox)
-                m = editor.model()
-                assert isinstance(m, SensorListModel)
-                sensor = index.data(role=Qt.UserRole)
-                if isinstance(sensor, SensorInstrument):
-                    idx = m.sensor2idx(sensor)
-                    editor.setCurrentIndex(idx.row())
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(editor, QgsFeatureListComboBox)
-                value = editor.identifierValue()
-                if value != QVariant():
-                    plotStyle = index.data(role=Qt.UserRole)
-                    TP = plotStyle.temporalProfile()
-                    editor.setIdentifierValue(TP.id())
-                else:
-                    s  = ""
+    @QtCore.pyqtSlot()
+    def updatePlot3D(self):
+        if ENABLE_OPENGL and OPENGL_AVAILABLE:
 
-            else:
-                raise NotImplementedError()
+            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
+            assert isinstance(self.plot3D, ViewWidget3D)
 
-    def setModelData(self, w, model, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
+            # 1. ensure that data from all bands will be loaded
+            #    new loaded values will be shown in the next updatePlot3D call
+            coordinates = []
+            allPlotItems = []
+            for plotStyle3D in self.plotSettingsModel3D:
+                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
+                if plotStyle3D.isPlotable():
+                    coordinates.append(plotStyle3D.temporalProfile().coordinate())
 
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            if cname == model.cnExpression:
-                assert isinstance(w, QgsFieldExpressionWidget)
-                expr = w.asExpression()
-                exprLast = model.data(index, Qt.DisplayRole)
+            if len(coordinates) > 0:
+                self.loadCoordinate(coordinates, mode='all')
 
-                if w.isValidExpression() and expr != exprLast:
-                    model.setData(index, w.asExpression(), Qt.EditRole)
+            toRemove = [item for item in self.plot3D.items if item not in allPlotItems]
+            self.plot3D.removeItems(toRemove)
+            toAdd = [item for item in allPlotItems if item not in self.plot3D.items]
+            self.plot3D.addItems(toAdd)
 
-            elif cname == model.cnStyle:
-                assert isinstance(w, TemporalProfile3DPlotStyleButton)
-                model.setData(index, w.plotStyle(), Qt.EditRole)
 
-            elif cname == model.cnSensor:
-                assert isinstance(w, QComboBox)
-                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
-                assert isinstance(sensor, SensorInstrument)
-                model.setData(index, sensor, Qt.EditRole)
+            """
+            # 3. add new plot items
+            plotItems = []
+            for plotStyle3D in self.plotSettingsModel3D:
+                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
+                if plotStyle3D.isPlotable():
+                    items = plotStyle3D.createPlotItem(None)
+                    plotItems.extend(items)
 
-                s = ""
+            self.plot3D.addItems(plotItems)
+            self.plot3D.updateDataRanges()
+            self.plot3D.resetScaling()
+            """
 
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(w, QgsFeatureListComboBox)
-                fid = w.identifierValue()
-                if isinstance(fid, int):
-                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
-                    model.setData(index, TP, Qt.EditRole)
+    @QtCore.pyqtSlot()
+    def updatePlot2D(self):
+        if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
 
-            else:
-                raise NotImplementedError()
+            locations = set()
+            for plotStyle in self.plotSettingsModel2D:
+                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
+                if plotStyle.isPlotable():
+                    locations.add(plotStyle.temporalProfile().coordinate())
 
+                    for pdi in plotStyle.mPlotItems:
+                        assert isinstance(pdi, TemporalProfilePlotDataItem)
+                        pdi.updateDataAndStyle()
 
+            #2. load pixel data
+            self.loadCoordinate(list(locations))
 
+            # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py
+            return
 
 
 def examplePixelLoader():
diff --git a/eotimeseriesviewer/temporalprofiles2d.py b/eotimeseriesviewer/temporalprofiles2d.py
index 1a17808a01a16c6e1d85b3b03484b7c17693a0e6..a1e68d4cb6d74b8d98e05a6f7a4aec88c1acd81a 100644
--- a/eotimeseriesviewer/temporalprofiles2d.py
+++ b/eotimeseriesviewer/temporalprofiles2d.py
@@ -46,7 +46,7 @@ OPENGL_AVAILABLE = False
 DEFAULT_SAVE_PATH = None
 DEFAULT_CRS = QgsCoordinateReferenceSystem('EPSG:4326')
 
-FN_ID = 'id'
+FN_ID = 'fid'
 FN_X = 'x'
 FN_Y = 'y'
 FN_NAME = 'name'
@@ -594,7 +594,7 @@ class TemporalProfile(QObject):
             assert isinstance(tsd, TimeSeriesDatum)
             meta = {'doy': tsd.mDOY,
                     'date': str(tsd.mDate),
-                    'nodata':False}
+                    'nodata': False}
 
             self.updateData(tsd, meta, skipStatusUpdate=True)
         #self.updateLoadingStatus()
@@ -620,9 +620,13 @@ class TemporalProfile(QObject):
     def geometry(self):
         return self.mLayer.getFeature(self.mID).geometry()
 
-    def coordinate(self):
-        x,y = self.geometry().asPoint()
-        return SpatialPoint(self.mLayer.crs(), x,y)
+    def coordinate(self)->SpatialPoint:
+        """
+        Returns the profile coordinate
+        :return:
+        """
+        x, y = self.geometry().asPoint()
+        return SpatialPoint(self.mLayer.crs(), x, y)
 
     def id(self):
         """Feature ID in connected QgsVectorLayer"""
@@ -865,7 +869,7 @@ class TemporalProfile(QObject):
         return tsd in self.mData.keys()
 
     def __repr__(self):
-        return 'TemporalProfile {}'.format(self.mID)
+        return 'TemporalProfile {} "{}"'.format(self.id(), self.name())
 
 
 class TemporalProfilePlotDataItem(pg.PlotDataItem):
@@ -1030,7 +1034,7 @@ class TemporalProfileLayer(QgsVectorLayer):
             srs.ImportFromEPSG(4326)
             co = ['GEOMETRY_NAME=geom',
                   'GEOMETRY_NULLABLE=YES',
-                  'FID=fid'
+                  'FID={}'.format(FN_ID)
                   ]
 
             lyr = dsSrc.CreateLayer(name, srs=srs, geom_type=ogr.wkbPoint, options=co)
@@ -1066,13 +1070,13 @@ class TemporalProfileLayer(QgsVectorLayer):
         self.mTimeSeries = timeSeries
         #symbol = QgsFillSymbol.createSimple({'style': 'no', 'color': 'red', 'outline_color': 'black'})
         #self.mLocations.renderer().setSymbol(symbol)
-        self.mNextID = 1
+        #self.mNextID = 1
 
         self.TS = None
         self.setName('EOTS Temporal Profiles')
         fields = QgsFields()
-        fields.append(createQgsField(FN_ID, self.mNextID))
-        fields.append(createQgsField(FN_NAME,''))
+        #fields.append(createQgsField(FN_ID, self.mNextID))
+        fields.append(createQgsField(FN_NAME, ''))
         fields.append(createQgsField(FN_X, 0.0, comment='Longitude'))
         fields.append(createQgsField(FN_Y, 0.0, comment='Latitude'))
         #fields.append(createQgsField(FN_N_TOTAL, 0, comment='Total number of band values'))
@@ -1094,8 +1098,6 @@ class TemporalProfileLayer(QgsVectorLayer):
         assert isinstance(self.mTimeSeries, TimeSeries)
 
         # Get or create the TimeSeriesProfiles which will store the loaded values
-
-
         tasks = []
 
         theGeometries = []
@@ -1226,6 +1228,8 @@ class TemporalProfileLayer(QgsVectorLayer):
         :param addedFeatures:
         :return:
         """
+        if layerID != self.id():
+            s = ""
 
         if len(addedFeatures) > 0:
 
@@ -1245,14 +1249,17 @@ class TemporalProfileLayer(QgsVectorLayer):
 
 
     def onFeaturesRemoved(self,  layerID, removedFIDs):
+        if layerID != self.id():
+            s = ""
+
         if len(removedFIDs) > 0:
 
-            toRemove = []
+            removed = []
 
             for fid in removedFIDs:
-                toRemove.append(self.mProfiles.pop(fid))
+                removed.append(self.mProfiles.pop(fid))
 
-            self.sigTemporalProfilesRemoved.emit(toRemove)
+            self.sigTemporalProfilesRemoved.emit(removed)
 
 
     def initConditionalStyles(self):
@@ -1266,49 +1273,95 @@ class TemporalProfileLayer(QgsVectorLayer):
         #styles.setRowStyles([red])
 
 
-    def createTemporalProfiles(self, coordinates)->list:
+    def createTemporalProfiles(self, coordinates, names:list=None)->list:
         """
         Creates temporal profiles
         :param coordinates:
         :return:
         """
-        if not isinstance(coordinates, list):
+
+        if isinstance(coordinates, QgsVectorLayer):
+            lyr = coordinates
+            coordinates = []
+            names = []
+            trans = QgsCoordinateTransform()
+            trans.setSourceCrs(lyr.crs())
+            trans.setDestinationCrs(self.crs())
+
+            nameField = None
+            if isinstance(names, str) and names in lyr.fields().names():
+                nameField = names
+            else:
+                for name in lyr.fields().names():
+                    if re.search('names?', name, re.I):
+                        nameField = name
+                        break
+            if nameField is None:
+                nameField = lyr.fields().names()[0]
+
+            for f in lyr.getFeatures():
+                assert isinstance(f, QgsFeature)
+                g = f.geometry()
+                if g.isEmpty():
+                    continue
+                g = g.centroid()
+                assert g.transform(trans) == 0
+                coordinates.append(SpatialPoint(self.crs(), g.asPoint()))
+                names.append(f.attribute(nameField))
+
+            del trans
+
+        elif not isinstance(coordinates, list):
             coordinates = [coordinates]
 
+        assert isinstance(coordinates, list)
+
+        if not isinstance(names, list):
+            n = self.featureCount()
+            names = []
+            for i in range(len(coordinates)):
+                names.append('Profile {}'.format(n+i+1))
+
+        assert len(coordinates) == len(names)
+
         features = []
         n = self.dataProvider().featureCount()
-        for i, coordinate in enumerate(coordinates):
+        for i, (coordinate, name) in enumerate(zip(coordinates, names)):
             assert isinstance(coordinate, SpatialPoint)
 
             f = QgsFeature(self.fields())
             f.setGeometry(QgsGeometry.fromPointXY(coordinate.toCrs(self.crs())))
-            f.setAttribute(FN_ID, self.mNextID)
-            f.setAttribute(FN_NAME, 'TP {}'.format(self.mNextID))
+            #f.setAttribute(FN_ID, self.mNextID)
+            f.setAttribute(FN_NAME, name)
             f.setAttribute(FN_X, coordinate.x())
             f.setAttribute(FN_Y, coordinate.y())
             #f.setAttribute(FN_N_LOADED_PERCENT, 0.0)
             #f.setAttribute(FN_N_LOADED, 0)
             #f.setAttribute(FN_N_TOTAL, 0)
             #f.setAttribute(FN_N_NODATA, 0)
-            self.mNextID += 1
+            #self.mNextID += 1
             features.append(f)
 
+        if len(features) == 0:
+            return []
+
         b = self.isEditable()
-        tps_before = list(self.mProfiles.values())
         self.startEditing()
+
+        newFeatures = []
+        def onFeaturesAdded(lid, fids):
+            newFeatures.extend(fids)
+
+        self.committedFeaturesAdded.connect(onFeaturesAdded)
+        self.beginEditCommand('Add {} profile locations'.format(len(features)))
         success = self.addFeatures(features)
+        self.endEditCommand()
         self.saveEdits(leaveEditable=b)
+        self.committedFeaturesAdded.disconnect(onFeaturesAdded)
 
-
-        if success:
-            assert n+len(features) == self.featureCount()
-            assert self.featureCount() == len(self.mProfiles)
-            profiles = [tp for tp in self.mProfiles.values() if tp not in tps_before]
-            #for p in profiles:
-            #    p.updateLoadingStatus()
-            return profiles
-        else:
-            return []
+        assert self.featureCount() == len(self.mProfiles)
+        profiles = [self.mProfiles[f.id()] for f in newFeatures]
+        return profiles
 
 
     def saveEdits(self, leaveEditable=False, triggerRepaint=True):
diff --git a/eotimeseriesviewer/ui/profileviewdock.ui b/eotimeseriesviewer/ui/profileviewdock.ui
index 728b14f161946aa0459b4e0f022a7da1a5720665..891a53b0b3c724267c8f16186a18c6b03a91ca4d 100644
--- a/eotimeseriesviewer/ui/profileviewdock.ui
+++ b/eotimeseriesviewer/ui/profileviewdock.ui
@@ -6,19 +6,19 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>773</width>
-    <height>235</height>
+    <width>799</width>
+    <height>433</height>
    </rect>
   </property>
   <property name="minimumSize">
    <size>
-    <width>0</width>
-    <height>0</height>
+    <width>537</width>
+    <height>235</height>
    </size>
   </property>
   <property name="maximumSize">
    <size>
-    <width>16777215</width>
+    <width>524287</width>
     <height>524287</height>
    </size>
   </property>
@@ -205,7 +205,7 @@
             <number>0</number>
            </property>
            <property name="currentIndex">
-            <number>2</number>
+            <number>0</number>
            </property>
            <widget class="QWidget" name="page2D">
             <layout class="QVBoxLayout" name="verticalLayout_3">
@@ -265,7 +265,7 @@
                   <number>0</number>
                  </property>
                  <item>
-                  <widget class="QTableView" name="tableView2DProfiles">
+                  <widget class="PlotSettingsTableView" name="tableView2DProfiles">
                    <property name="sizePolicy">
                     <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
                      <horstretch>1</horstretch>
@@ -942,6 +942,11 @@ background-color: rgb(0, 0, 0);</string>
    <extends>QGraphicsView</extends>
    <header>eotimeseriesviewer.temporalprofiles2d</header>
   </customwidget>
+  <customwidget>
+   <class>PlotSettingsTableView</class>
+   <extends>QTableView</extends>
+   <header>eotimeseriesviewer.profilevisualization</header>
+  </customwidget>
  </customwidgets>
  <resources>
   <include location="resources.qrc"/>
diff --git a/eotimeseriesviewer/ui/timeseriesdock.ui b/eotimeseriesviewer/ui/timeseriesdock.ui
index e3ccc43981adc8e67b8fe3e54d32ba803904f4fd..a0856d09d1b01ccae4c3b9aaa67d65eed3cf6579 100644
--- a/eotimeseriesviewer/ui/timeseriesdock.ui
+++ b/eotimeseriesviewer/ui/timeseriesdock.ui
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>640</width>
-    <height>198</height>
+    <height>261</height>
    </rect>
   </property>
   <property name="sizePolicy">
@@ -278,7 +278,7 @@
        <item>
         <widget class="QLabel" name="summary">
          <property name="sizePolicy">
-          <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+          <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
            <horstretch>2</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>