diff --git a/timeseriesviewer/virtualrasters.py b/timeseriesviewer/virtualrasters.py
index 441350ee0adcfd95d9371d9c8f6c06c3ee26d62f..e6c51a0cc6fa15a23ace9bb8c0a3ef076db82d29 100644
--- a/timeseriesviewer/virtualrasters.py
+++ b/timeseriesviewer/virtualrasters.py
@@ -20,40 +20,64 @@
 """
 # noinspection PyPep8Naming
 from __future__ import absolute_import
-import os, sys, re, pickle
+import os, sys, re, pickle, tempfile
+from collections import OrderedDict
 import tempfile
-from osgeo import gdal
+from osgeo import gdal, osr, ogr
 from qgis.core import *
+from qgis.gui import *
 from PyQt4.QtCore import *
 from PyQt4.QtGui import *
-from timeseriesviewer import file_search
 
-class VirtualBandInputSource(object):
-    def __init__(self, path, bandIndex):
-        self.path = os.path.normpath(path)
-        self.bandIndex = bandIndex
-        self.noData = None
+def px2geo(px, gt):
+    #see http://www.gdal.org/gdal_datamodel.html
+    gx = gt[0] + px.x()*gt[1]+px.y()*gt[2]
+    gy = gt[3] + px.x()*gt[4]+px.y()*gt[5]
+    return QgsPoint(gx,gy)
+
+
+class VRTRasterInputSourceBand(object):
+    def __init__(self, path, bandIndex, bandName=''):
+        self.mPath = os.path.normpath(path)
+        self.mBandIndex = bandIndex
+        self.mBandName = bandName
+        self.mNoData = None
         self.mVirtualBand = None
 
-    def __eq__(self, other):
-        if isinstance(other, VirtualBandInputSource):
-            return self.path == other.path and self.bandIndex == other.bandIndex
+
+    def isEqual(self, other):
+        if isinstance(other, VRTRasterInputSourceBand):
+            return self.mPath == other.mPath and self.mBandIndex == other.mBandIndex
         else:
             return False
 
+    def __reduce_ex__(self, protocol):
+
+        return self.__class__, (self.mPath, self.mBandIndex, self.mBandName), self.__getstate__()
+
+    def __getstate__(self):
+        state = self.__dict__.copy()
+        state.pop('mVirtualBand')
+        return state
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+
     def virtualBand(self):
         return self.mVirtualBand
 
 
-class VirtualBand(QObject):
-
+class VRTRasterBand(QObject):
+    sigNameChanged = pyqtSignal(str)
+    sigSourceInserted = pyqtSignal(int, VRTRasterInputSourceBand)
+    sigSourceRemoved = pyqtSignal(int, VRTRasterInputSourceBand)
     def __init__(self, name='', parent=None):
-        super(VirtualBand, self).__init__(parent)
+        super(VRTRasterBand, self).__init__(parent)
         self.sources = []
         self.mName = name
         self.mVRT = None
 
-    sigNameChanged = pyqtSignal(str)
+
     def setName(self, name):
         oldName = self.mName
         self.mName = name
@@ -64,73 +88,108 @@ class VirtualBand(QObject):
         return self.mName
 
 
-    def addSourceBand(self, path, bandIndex):
-        vBand = VirtualBandInputSource(path, bandIndex)
-        return self.insertSourceBand(len(self.sources), vBand)
 
-    sigSourceBandInserted = pyqtSignal(VirtualBandInputSource)
-    def insertSourceBand(self, index, virtualBandInputSource):
-        assert isinstance(virtualBandInputSource, VirtualBandInputSource)
+    def addSource(self, virtualBandInputSource):
+        assert isinstance(virtualBandInputSource, VRTRasterInputSourceBand)
+        self.insertSource(len(self.sources), virtualBandInputSource)
+
+    def insertSource(self, index, virtualBandInputSource):
+        assert isinstance(virtualBandInputSource, VRTRasterInputSourceBand)
         virtualBandInputSource.mVirtualBand = self
         assert index <= len(self.sources)
         self.sources.insert(index, virtualBandInputSource)
-        self.sigSourceBandInserted.emit(virtualBandInputSource)
+        self.sigSourceInserted.emit(index, virtualBandInputSource)
 
     def bandIndex(self):
-        if isinstance(self.mVRT, VirtualRasterBuilder):
-            return self.mVRT.vBands.index(self)
+        if isinstance(self.mVRT, VRTRaster):
+            return self.mVRT.mBands.index(self)
         else:
             return None
 
-    sigSourceBandRemoved = pyqtSignal(int, VirtualBandInputSource)
-    def removeSourceBand(self, bandOrIndex):
+
+    def removeSource(self, vrtRasterInputSourceBand):
         """
-        Removes a virtual band
-        :param bandOrIndex: int | VirtualBand
-        :return: The VirtualBand that was removed
+        Removes a VRTRasterInputSourceBand
+        :param vrtRasterInputSourceBand: band index| VRTRasterInputSourceBand
+        :return: The VRTRasterInputSourceBand that was removed
         """
-        if not isinstance(bandOrIndex, VirtualBand):
-            bandOrIndex = self.sources[bandOrIndex]
-        i = self.sources.index(bandOrIndex)
-        self.sources.remove(bandOrIndex)
-        self.sigSourceBandRemoved.emit(i, bandOrIndex)
-        return bandOrIndex
+        if not isinstance(vrtRasterInputSourceBand, VRTRasterInputSourceBand):
+            vrtRasterInputSourceBand = self.sources[vrtRasterInputSourceBand]
+        if vrtRasterInputSourceBand in self.sources:
+            i = self.sources.index(vrtRasterInputSourceBand)
+            self.sources.remove(vrtRasterInputSourceBand)
+            self.sigSourceRemoved.emit(i, vrtRasterInputSourceBand)
+
 
     def sourceFiles(self):
         """
         :return: list of file-paths to all source files
         """
-        files = set([inputSource.path for inputSource in self.sources])
+        files = set([inputSource.mPath for inputSource in self.sources])
         return sorted(list(files))
 
     def __repr__(self):
         infos = ['VirtualBand name="{}"'.format(self.mName)]
         for i, info in enumerate(self.sources):
-            assert isinstance(info, VirtualBandInputSource)
-            infos.append('\t{} SourceFileName {} SourceBand {}'.format(i+1, info.path, info.bandIndex))
+            assert isinstance(info, VRTRasterInputSourceBand)
+            infos.append('\t{} SourceFileName {} SourceBand {}'.format(i + 1, info.mPath, info.mBandIndex))
         return '\n'.join(infos)
 
+LUT_ReampleAlg = {'nearest': gdal.GRA_NearestNeighbour,
+                  'bilinear': gdal.GRA_Bilinear,
+                  'mode':gdal.GRA_Mode,
+                  'lanczos':gdal.GRA_Lanczos,
+                  'average':gdal.GRA_Average,
+                  'cubic':gdal.GRA_Cubic,
+                  'cubic_splie':gdal.GRA_CubicSpline}
 
 
-class VirtualRasterBuilder(QObject):
+class VRTRaster(QObject):
 
-    def __init__(self, parent=None):
-        super(VirtualRasterBuilder, self).__init__(parent)
-        self.vBands = []
-        self.vMetadata = dict()
+    sigSourceBandInserted = pyqtSignal(VRTRasterBand, VRTRasterInputSourceBand)
+    sigSourceBandRemoved = pyqtSignal(VRTRasterBand, VRTRasterInputSourceBand)
+    sigSourceRasterAdded = pyqtSignal(list)
+    sigSourceRasterRemoved = pyqtSignal(list)
+    sigBandInserted = pyqtSignal(int, VRTRasterBand)
+    sigBandRemoved = pyqtSignal(int, VRTRasterBand)
+    sigCrsChanged = pyqtSignal(QgsCoordinateReferenceSystem)
 
 
+    def __init__(self, parent=None):
+        super(VRTRaster, self).__init__(parent)
+        self.mBands = []
+        self.mCrs = None
+        self.mResampleAlg = gdal.GRA_NearestNeighbour
+        self.mMetadata = dict()
+        self.mSourceRasterBounds = dict()
+        self.mOutputBounds = None
+        self.sigSourceBandRemoved.connect(self.updateSourceRasterBounds)
+        self.sigSourceBandInserted.connect(self.updateSourceRasterBounds)
+        self.sigBandRemoved.connect(self.updateSourceRasterBounds)
+        self.sigBandInserted.connect(self.updateSourceRasterBounds)
+
+    def setCrs(self, crs):
+        if isinstance(crs, osr.SpatialReference):
+            auth = '{}:{}'.format(crs.GetAttrValue('AUTHORITY',0), crs.GetAttrValue('AUTHORITY',1))
+            crs = QgsCoordinateReferenceSystem(auth)
+        if isinstance(crs, QgsCoordinateReferenceSystem):
+            if crs != self.mCrs:
+                self.mCrs = crs
+                self.sigCrsChanged.emit(self.mCrs)
+
+
+    def crs(self):
+        return self.mCrs
+
     def addVirtualBand(self, virtualBand):
         """
         Adds a virtual band
         :param virtualBand: the VirtualBand to be added
         :return: VirtualBand
         """
-        assert isinstance(virtualBand, VirtualBand)
+        assert isinstance(virtualBand, VRTRasterBand)
         return self.insertVirtualBand(len(self), virtualBand)
 
-
-
     def insertSourceBand(self, virtualBandIndex, pathSource, sourceBandIndex):
         """
         Inserts a source band into the VRT stack
