Commit d4619bc7 authored by Benjamin Jakimow's avatar Benjamin Jakimow
Browse files

profilevisualization.py: fixed error if TemporalProfile coordinate is out of extents7

mapvisualization.py:
- refactored QGIS syncing

EOTimeSeriesViewer:
- added close()
- added setCurrentSource(TimeSeriesSource)

timseries.py:
- fixed sensor mockup in SensorInstrument
- reorderer classes
- speed-up TimeSeries.showTSDs()
- implemented TimeSeries.findSource(...)
- TimeSeriesTreeView context menu can be used to open selected source images in QGIS

create_plugin.py:
- added example directory to plugin folder

eotimeseriesviewer/ui:
- cleanup icons
- modified qgsMapCenter.svg
parent faa3ee5c
......@@ -314,7 +314,6 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
sigCurrentSpectralProfilesChanged = pyqtSignal(list)
sigCurrentTemporalProfilesChanged = pyqtSignal(list)
sigCloses = pyqtSignal()
def __init__(self):
"""Constructor.
......@@ -328,8 +327,7 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
QgisInterface.__init__(self)
QApplication.processEvents()
assert EOTimeSeriesViewer.instance() is None, 'EOTimeSeriesViewer instance already exists.'
EOTimeSeriesViewer._instance = self
self.ui = EOTimeSeriesViewerUI()
......@@ -348,6 +346,7 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
def onClosed():
EOTimeSeriesViewer._instance = None
self.ui.sigAboutToBeClosed.connect(onClosed)
QgsApplication.instance().messageLog().messageReceived.connect(self.logMessage)
......@@ -382,6 +381,7 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
mw.sigMapViewAdded.connect(self.onMapViewAdded)
mw.sigCurrentLocationChanged.connect(self.setCurrentLocation)
self.ui.optionSyncMapCenter.toggled.connect(self.mapWidget().setSyncWithQGISMapCanvas)
tb = self.ui.toolBarTimeControl
assert isinstance(tb, QToolBar)
tb.addAction(mw.actionFirstDate)
......@@ -394,7 +394,7 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
tstv = self.ui.dockTimeSeries.timeSeriesTreeView
assert isinstance(tstv, TimeSeriesTreeView)
tstv.sigMoveToDateRequest.connect(self.setCurrentDate)
tstv.sigMoveToSource.connect(self.setCurrentSource)
self.mCurrentMapLocation = None
self.mCurrentMapSpectraLoading = 'TOP'
......@@ -684,6 +684,9 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
if len(files) > 0:
self.addTimeSeriesImages(files)
def close(self):
self.ui.close()
def actionZoomActualSize(self):
return self.ui.actionZoomPixelScale
......@@ -696,8 +699,16 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
def actionZoomOut(self):
return self.ui.actionZoomOut
def setCurrentSource(self, tss: TimeSeriesSource):
"""
Moves the map view to a TimeSeriesSource
"""
tss = self.timeSeries().findSource(tss)
if isinstance(tss, TimeSeriesSource):
self.ui.mMapWidget.setCurrentDate(tss.timeSeriesDate())
self.ui.mMapWidget.setSpatialExtent(tss.spatialExtent())
def setCurrentDate(self, tsd):
def setCurrentDate(self, tsd: TimeSeriesDate):
"""
Moves the viewport of the scroll window to a specific TimeSeriesDate
:param tsd: TimeSeriesDate or numpy.datetime64
......@@ -773,8 +784,6 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
if self.ui.optionSyncMapCenter.isChecked():
self.ui.mMapWidget.syncQGISCanvasCenter(qgisChanged)
self.ui.mMapWidget.sigSpatialExtentChanged.connect(lambda: onSyncRequest(False))
iface.mapCanvas().extentsChanged.connect(lambda: onSyncRequest(True))
def onShowSettingsDialog(self):
......@@ -1183,6 +1192,7 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
Loads an example time series
:param n: int, max. number of images to load. Useful for developer test-cases
"""
import example.Images
exampleDataDir = os.path.dirname(example.__file__)
rasterFiles = list(file_search(exampleDataDir, '*.tif', recursive=True))
......
......@@ -850,6 +850,9 @@ class MapWidget(QFrame):
self.mGrid.setContentsMargins(0, 0, 0, 0)
self.mSyncLock = False
self.mSyncQGISMapCanvasCenter: bool = False
self.mLastQGISMapCanvasCenter: SpatialPoint = None
self.mLastEOTSVMapCanvasCenter: SpatialPoint = None
self.mMaxNumberOfCachedLayers = 0
......@@ -1024,6 +1027,9 @@ class MapWidget(QFrame):
"""
Calls the timedRefresh() routine for all MapCanvases
"""
if self.mSyncQGISMapCanvasCenter:
self.syncQGISCanvasCenter()
for c in self.mapCanvases():
assert isinstance(c, MapCanvas)
c.timedRefresh()
......@@ -1308,41 +1314,68 @@ class MapWidget(QFrame):
"""
return self.mMapViews[:]
def syncQGISCanvasCenter(self, qgisChanged:bool):
def setSyncWithQGISMapCanvas(self, b: bool):
assert isinstance(b, bool)
self.mSyncQGISMapCanvasCenter = b
def syncQGISCanvasCenter(self):
if self.mSyncLock:
return
iface = qgis.utils.iface
assert isinstance(iface, QgisInterface)
if not isinstance(iface, QgisInterface):
return
c = iface.mapCanvas()
if not isinstance(c, QgsMapCanvas):
if not isinstance(c, QgsMapCanvas) or len(self.mapCanvases()) == 0:
return
tsvCenter = self.spatialExtent().spatialCenter()
qgsCenter = SpatialExtent.fromMapCanvas(c).spatialCenter()
if qgisChanged:
# change EOTSV
if tsvCenter.crs().isValid():
self.mSyncLock = True
qgsCenter = qgsCenter.toCrs(tsvCenter.crs())
if isinstance(qgsCenter, SpatialPoint):
self.setSpatialCenter(qgsCenter)
QApplication.processEvents()
self.mSyncLock = False
def mapTolerance(canvas: QgsMapCanvas) -> QgsVector:
m2p = canvas.mapSettings().mapToPixel()
return m2p.toMapCoordinates(1, 1) - m2p.toMapCoordinates(0, 0)
recentQGISCenter = SpatialPoint.fromMapCanvasCenter(c)
recentEOTSVCenter = self.spatialCenter()
if not isinstance(self.mLastQGISMapCanvasCenter, SpatialPoint):
self.mLastQGISMapCanvasCenter = SpatialPoint.fromMapCanvasCenter(c)
else:
self.mLastEOTSVMapCanvasCenter = self.mLastEOTSVMapCanvasCenter.toCrs(c.mapSettings().destinationCrs())
if not isinstance(self.mLastEOTSVMapCanvasCenter, SpatialPoint):
self.mLastEOTSVMapCanvasCenter = self.spatialCenter()
else:
# change QGIS
if qgsCenter.crs().isValid():
self.mSyncLock = True
tsvCenter = tsvCenter.toCrs(qgsCenter.crs())
if isinstance(tsvCenter, SpatialPoint):
c.setCenter(tsvCenter)
QApplication.processEvents()
self.mSyncLock = False
self.mLastEOTSVMapCanvasCenter = self.mLastEOTSVMapCanvasCenter.toCrs(self.crs())
shiftQGIS = recentQGISCenter - self.mLastQGISMapCanvasCenter
shiftEOTSV = recentEOTSVCenter - self.mLastEOTSVMapCanvasCenter
tolQGIS = mapTolerance(c)
tolEOTS = mapTolerance(self.mapCanvases()[0])
shiftedQGIS = shiftQGIS.length() > tolQGIS.length()
shiftedEOTSV = shiftEOTSV.length() > tolEOTS.length()
if not (shiftedEOTSV or shiftedQGIS):
return
self.mSyncLock = True
if shiftedQGIS:
# apply change to EOTSV
self.mLastQGISMapCanvasCenter = recentQGISCenter
newCenterEOTSV = recentQGISCenter.toCrs(self.crs())
self.mLastEOTSVMapCanvasCenter = newCenterEOTSV
if isinstance(newCenterEOTSV, SpatialPoint):
self.setSpatialCenter(newCenterEOTSV)
elif shiftedEOTSV:
# apply change to QGIS
self.mLastEOTSVMapCanvasCenter = recentEOTSVCenter
newCenterQGIS = recentEOTSVCenter.toCrs(c.mapSettings().destinationCrs())
self.mLastQGISMapCanvasCenter = newCenterQGIS
if isinstance(newCenterQGIS, SpatialPoint):
c.setCenter(newCenterQGIS)
self.mSyncLock = False
def _createMapCanvas(self)->MapCanvas:
mapCanvas = MapCanvas()
......
......@@ -1890,7 +1890,8 @@ class SpectralTemporalVisualization(QObject):
tssExtent = tss.spatialExtent()
for TP in TemporalProfiles:
assert isinstance(TP, TemporalProfile)
if tssExtent.contains(TP.coordinate().toCrs(tssExtent.crs())):
tpCoord = TP.coordinate().toCrs(tssExtent.crs())
if isinstance(tpCoord, SpatialPoint) and tssExtent.contains(tpCoord):
intersectingTPs.append(TP)
if len(intersectingTPs) > 0:
......
......@@ -251,7 +251,8 @@ class SensorInstrument(QObject):
from eotimeseriesviewer.tests import TestObjects
import uuid
path = '/vsimem/mockupImage.{}.bsq'.format(uuid.uuid4())
self.mMockupDS = TestObjects.createRasterDataset(path=path, nb=self.nb, eType=self.dataType, ns=2, nl=2)
drv: gdal.Driver = gdal.GetDriverByName('ENVI')
self.mMockupDS = drv.Create(path, 2, 2, self.nb, eType=self.dataType)
if self.wl is not None:
self.mMockupDS.SetMetadataItem('wavelength', '{{{}}}'.format(','.join(str(wl) for wl in self.wl)))
if self.wlu is not None:
......@@ -1017,101 +1018,6 @@ class TimeSeriesDate(QAbstractTableModel):
return hash(self.id())
class TimeSeriesTreeView(QTreeView):
sigMoveToDateRequest = pyqtSignal(TimeSeriesDate)
def __init__(self, parent=None):
super(TimeSeriesTreeView, self).__init__(parent)
def contextMenuEvent(self, event: QContextMenuEvent):
"""
Creates and shows the QMenu
:param event: QContextMenuEvent
"""
idx = self.indexAt(event.pos())
node = self.model().data(idx, role=Qt.UserRole)
menu = QMenu(self)
def setUri(paths):
urls = []
paths2 = []
for p in paths:
if os.path.isfile(p):
url = QUrl.fromLocalFile(p)
paths2.append(QDir.toNativeSeparators(p))
else:
url = QUrl(p)
paths2.append(p)
urls.append(url)
md = QMimeData()
md.setText('\n'.join(paths2))
md.setUrls(urls)
QApplication.clipboard().setMimeData(md)
if isinstance(node, TimeSeriesDate):
a = menu.addAction('Show map for {}'.format(node.date()))
a.setToolTip('Shows the map related to this time series date.')
a.triggered.connect(lambda _, tsd=node: self.sigMoveToDateRequest.emit(tsd))
menu.addSeparator()
a = menu.addAction('Copy path(s)')
a.triggered.connect(lambda _, paths=node.sourceUris(): setUri(paths))
a.setToolTip('Copy path to cliboard')
if isinstance(node, TimeSeriesSource):
a = menu.addAction('Copy path')
a.triggered.connect(lambda _, paths=[node.uri()]: setUri(paths))
a.setToolTip('Copy path to cliboard')
a = menu.addAction('Copy value(s)')
a.triggered.connect(lambda: self.onCopyValues())
a = menu.addAction('Hide date(s)')
a.setToolTip('Hides the selected time series dates.')
a.triggered.connect(lambda: self.onSetCheckState(Qt.Unchecked))
a = menu.addAction('Show date(s)')
a.setToolTip('Shows the selected time series dates.')
a.triggered.connect(lambda: self.onSetCheckState(Qt.Checked))
menu.popup(QCursor.pos())
def onSetCheckState(self, checkState):
"""
Sets a ChecState to all selected rows
:param checkState: Qt.CheckState
"""
indices = self.selectionModel().selectedIndexes()
rows = sorted(list(set([i.row() for i in indices])))
model = self.model()
if isinstance(model, QSortFilterProxyModel):
for r in rows:
idx = model.index(r, 0)
model.setData(idx, checkState, Qt.CheckStateRole)
def onCopyValues(self, delimiter='\t'):
"""
Copies selected cell values to the clipboard
"""
indices = self.selectionModel().selectedIndexes()
model = self.model()
if isinstance(model, QSortFilterProxyModel):
from collections import OrderedDict
R = OrderedDict()
for idx in indices:
if not idx.row() in R.keys():
R[idx.row()] = []
R[idx.row()].append(model.data(idx, Qt.DisplayRole))
info = []
for k, values in R.items():
info.append(delimiter.join([str(v) for v in values]))
info = '\n'.join(info)
QApplication.clipboard().setText(info)
class DateTimePrecision(enum.Enum):
"""
......@@ -1515,29 +1421,25 @@ class TimeSeries(QAbstractItemModel):
self.endInsertRows()
return tsd
def showTSDs(self, tsds:list, b:bool=True):
def showTSDs(self, tsds: list, b: bool = True):
tsds = [t for t in tsds if t in self]
minRow = None
maxRow = None
for t in tsds:
col = 0
idxMin = None
idxMax = None
for row, tsd in enumerate(self):
if tsd not in tsds:
continue
idx = self.tsdToIdx(t)
if minRow is None:
minRow = idx.row()
maxRow = idx.row()
idx = self.index(row, col)
if idxMin is None:
idxMin = idxMax = idx
else:
minRow = min(minRow, idx.row())
maxRow = min(maxRow, idx.row())
assert isinstance(t, TimeSeriesDate)
t.setVisibility(b)
if minRow:
ul = self.index(minRow, 0)
lr = self.index(maxRow, 0)
self.dataChanged.emit(ul, lr, [Qt.CheckStateRole])
idxMax = idx
tsd.setVisibility(b)
if isinstance(idxMin, QModelIndex):
self.dataChanged.emit(idxMin, idxMax, [Qt.CheckStateRole])
self.sigVisibilityChanged.emit()
def hideTSDs(self, tsds):
......@@ -2099,7 +2001,6 @@ class TimeSeries(QAbstractItemModel):
if role == Qt.BackgroundColorRole and tsd in self.mVisibleDate:
return QColor('yellow')
return None
def setData(self, index: QModelIndex, value: typing.Any, role: int):
......@@ -2124,6 +2025,16 @@ class TimeSeries(QAbstractItemModel):
return result
def findSource(self, tss: TimeSeriesSource) -> TimeSeriesSource:
"""
Returns the first TimeSeriesSource instance that is equal to the TimeSeriesSource.
"""
for tsd in self:
for tssCandidate in tsd:
if tssCandidate == tss:
return tssCandidate
return None
def findDate(self, date)->TimeSeriesDate:
"""
Returns a TimeSeriesDate closest to that in date
......@@ -2154,6 +2065,129 @@ class TimeSeries(QAbstractItemModel):
flags = flags | Qt.ItemIsUserCheckable
return flags
class TimeSeriesTreeView(QTreeView):
sigMoveToDateRequest = pyqtSignal(TimeSeriesDate)
sigMoveToSource = pyqtSignal(TimeSeriesSource)
def __init__(self, parent=None):
super(TimeSeriesTreeView, self).__init__(parent)
def contextMenuEvent(self, event: QContextMenuEvent):
"""
Creates and shows the QMenu
:param event: QContextMenuEvent
"""
idx = self.indexAt(event.pos())
node = self.model().data(idx, role=Qt.UserRole)
selectedTSDs = []
selectedTSSs = []
for idx in self.selectionModel().selectedRows():
node = idx.data(Qt.UserRole)
if isinstance(node, TimeSeriesDate):
selectedTSDs.append(node)
selectedTSSs.extend(node[:])
if isinstance(node, TimeSeriesSource):
selectedTSSs.append(node)
selectedTSSs = sorted(set(selectedTSSs))
menu = QMenu(self)
a = menu.addAction('Copy path(s)')
a.setEnabled(len(selectedTSSs) > 0)
a.triggered.connect(lambda _, tss=selectedTSSs: self.setClipboardUris(tss))
a.setToolTip('Copy path(s) to clipboard.')
a = menu.addAction('Copy value(s)')
a.triggered.connect(lambda: self.onCopyValues())
menu.addSeparator()
if isinstance(node, TimeSeriesDate):
a = menu.addAction('Show map for {}'.format(node.date()))
a.setToolTip('Shows the map related to this time series date.')
a.triggered.connect(lambda *args, tsd=node: self.sigMoveToDateRequest.emit(tsd))
menu.addSeparator()
elif isinstance(node, TimeSeriesSource):
a = menu.addAction('Show {}'.format(node.name()))
a.setToolTip('Shows the map with {} and zooms to'.format(node.uri()))
a.triggered.connect(lambda *args, tss=node: self.sigMoveToSource.emit(tss))
menu.addSeparator()
a = menu.addAction('Hide date(s)')
a.setToolTip('Hides the selected time series dates.')
a.triggered.connect(lambda *args, tsds=selectedTSDs: self.timeseries().showTSDs(tsds, False))
a = menu.addAction('Show date(s)')
a.setToolTip('Shows the selected time series dates.')
a.triggered.connect(lambda *args, tsds=selectedTSDs: self.timeseries().showTSDs(tsds, True))
menu.addSeparator()
a = menu.addAction('Open in QGIS')
a.setToolTip('Adds the selected images to the QGIS map canvas')
a.triggered.connect(lambda *args, tss=selectedTSSs: self.openInQGIS(tss))
menu.popup(QCursor.pos())
def openInQGIS(self, tssList: typing.List[TimeSeriesSource]):
import qgis.utils
iface = qgis.utils.iface
if isinstance(iface, QgisInterface):
layers = [QgsRasterLayer(tss.uri(), tss.name()) for tss in tssList]
QgsProject.instance().addMapLayers(layers, True)
def setClipboardUris(self, tssList: typing.List[TimeSeriesSource]):
urls = []
paths = []
for tss in tssList:
uri = tss.uri()
if os.path.isfile(uri):
url = QUrl.fromLocalFile(uri)
paths.append(QDir.toNativeSeparators(uri))
else:
url = QUrl(uri)
paths.append(uri)
urls.append(url)
md = QMimeData()
md.setText('\n'.join(paths))
md.setUrls(urls)
QApplication.clipboard().setMimeData(md)
def timeseries(self) -> TimeSeries:
return self.model().sourceModel()
def onSetCheckState(self, tsds: typing.List[TimeSeriesDate], checkState: Qt.CheckStateRole):
"""
Sets a ChecState to all selected rows
:param checkState: Qt.CheckState
"""
def onCopyValues(self, delimiter='\t'):
"""
Copies selected cell values to the clipboard
"""
indices = self.selectionModel().selectedIndexes()
model = self.model()
if isinstance(model, QSortFilterProxyModel):
from collections import OrderedDict
R = OrderedDict()
for idx in indices:
if not idx.row() in R.keys():
R[idx.row()] = []
R[idx.row()].append(model.data(idx, Qt.DisplayRole))
info = []
for k, values in R.items():
info.append(delimiter.join([str(v) for v in values]))
info = '\n'.join(info)
QApplication.clipboard().setText(info)
regSensorName = re.compile(r'(SATELLITEID|(sensor|product)[ _]?(type|name))', re.IGNORECASE)
def sensorName(dataset:gdal.Dataset)->str:
"""
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>136</height>
</rect>
</property>
<property name="windowTitle">
<string>Add Sentinel 2 Products</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>File(s)</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QgsFileWidget" name="mQgsFileWidget">
<property name="dialogTitle">
<string>Sentinel File(s)</string>
</property>
<property name="filter">
<string>*MTD_MSIL*.xml</string>
</property>
<property name="storageMode">
<enum>QgsFileWidget::GetMultipleFiles</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Subdataset</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cbSubDataSet"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QgsFileWidget</class>
<extends>QWidget</extends>
<header>qgsfilewidget.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>