@@ -139,50 +198,57 @@ class VirtualRasterBuilder(QObject):
         :param sourceBandIndex: source file band index
         """
 
-        while virtualBandIndex > len(self.vBands)-1:
-            self.insertVirtualBand(len(self.vBands), VirtualBand())
+        while virtualBandIndex > len(self.mBands)-1:
+
+            self.insertVirtualBand(len(self.mBands), VRTRasterBand())
 
-        vBand = self.vBands[virtualBandIndex]
+        vBand = self.mBands[virtualBandIndex]
         vBand.addSourceBand(pathSource, sourceBandIndex)
 
-    sigSourceAdded = pyqtSignal(VirtualBand, VirtualBandInputSource)
-    sigBandAdded = pyqtSignal(VirtualBand)
-    def insertVirtualBand(self, i, virtualBand):
+
+    def insertVirtualBand(self, index, virtualBand):
         """
         Inserts a VirtualBand
-        :param i: the insert position
+        :param index: the insert position
         :param virtualBand: the VirtualBand to be inserted
         :return: the VirtualBand
         """
-        assert isinstance(virtualBand, VirtualBand)
-        assert i <= len(self.vBands)
+        assert isinstance(virtualBand, VRTRasterBand)
+        assert index <= len(self.mBands)
+        if len(virtualBand.name()) == 0:
+            virtualBand.setName('Band {}'.format(index+1))
         virtualBand.mVRT = self
-        virtualBand.sigSourceBandInserted.connect(lambda sourceBand: self.sigSourceAdded(virtualBand, VirtualBandInputSource))
-        self.vBands.insert(i, virtualBand)
-        self.sigBandAdded.emit(virtualBand)
 
-        return self[i]
+        virtualBand.sigSourceInserted.connect(
+            lambda _, sourceBand: self.sigSourceBandInserted.emit(virtualBand, sourceBand))
+        virtualBand.sigSourceRemoved.connect(
+            lambda _, sourceBand: self.sigSourceBandInserted.emit(virtualBand, sourceBand))
+
+        self.mBands.insert(index, virtualBand)
+        self.sigBandInserted.emit(index, virtualBand)
+
+        return self[index]
+
 
 
-    sigBandsRemoved = pyqtSignal(list)
     def removeVirtualBands(self, bandsOrIndices):
         assert isinstance(bandsOrIndices, list)
         to_remove = []
-        for bandOrIndex in bandsOrIndices:
-            if not isinstance(bandOrIndex, VirtualBand):
-                bandOrIndex = self.vBands[bandOrIndex]
-            to_remove.append(bandOrIndex)
+        for virtualBand in bandsOrIndices:
+            if not isinstance(virtualBand, VRTRasterBand):
+                virtualBand = self.mBands[virtualBand]
+            to_remove.append((self.mBands.index(virtualBand), virtualBand))
 
-        for band in to_remove:
-            self.vBands.remove(band)
+        to_remove = sorted(to_remove, key=lambda t: t[0], reverse=True)
+        for index, virtualBand in to_remove:
+            self.mBands.remove(virtualBand)
+            self.sigBandRemoved.emit(index, virtualBand)
 
-        self.sigBandsRemoved.emit(to_remove)
-        return to_remove
 
 
     def removeVirtualBand(self, bandOrIndex):
-        r = self.removeVirtualBands([bandOrIndex])
-        return r[0]
+        self.removeVirtualBands([bandOrIndex])
+
 
     def addFilesAsMosaic(self, files):
         """
@@ -197,9 +263,9 @@ class VirtualRasterBuilder(QObject):
             for b in range(nb):
                 if b+1 < len(self):
                     #add new virtual band
-                    self.addVirtualBand(VirtualBand())
+                    self.addVirtualBand(VRTRasterBand())
                 vBand = self[b]
-                assert isinstance(vBand, VirtualBand)
+                assert isinstance(vBand, VRTRasterBand)
                 vBand.addSourceBand(file, b)
         return self
 
@@ -217,19 +283,56 @@ class VirtualRasterBuilder(QObject):
             ds = None
             for b in range(nb):
                 #each new band is a new virtual band
-                vBand = self.addVirtualBand(VirtualBand())
-                assert isinstance(vBand, VirtualBand)
-                vBand.addSourceBand(file, b)
+                vBand = self.addVirtualBand(VRTRasterBand())
+                assert isinstance(vBand, VRTRasterBand)
+                vBand.addSource(VRTRasterInputSourceBand(file, b))
         return self
 
-    def sourceFiles(self):
+    def sourceRaster(self):
         files = set()
-        for vBand in self.vBands:
-            assert isinstance(vBand, VirtualBand)
+        for vBand in self.mBands:
+            assert isinstance(vBand, VRTRasterBand)
             files.update(set(vBand.sourceFiles()))
         return sorted(list(files))
 
-    def saveVRT(self, pathVRT, **kwds):
+    def sourceRasterBounds(self):
+        return self.mSourceRasterBounds
+
+    def outputBounds(self):
+        if isinstance(self.mOutputBounds, RasterBounds):
+            return
+            #calculate from source rasters
+
+    def setOutputBounds(self, bounds):
+        assert isinstance(self, RasterBounds)
+        self.mOutputBounds = bounds
+
+
+    def updateSourceRasterBounds(self):
+
+        srcFiles = self.sourceRaster()
+        toRemove = [f for f in self.mSourceRasterBounds.keys() if f not in srcFiles]
+        toAdd = [f for f in srcFiles if f not in self.mSourceRasterBounds.keys()]
+
+        for f in toRemove:
+            del self.mSourceRasterBounds[f]
+        for f in toAdd:
+            self.mSourceRasterBounds[f] = RasterBounds(f)
+
+        if len(srcFiles) > 0 and self.crs() == None:
+            self.setCrs(self.mSourceRasterBounds[srcFiles[0]].crs)
+
+        elif len(srcFiles) == 0:
+            self.setCrs(None)
+
+
+        if len(toRemove) > 0:
+            self.sigSourceRasterRemoved.emit(toRemove)
+        if len(toAdd) > 0:
+            self.sigSourceRasterAdded.emit(toAdd)
+
+
+    def saveVRT(self, pathVRT, resampleAlg=gdal.GRA_NearestNeighbour, **kwds):
         """
         :param pathVRT: path to VRT that is created
         :param options --- can be be an array of strings, a string or let empty and filled from other keywords..
@@ -248,6 +351,8 @@ class VirtualRasterBuilder(QObject):
         :return: gdal.DataSet(pathVRT)
         """
 
+        assert os.path.splitext(pathVRT)[-1].lower() == '.vrt'
+
         _kwds = dict()
         supported = ['options','resolution','outputBounds','xRes','yRes','targetAlignedPixels','addAlpha','resampleAlg',
         'outputSRS','allowProjectionDifference','srcNodata','VRTNodata','hideNodata','callback', 'callback_data']
@@ -255,26 +360,56 @@ class VirtualRasterBuilder(QObject):
             if k in supported:
                 _kwds[k] = kwds[k]
 
+        if 'resampleAlg' not in _kwds:
+            _kwds['resampleAlg'] = resampleAlg
 
-        dn = os.path.dirname(pathVRT)
-        if not os.path.isdir(dn):
-            os.mkdir(dn)
+        if isinstance(self.mOutputBounds, RasterBounds):
+            bounds = self.mOutputBounds.polygon
+            xmin, ymin,xmax, ymax = bounds
+            _kwds['outputBounds'] = (xmin, ymin,xmax, ymax)
 
-        srcFiles = self.sourceFiles()
+        dirVrt = os.path.dirname(pathVRT)
+        dirWarpedVRT = os.path.join(dirVrt, 'WarpedVRTs')
+        if not os.path.isdir(dirVrt):
+            os.mkdir(dirVrt)
+
+        srcLookup = dict()
         srcNodata = None
-        for src in srcFiles:
-            ds = gdal.Open(src)
-            band = ds.GetRasterBand(1)
+        for i, pathSrc in enumerate(self.sourceRaster()):
+            dsSrc = gdal.Open(pathSrc)
+            assert isinstance(dsSrc, gdal.Dataset)
+            band = dsSrc.GetRasterBand(1)
             noData = band.GetNoDataValue()
             if noData and srcNodata is None:
                 srcNodata = noData
 
+            crs = QgsCoordinateReferenceSystem(dsSrc.GetProjection())
+
+            if crs == self.mCrs:
+                srcLookup[pathSrc] = pathSrc
+            else:
+
+                if not os.path.isdir(dirWarpedVRT):
+                    os.mkdir(dirWarpedVRT)
+                pathVRT2 = os.path.join(dirWarpedVRT, 'warped.{}.vrt'.format(os.path.basename(pathSrc)))
+                wops = gdal.WarpOptions(format='VRT',
+                                        dstSRS=self.mCrs.toWkt())
+                tmp = gdal.Warp(pathVRT2, dsSrc, options=wops)
+                assert isinstance(tmp, gdal.Dataset)
+                tmp = None
+                srcLookup[pathSrc] = pathVRT2
+
+
+
+
+        srcFiles = [srcLookup[src] for src in self.sourceRaster()]
+
         vro = gdal.BuildVRTOptions(separate=True, **_kwds)
         #1. build a temporary VRT that described the spatial shifts of all input sources
         gdal.BuildVRT(pathVRT, srcFiles, options=vro)
         dsVRTDst = gdal.Open(pathVRT)
         assert isinstance(dsVRTDst, gdal.Dataset)
-        assert len(srcFiles) == dsVRTDst.RasterCount
+        assert len(srcLookup) == dsVRTDst.RasterCount
         ns, nl = dsVRTDst.RasterXSize, dsVRTDst.RasterYSize
         gt = dsVRTDst.GetGeoTransform()
         crs = dsVRTDst.GetProjectionRef()
@@ -301,8 +436,8 @@ class VirtualRasterBuilder(QObject):
         dsVRTDst.SetGeoTransform(gt)
 
         #2.2. add virtual bands
-        for i, vBand in enumerate(self.vBands):
-            assert isinstance(vBand, VirtualBand)
+        for i, vBand in enumerate(self.mBands):
+            assert isinstance(vBand, VRTRasterBand)
             assert dsVRTDst.AddBand(eType, options=['subClass=VRTSourcedRasterBand']) == 0
             vrtBandDst = dsVRTDst.GetRasterBand(i+1)
             assert isinstance(vrtBandDst, gdal.Band)
@@ -310,9 +445,9 @@ class VirtualRasterBuilder(QObject):
             md = {}
             #add all input sources for this virtual band
             for iSrc, sourceInfo in enumerate(vBand.sources):
-                assert isinstance(sourceInfo, VirtualBandInputSource)
-                bandIndex = sourceInfo.bandIndex
-                xml = SOURCE_TEMPLATES[sourceInfo.path]
+                assert isinstance(sourceInfo, VRTRasterInputSourceBand)
+                bandIndex = sourceInfo.mBandIndex
+                xml = SOURCE_TEMPLATES[srcLookup[sourceInfo.mPath]]
                 xml = re.sub('<SourceBand>1</SourceBand>','<SourceBand>{}</SourceBand>'.format(bandIndex+1), xml)
                 md['source_{}'.format(iSrc)] = xml
             vrtBandDst.SetMetadata(md,'vrt_sources')
@@ -331,22 +466,22 @@ class VirtualRasterBuilder(QObject):
     def __repr__(self):
 
         info = ['VirtualRasterBuilder: {} bands, {} source files'.format(
-            len(self.vBands), len(self.sourceFiles()))]
-        for vBand in self.vBands:
+            len(self.mBands), len(self.sourceRaster()))]
+        for vBand in self.mBands:
             info.append(str(vBand))
         return '\n'.join(info)
 
     def __len__(self):
-        return len(self.vBands)
+        return len(self.mBands)
 
     def __getitem__(self, slice):
-        return self.vBands[slice]
+        return self.mBands[slice]
 
     def __delitem__(self, slice):
         self.removeVirtualBands(self[slice])
 
     def __contains__(self, item):
-        return item in self.vBands
+        return item in self.mBands
 
     def __iter__(self):
         return iter(self.mClasses)
@@ -354,6 +489,95 @@ class VirtualRasterBuilder(QObject):
 
 
 
+
+class VRTRasterVectorLayer(QgsVectorLayer):
+
+    def __init__(self, vrtRaster, crs=None):
+        assert isinstance(vrtRaster, VRTRaster)
+        if crs is None:
+            crs = QgsCoordinateReferenceSystem('EPSG:4326')
+
+        uri = 'Polygon?crs={}'.format(crs.authid())
+        super(VRTRasterVectorLayer, self).__init__(uri, 'VRTRaster', 'memory', False)
+        self.mCrs = crs
+        self.mVRTRaster = vrtRaster
+
+        #initialize fields
+        assert self.startEditing()
+        # standard field names, types, etc.
+        fieldDefs = [('oid', QVariant.Int, 'integer'),
+                     ('type', QVariant.String, 'string'),
+                     ('name', QVariant.String, 'string'),
+                     ('path', QVariant.String, 'string'),
+                     ]
+        # initialize fields
+        for fieldDef in fieldDefs:
+            field = QgsField(fieldDef[0], fieldDef[1], fieldDef[2])
+            self.addAttribute(field)
+        self.commitChanges()
+
+        symbol = QgsFillSymbolV2.createSimple({'style': 'no', 'color': 'red', 'outline_color':'black'})
+        self.rendererV2().setSymbol(symbol)
+        self.label().setFields(self.fields())
+        self.label().setLabelField(3,3)
+        self.mVRTRaster.sigSourceRasterAdded.connect(self.onRasterInserted)
+        self.mVRTRaster.sigSourceRasterRemoved.connect(self.onRasterRemoved)
+        self.onRasterInserted(self.mVRTRaster.sourceRaster())
+
+
+    def onRasterInserted(self, listOfNewFiles):
+        assert isinstance(listOfNewFiles, list)
+        if len(listOfNewFiles) == 0:
+            return
+
+        for f in listOfNewFiles:
+            bounds = self.mVRTRaster.sourceRasterBounds()[f]
+            assert isinstance(bounds, RasterBounds)
+            oid = str(id(bounds))
+            geometry =QgsPolygonV2(bounds.polygon)
+            trans = QgsCoordinateTransform(bounds.crs, self.crs())
+            geometry.transform(trans)
+
+
+
+
+            feature = QgsFeature(self.pendingFields())
+            #feature.setGeometry(QgsGeometry(geometry))
+            feature.setGeometry(QgsGeometry.fromWkt(geometry.asWkt()))
+            #feature.setFeatureId(int(oid))
+            feature.setAttribute('oid', oid)
+            feature.setAttribute('type', 'source file')
+            feature.setAttribute('name', str(os.path.basename(f)))
+            feature.setAttribute('path', str(f))
+            #feature.setValid(True)
+            self.startEditing()
+            assert self.dataProvider().addFeatures([feature])
+            assert self.commitChanges()
+            self.updateExtents()
+
+
+    def onRasterRemoved(self, files):
+
+        self.startEditing()
+        self.selectAll()
+        toRemove = []
+        for f in self.selectedFeatures():
+            if f.attribute('path') in files:
+                toRemove.append(f.id())
+        self.setSelectedFeatures(toRemove)
+        self.deleteSelectedFeatures()
+        self.commitChanges()
+        s = ""
+
+
+    def spectralLibrary(self):
+        return self.mVRTRaster
+
+    def nSpectra(self):
+        return len(self.mVRTRaster)
+
+
+
 def createVirtualBandMosaic(bandFiles, pathVRT):
     drv = gdal.GetDriverByName('VRT')
 
@@ -409,26 +633,97 @@ def createVirtualBandStack(bandFiles, pathVRT):
 from timeseriesviewer.utils import loadUi
 
 class TreeNode(QObject):
+    sigWillAddChildren = pyqtSignal(QObject, int, int)
+    sigAddedChildren = pyqtSignal(QObject, int, int)
+    sigWillRemoveChildren = pyqtSignal(QObject, int, int)
+    sigRemovedChildren = pyqtSignal(QObject, int, int)
+    sigUpdated = pyqtSignal(QObject)
 
-    def __init__(self, parentNode):
+    def __init__(self, parentNode, name=None):
         super(TreeNode, self).__init__()
         self.mParent = parentNode
-        if isinstance(parentNode, TreeNode):
-            parentNode.mChildren.append(self)
+
         self.mChildren = []
-        self.mName = None
+        self.mName = name
         self.mValues = []
         self.mIcon = None
         self.mToolTip = None
 
+
+        if isinstance(parentNode, TreeNode):
+            parentNode.appendChildNodes(self)
+
+    def detach(self):
+        """
+        Detaches this TreeNode from its parent TreeNode
+        :return:
+        """
+        if isinstance(self.mParent, TreeNode):
+            self.mParent.mChildren.remove(self)
+            self.setParentNode(None)
+
+    def appendChildNodes(self, listOfChildNodes):
+        self.insertChildNodes(len(self.mChildren), listOfChildNodes)
+
+    def insertChildNodes(self, index, listOfChildNodes):
+        assert index <= len(self.mChildren)
+        if isinstance(listOfChildNodes, TreeNode):
+            listOfChildNodes = [listOfChildNodes]
+        assert isinstance(listOfChildNodes, list)
+        l = len(listOfChildNodes)
+        idxLast = index+l-1
+        self.sigWillAddChildren.emit(self, index, idxLast)
+        for i, node in enumerate(listOfChildNodes):
+            assert isinstance(node, TreeNode)
+            node.mParent = self
+            # connect node signals
+            node.sigWillAddChildren.connect(self.sigWillAddChildren)
+            node.sigAddedChildren.connect(self.sigAddedChildren)
+            node.sigWillRemoveChildren.connect(self.sigWillRemoveChildren)
+            node.sigRemovedChildren.connect(self.sigRemovedChildren)
+            node.sigUpdated.connect(self.sigUpdated)
+
+            self.mChildren.insert(index+i, node)
+
+        self.sigAddedChildren.emit(self, index, idxLast)
+
+    def removeChildNode(self, node):
+        assert node in self.mChildren
+        i = self.mChildren.index(node)
+        self.removeChildNodes(i, 1)
+
+    def removeChildNodes(self, row, count):
+
+        if row < 0 or count <= 0:
+            return False
+
+        rowLast = row + count - 1
+
+        if rowLast >= self.childCount():
+            return False
+
+        self.sigWillRemoveChildren.emit(self, row, rowLast)
+        to_remove = self.childNodes()[row:rowLast+1]
+        for n in to_remove:
+            self.mChildren.remove(n)
+            #n.mParent = None
+
+        self.sigRemovedChildren.emit(self, row, rowLast)
+
+
+
     def setToolTip(self, toolTip):
         self.mToolTip = toolTip
     def toolTip(self):
         return self.mToolTip
 
-    def parent(self):
+    def parentNode(self):
         return self.mParent
 
+    def setParentNode(self, treeNode):
+        assert isinstance(treeNode, TreeNode)
+        self.mParent = treeNode
+
     def setIcon(self, icon):
         self.mIcon = icon
 
@@ -455,6 +750,8 @@ class TreeNode(QObject):
     def childCount(self):
         return len(self.mChildren)
 
+    def childNodes(self):
+        return self.mChildren[:]
 
 class SourceRasterFileNode(TreeNode):
 
@@ -467,54 +764,155 @@ class SourceRasterFileNode(TreeNode):
         #populate metainfo
         ds = gdal.Open(path)
         assert isinstance(ds, gdal.Dataset)
+
+        crsNode = TreeNode(self, name='CRS')
+        crs = osr.SpatialReference()
+        crs.ImportFromWkt(ds.GetProjection())
+
+        authInfo = '{}:{}'.format(crs.GetAttrValue('AUTHORITY',0), crs.GetAttrValue('AUTHORITY',1))
+        crsNode.setValues([authInfo,crs.ExportToWkt()])
+        self.bandNode = TreeNode(None, name='Bands')
         for b in range(ds.RasterCount):
             band = ds.GetRasterBand(b+1)
-            bandNode = SourceRasterBandNode(self, b)
-            bandNode.setName('Band {}'.format(b+1))
-            bandNode.setValues([band.GetDescription()])
 
+            inputSource = VRTRasterInputSourceBand(path, b)
+            inputSource.mBandName = band.GetDescription()
+            if inputSource.mBandName in [None,'']:
+                inputSource.mBandName = '{}'.format(b + 1)
+            inputSource.mNoData = band.GetNoDataValue()
+
+            SourceRasterBandNode(self.bandNode, inputSource)
+        self.bandNode.setParentNode(self)
+        self.appendChildNodes(self.bandNode)
+
+    def sourceBands(self):
+        return [n.mSrcBand for n in self.bandNode.mChildren if isinstance(n, SourceRasterBandNode)]
 
 class SourceRasterBandNode(TreeNode):
-    def __init__(self, parentNode, bandIndex):
-        assert isinstance(parentNode, SourceRasterFileNode)
+    def __init__(self, parentNode, vrtRasterInputSourceBand):
+        assert isinstance(vrtRasterInputSourceBand, VRTRasterInputSourceBand)
         super(SourceRasterBandNode, self).__init__(parentNode)
 
-        self.mBandIndex = bandIndex
+        self.mSrcBand = vrtRasterInputSourceBand
+        self.setName(self.mSrcBand.mBandName)
+        #self.setValues([self.mSrcBand.mPath])
+        self.setToolTip('band {}:{}'.format(self.mSrcBand.mBandIndex+1, self.mSrcBand.mPath))
+
+class VRTRasterNode(TreeNode):
+    def __init__(self, parentNode, vrtRaster):
+        assert isinstance(vrtRaster, VRTRaster)
+
+        super(VRTRasterNode, self).__init__(parentNode)
+        self.mVRTRaster = vrtRaster
+        self.mVRTRaster.sigBandInserted.connect(self.onBandInserted)
+        self.mVRTRaster.sigBandRemoved.connect(self.onBandRemoved)
 
-class VirtualBandNode(TreeNode):
+    def onBandInserted(self, index, vrtRasterBand):
+        assert isinstance(vrtRasterBand, VRTRasterBand)
+        i = vrtRasterBand.bandIndex()
+        assert i == index
+        node = VRTRasterBandNode(None, vrtRasterBand)
+        self.insertChildNodes(i, [node])
+
+    def onBandRemoved(self, removedIdx):
+        self.removeChildNodes(removedIdx, 1)
+
+
+class VRTRasterBandNode(TreeNode):
     def __init__(self, parentNode, virtualBand):
-        assert isinstance(virtualBand, VirtualBand)
-        assert isinstance(parentNode, TreeNode) and parentNode.parent() == None
-        super(VirtualBandNode, self).__init__(parentNode)
+        assert isinstance(virtualBand, VRTRasterBand)
+
+        super(VRTRasterBandNode, self).__init__(parentNode)
         self.mVirtualBand = virtualBand
 
         self.setName(virtualBand.name())
 
-
-
+        #self.nodeBands = TreeNode(self, name='Input Bands')
+        #self.nodeBands.setToolTip('Source bands contributing to this virtual raster band')
+        self.nodeBands = self
         virtualBand.sigNameChanged.connect(self.setName)
+        virtualBand.sigSourceInserted.connect(lambda _, src: self.onSourceInserted(src))
+        virtualBand.sigSourceRemoved.connect(self.onSourceRemoved)
+        for src in self.mVirtualBand.sources:
+            self.onSourceInserted(src)
 
-    def onSourceAdded(self, inputSource):
-        assert isinstance(inputSource, VirtualBandInputSource)
+
+    def onSourceInserted(self, inputSource):
+        assert isinstance(inputSource, VRTRasterInputSourceBand)
         assert inputSource.virtualBand() == self.mVirtualBand
         i = self.mVirtualBand.sources.index(inputSource)
-        node = TreeNode(None)
-        node.setName(os.path.basename(inputSource.path))
-        node.setValues([inputSource.path])
-        self.mChildren.insert(i, node)
 
-    def onSourceRemoved(self, inputSource):
-        to_remove = [n for n in self.mChildren if inputSource.path in n.values()]
-        for n in to_remove:
-            self.mChildren.remove(n)
+        node = VRTRasterInputSourceBandNode(None, inputSource)
+        self.nodeBands.insertChildNodes(i, node)
 
+    def onSourceRemoved(self, row, inputSource):
+        assert isinstance(inputSource, VRTRasterInputSourceBand)
+
+        node = self.nodeBands.childNodes()[row]
+        if  node.mSrc != inputSource:
+            s = ""
+        self.nodeBands.removeChildNode(node)
 
-class TreeModelBase(QAbstractItemModel):
-    def __init__(self, parent=None):
-        super(TreeModelBase, self).__init__(parent)
+
+
+
+class VRTRasterInputSourceBandNode(TreeNode):
+    def __init__(self, parentNode, vrtRasterInputSourceBand):
+        assert isinstance(vrtRasterInputSourceBand, VRTRasterInputSourceBand)
+        super(VRTRasterInputSourceBandNode, self).__init__(parentNode)
+
+        self.mSrc = vrtRasterInputSourceBand
+        name = '{}:{}'.format(self.mSrc.mBandIndex+1, os.path.basename(self.mSrc.mPath))
+        self.setName(name)
+        #self.setValues([self.mSrc.mPath, self.mSrc.mBandIndex])
+
+    def sourceBand(self):
+        return self.mSrc
+
+class TreeView(QTreeView):
+
+    def __init__(self, *args, **kwds):
+        super(TreeView, self).__init__(*args, **kwds)
+
+class TreeModel(QAbstractItemModel):
+    def __init__(self, parent=None, rootNode = None):
+        super(TreeModel, self).__init__(parent)
 
         self.mColumnNames = ['Node','Value']
-        self.mRootNode = TreeNode(None)
+        self.mRootNode = rootNode if isinstance(rootNode, TreeNode) else TreeNode(None)
+        self.mRootNode.sigWillAddChildren.connect(self.nodeWillAddChildren)
+        self.mRootNode.sigAddedChildren.connect(self.nodeAddedChildren)
+        self.mRootNode.sigWillRemoveChildren.connect(self.nodeWillRemoveChildren)
+        self.mRootNode.sigRemovedChildren.connect(self.nodeRemovedChildren)
+        self.mRootNode.sigUpdated.connect(self.nodeUpdated)
+
+        self.mTreeView = None
+        if isinstance(parent, QTreeView):
+            self.connectTreeView(parent)
+
+    def nodeWillAddChildren(self, node, idx1, idxL):
+        idxNode = self.node2idx(node)
+        self.beginInsertRows(idxNode, idx1, idxL)
+
+
+    def nodeAddedChildren(self, node, idx1, idxL):
+        self.endInsertRows()
+        #for i in range(idx1, idxL+1):
+        for n in node.childNodes():
+            self.setColumnSpan(node)
+
+    def nodeWillRemoveChildren(self, node, idx1, idxL):
+        idxNode = self.node2idx(node)
+        self.beginRemoveRows(idxNode, idx1, idxL)
+
+    def nodeRemovedChildren(self, node, idx1, idxL):
+        self.endRemoveRows()
+
+
+    def nodeUpdated(self, node):
+        idxNode = self.node2idx(node)
+        self.dataChanged.emit(idxNode, idxNode)
+        self.setColumnSpan(node)
 
     def headerData(self, section, orientation, role):
         if orientation == Qt.Horizontal and role == Qt.DisplayRole:
@@ -530,13 +928,20 @@ class TreeModelBase(QAbstractItemModel):
     def parent(self, index):
         if not index.isValid():
             return QModelIndex()
-        node = index.internalPointer()
-        parentNode = node.parent()
-        if parentNode is None:
+        node = self.idx2node(index)
+        if not isinstance(node, TreeNode):
             return QModelIndex()
-        else:
-            row = parentNode.mChildren.index(node)
-            return self.createIndex(row, 0, parentNode)
+
+        parentNode = node.parentNode()
+        if not isinstance(parentNode, TreeNode):
+            return QModelIndex()
+
+        return self.node2idx(parentNode)
+
+        if node not in parentNode.mChildren:
+            return QModelIndex
+        row = parentNode.mChildren.index(node)
+        return self.createIndex(row, 0, parentNode)
 
     def rowCount(self, index):
 
@@ -554,19 +959,57 @@ class TreeModelBase(QAbstractItemModel):
 
         return len(self.mColumnNames)
 
+    def connectTreeView(self, treeView):
+        self.mTreeView = treeView
+
+    def setColumnSpan(self, node):
+        if isinstance(self.mTreeView, QTreeView) \
+                and isinstance(node, TreeNode) \
+                and isinstance(node.parentNode(), TreeNode) :
+            idxNode = self.node2idx(node)
+            idxParent = self.node2idx(node.parentNode())
+            span = len(node.values()) == 0
+            self.mTreeView.setFirstColumnSpanned(idxNode.row(), idxParent, span)
+            for n in node.childNodes():
+                self.setColumnSpan(n)
+
 
     def index(self, row, column, parentIndex=None):
 
+
+
         if parentIndex is None:
             parentNode = self.mRootNode
         else:
             parentNode = self.idx2node(parentIndex)
+
+        if row < 0 or row >= parentNode.childCount():
+            return QModelIndex()
+        if column < 0 or column >= len(self.mColumnNames):
+            return QModelIndex()
+
         if isinstance(parentNode, TreeNode) and row < len(parentNode.mChildren):
             return self.createIndex(row,column,parentNode.mChildren[row])
         else:
             return QModelIndex()
 
-
+    def findParentNode(self, node, parentNodeType):
+        assert isinstance(node, TreeNode)
+        while True:
+            if isinstance(node, parentNodeType):
+                return node
+            if not isinstance(node.parentNode(), TreeNode):
+                return None
+            node = node.parentNode()
+
+    def indexes2nodes(self, indexes):
+        assert isinstance(indexes, list)
+        nodes = []
+        for idx in indexes:
+            n = self.idx2node(idx)
+            if n not in nodes:
+                nodes.append(n)
+        return nodes
 
     def idx2node(self, index):
         if not index.isValid():
@@ -579,8 +1022,10 @@ class TreeModelBase(QAbstractItemModel):
         if node == self.mRootNode:
             return QModelIndex()
         else:
-            parentNode = node.parent()
+            parentNode = node.parentNode()
             assert isinstance(parentNode, TreeNode)
+            if node not in parentNode.mChildren:
+                return QModelIndex()
             r = parentNode.mChildren.index(node)
             return self.createIndex(r,0,node)
 
@@ -610,48 +1055,108 @@ class TreeModelBase(QAbstractItemModel):
         node = self.idx2node(index)
         return Qt.ItemIsEnabled | Qt.ItemIsSelectable
 
-class VRTRasterSourceModel(TreeModelBase):
+
+class RasterBounds(object):
+    def __init__(self, path):
+        self.path = None
+        self.polygon = None
+
+        self.crs = None
+
+        if path is not None:
+            self.fromImage(path)
+
+    def fromRectangle(self, crs, rectangle):
+        assert isinstance(rectangle, QgsRectangle)
+        assert isinstance(crs, QgsCoordinateReferenceSystem)
+        self.crs = crs
+        self.path = ''
+        s = ""
+
+
+    def fromImage(self, path):
+        self.path = path
+        ds = gdal.Open(path)
+        assert isinstance(ds, gdal.Dataset)
+        gt = ds.GetGeoTransform()
+        bounds = [px2geo(QPoint(0, 0), gt),
+                  px2geo(QPoint(ds.RasterXSize, 0), gt),
+                  px2geo(QPoint(ds.RasterXSize, ds.RasterYSize), gt),
+                  px2geo(QPoint(0, ds.RasterYSize), gt)]
+        crs = QgsCoordinateReferenceSystem(ds.GetProjection())
+        ring = ogr.Geometry(ogr.wkbLinearRing)
+        for p in bounds:
+            assert isinstance(p, QgsPoint)
+            ring.AddPoint(p.x(), p.y())
+        polygon = ogr.Geometry(ogr.wkbPolygon)
+        polygon.AddGeometry(ring)
+        self.polygon = QgsPolygonV2()
+        self.polygon.fromWkt(polygon.ExportToWkt())
+        self.polygon.exteriorRing().close()
+        assert self.polygon.exteriorRing().isClosed()
+
+        self.crs = crs
+
+    def __repr__(self):
+        return self.polygon.ExportToWkt()
+
+class SourceRasterModel(TreeModel):
     def __init__(self, parent=None):
-        super(VRTRasterSourceModel, self).__init__(parent)
+        super(SourceRasterModel, self).__init__(parent)
 
-        self.mFiles = []
         self.mColumnNames = ['File', 'Value']
 
+
+    def files(self):
+        return [n.mPath for n in self.mRootNode.childNodes() if isinstance(n, SourceRasterFileNode)]
+
     def addFile(self, file):
         self.addFiles([file])
 
 
-    def addFiles(self, listOfFiles):
-        assert isinstance(listOfFiles, list)
-        listOfFiles = [os.path.normpath(f) for f in listOfFiles]
-        listOfFiles = [f for f in listOfFiles if f not in self.mFiles and isinstance(gdal.Open(f), gdal.Dataset)]
-        if len(listOfFiles) > 0:
-            rootNode = self.mRootNode
-            rootIndex = self.node2idx(rootNode)
-            r0 = rootNode.childCount()
-
-            self.beginInsertRows(rootIndex, r0,  r0+len(listOfFiles) )
-            for f in listOfFiles:
+    def addFiles(self, newFiles):
+        assert isinstance(newFiles, list)
+        existingFiles = self.files()
+        newFiles = [os.path.normpath(f) for f in newFiles]
+        newFiles = [f for f in newFiles if f not in existingFiles and isinstance(gdal.Open(f), gdal.Dataset)]
+        if len(newFiles) > 0:
+            for f in newFiles:
                 SourceRasterFileNode(self.mRootNode, f)
-            self.endInsertRows()
 
 
+    def file2node(self, file):
+        for node in self.mRootNode.childNodes():
+            if isinstance(node, SourceRasterFileNode) and node.mPath == file:
+                return node
+        return None
+
     def removeFiles(self, listOfFiles):
         assert isinstance(listOfFiles, list)
 
+        toRemove = [n for n in self.mRootNode.childNodes() \
+            if isinstance(n, SourceRasterFileNode) and n.mPath in listOfFiles]
+
+        for n in toRemove:
+            n.parentNode().removeChildNode(n)
+
+
     def flags(self, index):
         if not index.isValid():
             return Qt.NoItemFlags
 
         node = self.idx2node(index)
 
-        flags = super(VRTRasterSourceModel, self).flags(index)
+        flags = super(SourceRasterModel, self).flags(index)
         #return flags
         if isinstance(node, SourceRasterFileNode) or \
             isinstance(node, SourceRasterBandNode):
             flags |= Qt.ItemIsDragEnabled
         return flags
 
+    def contextMenu(self):
+
+        return None
+
     def mimeTypes(self):
         # specifies the mime types handled by this model
         types = []
@@ -670,109 +1175,93 @@ class VRTRasterSourceModel(TreeModelBase):
                 nodes.append(n)
 
 
-        bandNodes = []
+        sourceBands = []
 
         for node in nodes:
             if isinstance(node, SourceRasterFileNode):
-                for n in [n for n in node.mChildren if isinstance(n, SourceRasterBandNode)]:
-                    if n not in bandNodes:
-                        bandNodes.append(n)
+                sourceBands.extend(node.sourceBands())
             if isinstance(node, SourceRasterBandNode):
-                if node not in bandNodes:
-                    bandNodes.append(node)
+                sourceBands.append(node.mSrcBand)
 
-        fileNodes = []
-        for n in bandNodes:
-            fileNode = n.parent()
-            assert isinstance(fileNode, SourceRasterFileNode)
-            if fileNode not in fileNodes:
-                fileNodes.append(fileNode)
-
-        uriList = [n.mPath for n in fileNodes]
-        bandList = [(n.parent().mPath, n.mBandIndex) for n in bandNodes]
+        sourceBands = list(OrderedDict.fromkeys(sourceBands))
+        uriList = [sourceBand.mPath for sourceBand in sourceBands]
+        uriList = list(OrderedDict.fromkeys(uriList))
 
         mimeData = QMimeData()
 
-        if len(bandList) > 0:
-            mimeData.setData('hub.vrtbuilder/bandlist', pickle.dumps(bandList))
+        if len(sourceBands) > 0:
+            mimeData.setData('hub.vrtbuilder/bandlist', pickle.dumps(sourceBands))
 
         # set text/uri-list
         if len(uriList) > 0:
             mimeData.setUrls([QUrl(p) for p in uriList])
-            mimeData.setText('\n'.join(uriList))
+            #mimeData.setText('\n'.join(uriList))
         return mimeData
 
 
 
-class VRTModel(TreeModelBase):
-    def __init__(self, parent=None, vrtBuilder=None):
-        super(VRTModel, self).__init__(parent)
+class VRTRasterTreeModel(TreeModel):
+    def __init__(self, parent=None, vrtRaster=None):
 
-        if vrtBuilder is None:
-            vrtBuilder = VirtualRasterBuilder()
-        else:
-            assert isinstance(vrtBuilder, VirtualRasterBuilder)
-        self.mVRTBuilder = vrtBuilder
-        self.mVRTBuilder.sigBandAdded.connect(self.onBandAdded)
-        self.mVRTBuilder.sigSourceAdded.connect(self.onSourceAdded)
-
-
-    def vBand2vBandNode(self, virtualBand):
-        assert isinstance(virtualBand, VirtualBand)
-        row = self.mVRTBuilder.vBands.index(virtualBand)
-        return self.mRootNode.mChildren[row]
-
-    def onSourceAdded(self, virtualBand, inputSource):
-        assert isinstance(virtualBand, VirtualBand)
-        assert isinstance(inputSource, VirtualBandInputSource)
-        vBandNode = self.vBand2vBandNode(virtualBand)
-        assert isinstance(vBandNode, VirtualBandNode)
-        idx = self.node2idx(vBandNode)
-        row = virtualBand.bandIndex()
-        self.beginInsertRows(idx, row, row)
-        node = TreeNode(None)
-        node.setName(os.path.basename(inputSource.path))
-        node.setValues([inputSource.path])
-        vBandNode.mChildren.insert(row, node)
-        self.endInsertRows()
+        vrtRaster = vrtRaster if isinstance(vrtRaster, VRTRaster) else VRTRaster()
+        rootNode = VRTRasterNode(None, vrtRaster)
+        super(VRTRasterTreeModel, self).__init__(parent, rootNode=rootNode)
+        self.mVRTRaster = vrtRaster
+        self.mColumnNames = ['Virtual Raster']
 
-    def onBandAdded(self, virtualBand):
-        assert isinstance(virtualBand, VirtualBand)
+    def setData(self, index, value, role):
+        node = self.idx2node(index)
+        col = index.column()
 
-        i = virtualBand.bandIndex()
-        rootNode = self.mRootNode
-        rootIdx = self.node2idx(self.mRootNode)
-        self.beginInsertRows(rootIdx, i, i)
-        vBandNode = VirtualBandNode(self.mRootNode, virtualBand)
+        if role == Qt.EditRole:
+            if isinstance(node, VRTRasterBandNode) and col == 0:
+                if len(value) > 0:
+                    node.setName(value)
+                    return True
 
-        #self.mRootNode.mChildren.insert(i, vBandNode)
-        self.endInsertRows()
 
-        virtualBand.sigSourceBandInserted.connect(self.onSourceBandInserted)
-        for inputSource in virtualBand.sources:
-            self.onSourceBandInserted(inputSource)
+        return False
+
+    def removeNodes(self, nodes):
 
-    def onSourceBandInserted(self, sourceBand):
-        assert isinstance(sourceBand, VirtualBandInputSource)
+        for vBandNode in [n for n in nodes if isinstance(n, VRTRasterBandNode)]:
+            self.mVRTRaster.removeVirtualBand(vBandNode.mVirtualBand)
 
-        vBand = sourceBand.virtualBand()
-        assert isinstance(vBand, VirtualBand)
-        VRT = vBand.mVRT
-        assert isinstance(VRT, VirtualRasterBuilder)
+        for vBandSrcNode in [n for n in nodes if isinstance(n, VRTRasterInputSourceBandNode)]:
+            assert isinstance(vBandSrcNode, VRTRasterInputSourceBandNode)
+            srcBand = vBandSrcNode.mSrc
 
-        i = vBand.sources.index(sourceBand)
+            srcBand.virtualBand().removeSource(srcBand)
 
-        idxParent = self.createIndex(VRT.vBands.index(vBand), 0, vBand)
-        sNode = VirtualBandInputSource
+
+    def removeRows(self, row, count, parent):
+        parentNode = self.idx2node(parent)
+
+
+        if isinstance(parentNode, VRTRasterBandNode):
+            #self.beginRemoveRows(parent, row, row+count-1)
+            vBand = parentNode.mVirtualBand
+            for n in parentNode.childNodes()[row:row+count]:
+                vBand.removeSource(n.mSrc)
+            #self.endRemoveRows()
+            return True
+        else:
+            return False
 
 
     def flags(self, index):
         if not index.isValid():
-            return Qt.NoItemFlags
+            return Qt.ItemIsDropEnabled
 
+        node = self.idx2node(index)
+        flags = super(VRTRasterTreeModel, self).flags(index)
 
-        flags = super(VRTModel, self).flags(index)
-        flags |= Qt.ItemIsDropEnabled | Qt.ItemIsEditable
+        if isinstance(node, VRTRasterBandNode):
+            flags |= Qt.ItemIsDropEnabled
+            flags |= Qt.ItemIsEditable
+        if isinstance(node, VRTRasterInputSourceBandNode):
+            flags |= Qt.ItemIsDropEnabled
+            flags |= Qt.ItemIsDragEnabled
         return flags
 
     def dragEnterEvent(self, event):
@@ -788,6 +1277,7 @@ class VRTModel(TreeModelBase):
 
     def dropEvent(self, event):
         assert isinstance(event, QDropEvent)
+
         if event.mimeData().hasFormat(u'hub.vrtbuilder/bandlist'):
             parent = self.mRootNode
             p = self.node2idx(parent)
@@ -806,25 +1296,87 @@ class VRTModel(TreeModelBase):
         types.append('hub.vrtbuilder/bandlist')
         return types
 
+
+    def mimeData(self, indexes):
+        indexes = sorted(indexes)
+        nodes = [self.idx2node(i) for i in indexes]
+
+        sourceBands = []
+
+        for node in nodes:
+            if isinstance(node, VRTRasterInputSourceBandNode):
+                sourceBand = node.sourceBand()
+                assert isinstance(sourceBand, VRTRasterInputSourceBand)
+                sourceBands.append(sourceBand)
+
+        sourceBands = list(OrderedDict.fromkeys(sourceBands))
+        uriList = [sourceBand.mPath for sourceBand in sourceBands]
+        uriList = list(OrderedDict.fromkeys(uriList))
+
+        mimeData = QMimeData()
+
+        if len(sourceBands) > 0:
+            mimeData.setData('hub.vrtbuilder/bandlist', pickle.dumps(sourceBands))
+
+        # set text/uri-list
+        if len(uriList) > 0:
+            mimeData.setUrls([QUrl(p) for p in uriList])
+            mimeData.setText('\n'.join(uriList))
+
+        return mimeData
+
+
+
     def dropMimeData(self, mimeData, action, row, column, parentIndex):
+        if action == Qt.IgnoreAction:
+            return True
+
         assert isinstance(mimeData, QMimeData)
         #assert isinstance(action, QDropEvent)
-        bands = []
+        sourceBands = []
 
         if u'hub.vrtbuilder/bandlist' in mimeData.formats():
             dump = mimeData.data(u'hub.vrtbuilder/bandlist')
-            bands = pickle.loads(dump)
+            sourceBands = pickle.loads(dump)
+
+        if u'hub.vrtbuilder/vrt.indices' in mimeData.formats():
+            dump = mimeData.data(u'hub.vrtbuilder/vrt.indices')
+            indices = pickle.loads(dump)
+            s = ""
 
+            if action == Qt.MoveAction:
+                s = ""
+
+        if len(sourceBands) == 0:
+            return False
         parentNode = self.idx2node(parentIndex)
-        if parentNode == self.mRootNode:
+        if isinstance(parentNode,VRTRasterNode):
             #add VRT band per band
-            for band in bands:
-                path, bandIndex = band
-                vBand = VirtualBand()
-                vBand.addSourceBand(path, bandIndex)
-                self.mVRTBuilder.addVirtualBand(vBand)
+            for sourceBand in sourceBands:
+                assert isinstance(sourceBand, VRTRasterInputSourceBand)
+                vBand = VRTRasterBand()
+                vBand.addSource(sourceBand)
+                self.mVRTRaster.addVirtualBand(vBand)
+            return True
+
+        if isinstance(parentNode, VRTRasterInputSourceBandNode):
+            #insert after this node
+            row = parentNode.parentNode().mChildren.index(parentNode)+1
+            parentNode = parentNode.parentNode()
+
+        if isinstance(parentNode, VRTRasterBandNode):
+            vBand = parentNode.mVirtualBand
+            assert isinstance(vBand, VRTRasterBand)
+            if row < 0:
+                row = 0
+            for sourceBand in sourceBands:
+                vBand.insertSource(row, sourceBand)
+                row += 1
+            return True
+
+
             s = ""
-        s = ""
+        return False
 
     def supportedDragActions(self):
         return Qt.CopyAction | Qt.MoveAction
@@ -832,20 +1384,147 @@ class VRTModel(TreeModelBase):
     def supportedDropActions(self):
         return Qt.CopyAction | Qt.MoveAction
 
-class VirtualRasterBuilderWidget(QFrame, loadUi('vrtbuilder.ui')):
+class VRTBuilderWidget(QFrame, loadUi('vrtbuilder.ui')):
 
     def __init__(self, parent=None):
-        super(VirtualRasterBuilderWidget, self).__init__(parent)
+        super(VRTBuilderWidget, self).__init__(parent)
         self.setupUi(self)
-        self.sourceFileModel = VRTRasterSourceModel()
-        self.treeViewSourceFiles.setModel(self.sourceFileModel)
+        self.sourceFileModel = SourceRasterModel(parent=self.treeViewSourceFiles)
 
-        self.vrtBuilder = VirtualRasterBuilder()
-        self.vrtBuilderModel = VRTModel(parent=self, vrtBuilder=self.vrtBuilder)
+        self.treeViewSourceFiles.setModel(self.sourceFileModel)
+        self.treeViewSourceFiles.selectionModel().selectionChanged.connect(self.onSrcModelSelectionChanged)
+
+        self.mCrsManuallySet = False
+        self.mBoundsManuallySet = False
+
+        self.tbNoData.setValidator(QDoubleValidator())
+
+        self.tbOutputPath.textChanged.connect(self.onOutputPathChanged)
+
+        filter='GDAL Virtual Raster (*.vrt);;GeoTIFF (*.tiff *.tif);;ENVI (*.bsq *.bil *.bip)'
+        self.btnSelectVRTPath.clicked.connect(lambda :
+                                              self.tbOutputPath.setText(
+                                                  QFileDialog.getSaveFileName(self,
+                                                                              directory=self.tbOutputPath.text(),
+                                                                              caption='Select output image',
+                                                                              filter=filter)
+                                              ))
+        self.buttonBox.button(QDialogButtonBox.Ok).clicked.connect(self.saveFile)
+        self.vrtRaster = VRTRaster()
+        self.vrtRasterLayer = VRTRasterVectorLayer(self.vrtRaster)
+        QgsMapLayerRegistry.instance().addMapLayer(self.vrtRasterLayer)
+
+        assert isinstance(self.previewMap, QgsMapCanvas)
+        self.previewMap.setLayerSet([QgsMapCanvasLayer(self.vrtRasterLayer)])
+        self.previewMap.contextMenuEvent = self.mapCanvasContextMenuEvent
+        self.vrtRaster.sigCrsChanged.connect(self.updateSummary)
+        self.vrtRaster.sigSourceBandInserted.connect(self.updateSummary)
+        self.vrtRaster.sigSourceBandRemoved.connect(self.updateSummary)
+
+        self.vrtRaster.sigBandInserted.connect(self.updateSummary)
+        self.vrtRaster.sigBandRemoved.connect(self.updateSummary)
+
+        self.vrtRaster.sigSourceRasterAdded.connect(self.mapRefresh)
+        self.vrtBuilderModel = VRTRasterTreeModel(parent=self.treeViewVRT, vrtRaster=self.vrtRaster)
         self.treeViewVRT.setModel(self.vrtBuilderModel)
-        self.treeViewVRT.dragEnterEvent = self.vrtBuilderModel.dragEnterEvent
-        self.treeViewVRT.dragMoveEvent = self.vrtBuilderModel.dragMoveEvent
-        self.treeViewVRT.dropEvent = self.vrtBuilderModel.dropEvent
+        self.treeViewVRT.selectionModel().selectionChanged.connect(self.onVRTModelSelectionChanged)
+        #self.vrtBuilderModel.redirectDropEvent(self.treeViewVRT)
+        #self.treeViewVRT.dragEnterEvent = self.vrtBuilderModel.dragEnterEvent
+        #self.treeViewVRT.dragMoveEvent = self.vrtBuilderModel.dragMoveEvent
+
+
+
+        self.treeViewVRT.setAutoExpandDelay(50)
+        self.treeViewVRT.setDragEnabled(True)
+        self.treeViewVRT.contextMenuEvent = self.vrtTreeViewContextMenuEvent
+
+
+        self.btnExpandAllVRT.clicked.connect(lambda :self.expandNodes(self.treeViewVRT, True))
+        self.btnCollapseAllVRT.clicked.connect(lambda: self.expandNodes(self.treeViewVRT, False))
+
+        self.btnExpandAllSrc.clicked.connect(lambda :self.expandNodes(self.treeViewSourceFiles, True))
+        self.btnCollapseAllSrc.clicked.connect(lambda: self.expandNodes(self.treeViewSourceFiles, False))
+
+        self.btnAddVirtualBand.clicked.connect(lambda : self.vrtRaster.addVirtualBand(VRTRasterBand(name='Band {}'.format(len(self.vrtRaster)+1))))
+        self.btnRemoveVirtualBands.clicked.connect(lambda : self.vrtBuilderModel.removeNodes(
+                                                   self.vrtBuilderModel.indexes2nodes(self.treeViewVRT.selectedIndexes())
+                                                    )
+                                                   )
+        self.btnAddFromRegistry.clicked.connect(self.loadSrcFromMapLayerRegistry)
+        self.btnAddSrcFiles.clicked.connect(lambda :
+                                            self.sourceFileModel.addFiles(
+                                                QFileDialog.getOpenFileNames(self, "Open raster images",
+                                                                            directory='')
+                                            ))
+
+        self.btnRemoveSrcFiles.clicked.connect(lambda : self.sourceFileModel.removeFiles(
+            [n.mPath for n in self.selectedSourceFileNodes()]
+        ))
+
+        self.mQgsProjectionSelectionWidget.dialog().setMessage('Set VRT CRS')
+        self.mQgsProjectionSelectionWidget.crsChanged.connect(self.vrtRaster.setCrs)
+
+
+
+    def loadSrcFromMapLayerRegistry(self):
+
+        reg = QgsMapLayerRegistry.instance()
+        for lyr in reg.mapLayers().values():
+            if isinstance(lyr, QgsRasterLayer):
+                self.sourceFileModel.addFile(lyr.source())
+
+
+    def expandNodes(self, treeView, expand):
+        assert isinstance(treeView, QTreeView)
+
+        indices = treeView.selectedIndexes()
+        if len(indices) == 0:
+            treeView.selectAll()
+            indices += treeView.selectedIndexes()
+            treeView.clearSelection()
+        for idx in indices:
+            treeView.setExpanded(idx, expand)
+
+
+    def onVRTModelSelectionChanged(self, selected, deselected):
+
+        self.btnRemoveVirtualBands.setEnabled(selected.count() > 0)
+
+    def selectedSourceFileNodes(self):
+
+        indexes =  self.treeViewSourceFiles.selectionModel().selectedIndexes()
+        selectedFileNodes = [self.sourceFileModel.idx2node(idx) for idx in indexes]
+        return [n for n in selectedFileNodes if isinstance(n, SourceRasterFileNode)]
+
+
+    def saveFile(self):
+        path = self.tbOutputPath.text()
+        ext = os.path.splitext(path)[-1]
+
+        saveBinary = ext != '.vrt'
+        if saveBinary:
+            pathVrt = path+'.vrt'
+        else:
+            pathVrt = path
+
+
+        self.vrtRaster.saveVRT(pathVrt)
+
+
+    def onOutputPathChanged(self, path):
+        assert isinstance(self.buttonBox, QDialogButtonBox)
+        isEnabled = False
+        if len(path) > 0:
+            ext = os.path.splitext(path)[-1].lower()
+            isEnabled = ext in ['.vrt', '.bsq', '.tif', '.tiff']
+
+        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(isEnabled)
+
+
+    def onSrcModelSelectionChanged(self, selected, deselected):
+
+        self.btnRemoveSrcFiles.setEnabled(len(self.selectedSourceFileNodes()) > 0)
+        s = ""
 
 
     def addSourceFiles(self, files):
@@ -855,21 +1534,85 @@ class VirtualRasterBuilderWidget(QFrame, loadUi('vrtbuilder.ui')):
         """
         self.sourceFileModel.addFiles(files)
 
+    def updateSummary(self):
+
+
+        self.tbSourceFileCount.setText('{}'.format(len(self.vrtRaster.sourceRaster())))
+        self.tbVRTBandCount.setText('{}'.format(len(self.vrtRaster)))
+
+        crs = self.vrtRaster.crs()
+        if isinstance(crs, QgsCoordinateReferenceSystem):
+            self.previewMap.setDestinationCrs(crs)
+            if crs != self.mQgsProjectionSelectionWidget.crs():
+                self.mQgsProjectionSelectionWidget.setCrs(crs)
+        self.mapRefresh()
+
+    def mapCanvasContextMenuEvent(self, event):
+        menu = QMenu()
+        action = menu.addAction('Refresh')
+        action.triggered.connect(self.previewMap.refresh)
+
+        action = menu.addAction('Reset')
+        action.triggered.connect(self.mapRefresh)
+
+        menu.exec_(event.globalPos())
+
+
+    def vrtTreeViewContextMenuEvent(self, event):
+
+        idx = self.treeViewVRT.indexAt(event.pos())
+        if not idx.isValid():
+            pass
+
+        nodes = self.vrtBuilderModel.indexes2nodes(self.treeViewVRT.selectedIndexes())
+        menu = QMenu(self.treeViewVRT)
+        a = menu.addAction('Remove')
+        a.triggered.connect(lambda: self.vrtBuilderModel.removeNodes(nodes))
+
+        a = menu.addAction('Move map to')
+
+        menu.exec_(self.treeViewVRT.viewport().mapToGlobal(event.pos()))
+        """
+        if (menu & & menu->actions().count() != 0 )
+        menu->exec (mapToGlobal(event->pos() ) );
+        delete
+        menu;
+        """
+
+    def mapRefresh(self):
+        extent = self.vrtRasterLayer.extent()
+        if isinstance(extent, QgsRectangle):
+            extent.scale(1.05)
+            self.previewMap.setExtent(extent)
+            self.previewMap.refresh()
+
+
+        s = ""
 if __name__ == '__main__':
     import site, sys
     #add site-packages to sys.path as done by enmapboxplugin.py
 
-    from timeseriesviewer import utils
+    from timeseriesviewer import utils, DIR_EXAMPLES
     qgsApp = utils.initQgisApplication()
 
-    from example.Images import Img_2014_03_20_LC82270652014079LGN00_BOA, Img_2014_04_29_LE72270652014119CUB00_BOA
+    from example.Images import Img_2014_03_20_LC82270652014079LGN00_BOA, re_2014_08_17
+
+    #r = VRTRaster()
+    #r.addFilesAsStack([Img_2014_03_20_LC82270652014079LGN00_BOA, Img_2014_04_29_LE72270652014119CUB00_BOA])
+    #print(r.sourceRasterBounds())
 
-    w = VirtualRasterBuilderWidget()
-    w.addSourceFiles([Img_2014_03_20_LC82270652014079LGN00_BOA, Img_2014_04_29_LE72270652014119CUB00_BOA])
 
-    w.vrtBuilder.addVirtualBand(VirtualBand(name='Band 1'))
+    w = VRTBuilderWidget()
+    p1 = r'S:/temp/temp_ar/4benjamin/05_CBERS/CBERS_4_MUX_20150603_167_107_L4_BAND5_GRID_SURFACE.tif'
+    p2 = r'D:/Repositories/QGIS_Plugins/hub-timeseriesviewer/example/Images/re_2014-06-25.tif'
+    p3 = r'D:/Repositories/QGIS_Plugins/hub-timeseriesviewer/example/Images/2014-08-27_LC82270652014239LGN00_BOA.tif'
+    w.addSourceFiles([p1, p2, p3])
+    w.vrtRaster.addFilesAsStack([p1, p2, p3])
 
     w.show()
+    pathTmp = os.path.join(DIR_EXAMPLES, 'test.vrt')
+    w.vrtRaster.saveVRT(pathTmp)
+    # w.vrtBuilder.addVirtualBand(VRTRasterBand(name='Band 1'))
 
     qgsApp.exec_()
     qgsApp.exitQgis()