From 134732780da79f59c4e28887edb67a635d494af9 Mon Sep 17 00:00:00 2001 From: Benjamin Jakimow <no.email> Date: Wed, 21 Dec 2016 02:46:46 +0100 Subject: [PATCH] refactoring action based event handling --- timeseriesviewer/main.py | 251 ++++++++++++++------------------- timeseriesviewer/timeseries.py | 161 ++++++++++++++++----- 2 files changed, 234 insertions(+), 178 deletions(-) diff --git a/timeseriesviewer/main.py b/timeseriesviewer/main.py index 190315b1..09d5184c 100644 --- a/timeseriesviewer/main.py +++ b/timeseriesviewer/main.py @@ -221,51 +221,63 @@ class TimeSeriesItemModel(QAbstractItemModel): class BandView(QObject): - removeView = pyqtSignal(object) + sigAddBandView = pyqtSignal(object) + sigRemoveBandView = pyqtSignal(object) - def __init__(self, TS, recommended_bands=None): + def __init__(self, TS, recommended_bands=None, parent=None, showSensorNames=True): super(BandView, self).__init__() + self.ui = widgets.BandViewUI(parent) + self.ui.create() + + #forward actions with reference to this band view + self.ui.actionAddBandView.triggered.connect(lambda : self.sigAddBandView.emit(self)) + self.ui.actionRemoveBandView.triggered.connect(lambda: self.sigRemoveBandView.emit(self)) + assert type(TS) is TimeSeries - self.representation = collections.OrderedDict() + self.sensorViews = collections.OrderedDict() self.TS = TS - self.TS.sensorAdded.connect(self.checkSensors) - self.TS.changed.connect(self.checkSensors) + self.TS.sigSensorAdded.connect(self.addSensor) + self.TS.sigChanged.connect(self.removeSensor) - self.Sensors = self.TS.Sensors + self.mShowSensorNames = showSensorNames - import copy - for sensor in self.Sensors: - self.initSensor(copy.deepcopy(sensor)) + for sensor in self.TS.Sensors: + self.addSensor(sensor) + def setTitle(self, title): + self.ui.labelViewName.setText(title) + def showSensorNames(self, b): + assert isinstance(b, bool) + self.mShowSensorNames = b - def checkSensors(self): - represented_sensors = set(self.representation.keys()) - ts_sensors = set(self.TS.Sensors.keys()) + for s,w in self.sensorViews.items(): + w.showSensorName(b) - to_add = ts_sensors - represented_sensors - to_remove = represented_sensors - ts_sensors - for S in to_remove: - self.representation[S].getWidget().close() - self.representation.pop(S) - for S in to_add: - self.initSensor(S) + def removeSensor(self, sensor): + assert type(sensor) is SensorInstrument + assert sensor in self.sensorViews.keys() + self.sensorViews[sensor].close() + self.sensorViews.pop(sensor) - def initSensor(self, sensor): + def addSensor(self, sensor): """ :param sensor: :return: """ assert type(sensor) is SensorInstrument - if sensor not in self.representation.keys(): - x = widgets.ImageChipViewSettings(sensor) - self.representation[sensor] = x - + assert sensor not in self.sensorViews.keys() + w = widgets.ImageChipViewSettings(sensor) + w.showSensorName(self.mShowSensorNames) + self.sensorViews[sensor] = w + i = self.ui.mainLayout.count()-1 + self.ui.mainLayout.insertWidget(i, w.ui) + s = "" - def getWidget(self, sensor): + def getSensorWidget(self, sensor): assert type(sensor) is SensorInstrument - return self.representation[sensor] + return self.sensorViews[sensor] class RenderJob(object): @@ -564,32 +576,6 @@ def Array2Image(d3d): return QImage(d3d.data, ns, nl, QImage.Format_RGB888) -class VerticalLabel(QLabel): - def __init__(self, text): - super(VerticalLabel, self).__init__(text) - self.update() - self.updateGeometry() - - - def paintEvent(self, ev): - p = QPainter(self) - p.rotate(-90) - rgn = QRect(-self.height(), 0, self.height(), self.width()) - align = self.alignment() - self.hint = p.drawText(rgn, align, self.text()) - p.end() - - self.setMaximumWidth(self.hint.height()) - self.setMinimumWidth(0) - self.setMaximumHeight(16777215) - self.setMinimumHeight(self.hint.width()) - - def sizeHint(self): - if hasattr(self, 'hint'): - return QSize(self.hint.height(), self.hint.width()) - else: - return QSize(19, 50) - class ImageChipBuffer(object): @@ -741,14 +727,14 @@ class TimeSeriesViewer: # Create the dialog (after translation) and keep reference from timeseriesviewer.ui.widgets import TimeSeriesViewerUI self.ui = TimeSeriesViewerUI() - D = self.ui + #init empty time series self.TS = TimeSeries() self.hasInitialCenterPoint = False - self.TS.datumAdded.connect(self.ua_datumAdded) - self.TS.changed.connect(self.timeseriesChanged) - self.TS.progress.connect(self.ua_TSprogress) + self.TS.sigTimeSeriesDatumAdded.connect(self.ua_datumAdded) + self.TS.sigChanged.connect(self.timeseriesChanged) + self.TS.sigProgress.connect(self.ua_TSprogress) #init TS model TSM = TimeSeriesTableModel(self.TS) @@ -764,29 +750,36 @@ class TimeSeriesViewer: self.ValidatorPxX = QIntValidator(0,99999) self.ValidatorPxY = QIntValidator(0,99999) - D.btn_showPxCoordinate.clicked.connect(lambda: self.showSubsetsStart()) - D.btn_selectByCoordinate.clicked.connect(self.ua_selectByCoordinate) - D.btn_selectByRectangle.clicked.connect(self.ua_selectByRectangle) - D.btn_addBandView.clicked.connect(lambda :self.ua_addBandView()) - - D.btn_addTSImages.clicked.connect(lambda :self.ua_addTSImages()) - D.btn_addTSMasks.clicked.connect(lambda :self.ua_addTSMasks()) - D.btn_loadTSFile.clicked.connect(self.ua_loadTSFile) - D.btn_saveTSFile.clicked.connect(self.ua_saveTSFile) - D.btn_addTSExample.clicked.connect(self.ua_loadExampleTS) + + #connect actions with logic + + #D.btn_showPxCoordinate.clicked.connect(lambda: self.showSubsetsStart()) + D.actionSelectCenter.triggered.connect(self.ua_selectByCoordinate) + D.actionSelectArea.triggered.connect(self.ua_selectByRectangle) + + D.actionAddBandView.triggered.connect(self.ua_addBandView) + #D.actionRemoveBandView.triggered.connect(self.ua_removeBandView) + + D.actionAddTSD.triggered.connect(self.ua_addTSImages) + D.actionRemoveTSD.triggered.connect(self.ua_removeTSD) + + D.actionLoadTS.triggered.connect(self.ua_loadTSFile) + D.actionClearTS.triggered.connect(self.ua_clear_TS) + D.actionSaveTS.triggered.connect(self.ua_saveTSFile) + D.actionAddTSExample.triggered.connect(self.ua_loadExampleTS) + D.btn_labeling_clear.clicked.connect(D.tb_labeling_text.clear) - D.actionAdd_Images.triggered.connect(lambda :self.ua_addTSImages()) - D.actionAdd_Masks.triggered.connect(lambda :self.ua_addTSMasks()) - D.actionLoad_Time_Series.triggered.connect(self.ua_loadTSFile) - D.actionSave_Time_Series.triggered.connect(self.ua_saveTSFile) - D.actionLoad_Example_Time_Series.triggered.connect(self.ua_loadExampleTS) D.actionAbout.triggered.connect( \ lambda: QMessageBox.about(self.ui, 'SenseCarbon TimeSeriesViewer', 'A viewer to visualize raster time series data')) - D.btn_removeTSD.clicked.connect(lambda : self.ua_removeTSD(None)) - D.btn_removeTS.clicked.connect(self.ua_clear_TS) - D.sliderDOI.sliderMoved.connect(self.setDOILabel) + D.actionFirstTSD.triggered.connect(lambda: self.setDOISliderValue('first')) + D.actionLastTSD.triggered.connect(lambda: self.setDOISliderValue('last')) + D.actionNextTSD.triggered.connect(lambda: self.setDOISliderValue('next')) + D.actionPreviousTSD.triggered.connect(lambda: self.setDOISliderValue('previous')) + + + D.sliderDOI.valueChanged.connect(self.setDOI) D.spinBox_ncpu.setRange(0, multiprocessing.cpu_count()) @@ -796,22 +789,37 @@ class TimeSeriesViewer: if self.iface: self.canvas = self.iface.mapCanvas() - self.RectangleMapTool = qgis_add_ins.RectangleMapTool(self.canvas) self.RectangleMapTool.rectangleDrawed.connect(self.setSpatialSubset) self.PointMapTool = qgis_add_ins.PointMapTool(self.canvas) self.PointMapTool.coordinateSelected.connect(self.setSpatialSubset) - + else: + D.btnSelectCenterCoordinate.setEnabled(False) + D.btnSelectArea.setEnabled(False) #self.RectangleMapTool.connect(self.ua_selectByRectangle_Done) - self.ICP = self.ui.scrollArea_imageChip_content.layout() - self.ui.scrollArea_bandViews_content.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) - self.BVP = self.ui.scrollArea_bandViews_content.layout() + self.ICP = D.scrollAreaSubsetContent.layout() + D.scrollAreaBandViewsContent.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + self.BVP = self.ui.scrollAreaBandViewsContent.layout() self.check_enabled() s = "" - def setDOILabel(self, i): + + def setDOISliderValue(self, key): + ui = self.ui + v = ui.sliderDOI.value() + if key == 'first': + v = ui.sliderDOI.minimum() + elif key == 'last': + v = ui.sliderDOI.maximum() + elif key =='next': + v = min([v+1,ui.sliderDOI.maximum()]) + elif key =='previous': + v = max([v - 1, ui.sliderDOI.minimum()]) + ui.sliderDOI.setValue(v) + + def setDOI(self, i): TSD = self.TS.data[i-1] self.ui.labelDOI.setText(str(TSD.date)) s = "" @@ -847,8 +855,6 @@ class TimeSeriesViewer: self.TS.loadFromFile(path) M.endResetModel() - self.refreshBandViews() - self.check_enabled() def ua_saveTSFile(self): @@ -955,9 +961,9 @@ class TimeSeriesViewer: hasQGIS = qgis_available #D.tabWidget_viewsettings.setEnabled(hasTS) - D.btn_showPxCoordinate.setEnabled(hasTS and hasTSV) - D.btn_selectByCoordinate.setEnabled(hasQGIS) - D.btn_selectByRectangle.setEnabled(hasQGIS) + #D.btn_showPxCoordinate.setEnabled(hasTS and hasTSV) + D.btnSelectCenterCoordinate.setEnabled(hasQGIS) + D.btnSelectArea.setEnabled(hasQGIS) @@ -1081,7 +1087,7 @@ class TimeSeriesViewer: viewList = list() j = 1 for view in self.BAND_VIEWS: - viewWidget = view.getWidget(TSD.sensor) + viewWidget = view.getSensorWidget(TSD.sensor) layerRenderer = viewWidget.layerRenderer() #imageLabel = QLabel() @@ -1122,7 +1128,7 @@ class TimeSeriesViewer: if sensor not in LUT_RENDERER.keys(): LUT_RENDERER[sensor] = [] LUT_RENDERER[sensor].append( - view.getWidget(sensor).layerRenderer() + view.getSensorWidget(sensor).layerRenderer() ) @@ -1187,72 +1193,31 @@ class TimeSeriesViewer: self.check_enabled() - def ua_addTSMasks(self, files=None): - - if files is None: - files = QFileDialog.getOpenFileNames() - - l = len(files) - if l > 0: - M = self.ui.tableView_TimeSeries.model() - M.beginResetModel() - self.TS.addMasks(files, raise_errors=False) - M.endResetModel() - self.check_enabled() + def ua_addBandView(self): + bandView = BandView(self.TS, showSensorNames=len(self.BAND_VIEWS)==0) + bandView.sigAddBandView.connect(self.ua_addBandView) + bandView.sigRemoveBandView.connect(self.ua_removeBandView) - def ua_addBandView(self): - bandView = BandView(self.TS) #bandView.removeView.connect(self.ua_removeBandView) self.BAND_VIEWS.append(bandView) - self.refreshBandViews() - - - def refreshBandViews(self): - if len(self.BAND_VIEWS) == 0 and len(self.TS) > 0: - self.ua_addBandView() # add two bandviews by default - self.ua_addBandView() - - self.clearLayoutWidgets(self.BVP) - - for i, BV in enumerate(self.BAND_VIEWS): - W = QWidget() - - hl = QHBoxLayout() - hl.setSpacing(2) - hl.setMargin(0) + self.BVP.addWidget(bandView.ui) + bandView.setTitle('#{}'.format(len(self.BAND_VIEWS))) - textLabel = VerticalLabel('View {}'.format(i+1)) - #textLabel = QLabel('View {}'.format(i+1)) - textLabel.setToolTip('') - textLabel.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed) - hl.addWidget(textLabel) - - for S in self.TS.Sensors.keys(): - w = BV.getWidget(S).ui - if i > 0: - w.setTitle(None) #show sensor name only on top - w.setMaximumSize(w.size()) - #w.setMinimumSize(w.size()) - w.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.MinimumExpanding) - #w.setBands(band_recommendation) - hl.addWidget(w) - s = "" - - hl.addItem(QSpacerItem(1,1)) - W.setLayout(hl) - self.BVP.addWidget(W) - self.BVP.addItem(QSpacerItem(1, 1)) - self.check_enabled() - + def ua_removeBandView(self, bandView): + #bandView.ui.setHidden(True) + self.BAND_VIEWS.remove(bandView) + self.BVP.removeWidget(bandView.ui) + bandView.ui.close() + self.BAND_VIEWS[0].showSensorNames(True) + for i, bv in enumerate(self.BAND_VIEWS): + bv.setTitle('#{}'.format(i+1)) + #self.ui.scrollAreaBandViewsContent.update() - def ua_removeBandView(self, w): - self.BAND_VIEWS.remove(w) - self.refreshBandViews() def ua_clear_TS(self): #remove views diff --git a/timeseriesviewer/timeseries.py b/timeseriesviewer/timeseries.py index a2f56c99..2c1903f6 100644 --- a/timeseriesviewer/timeseries.py +++ b/timeseriesviewer/timeseries.py @@ -22,7 +22,27 @@ def transformGeometry(geom, crsSrc, crsDst, trans=None): assert isinstance(trans, QgsCoordinateTransform) return trans.transform(geom) +METRIC_EXPONENTS = { + "nm":-9,"um": -6, "mm":-3, "cm":-2, "dm":-1, "m": 0,"hm":2, "km":3 +} +#add synonyms +METRIC_EXPONENTS['nanometers'] = METRIC_EXPONENTS['nm'] +METRIC_EXPONENTS['micrometers'] = METRIC_EXPONENTS['um'] +METRIC_EXPONENTS['millimeters'] = METRIC_EXPONENTS['mm'] +METRIC_EXPONENTS['centimeters'] = METRIC_EXPONENTS['cm'] +METRIC_EXPONENTS['decimeters'] = METRIC_EXPONENTS['dm'] +METRIC_EXPONENTS['meters'] = METRIC_EXPONENTS['m'] +METRIC_EXPONENTS['hectometers'] = METRIC_EXPONENTS['hm'] +METRIC_EXPONENTS['kilometers'] = METRIC_EXPONENTS['km'] +def convertMetricUnit(value, u1, u2): + assert u1 in METRIC_EXPONENTS.keys() + assert u2 in METRIC_EXPONENTS.keys() + + e1 = METRIC_EXPONENTS[u1] + e2 = METRIC_EXPONENTS[u2] + + return value * 10**(e1-e2) class SensorInstrument(QObject): @@ -37,12 +57,15 @@ class SensorInstrument(QObject): , (5, 5., 5.): 'RE 5m' \ } - """ - def fromGDALDataSet(self, ds): - assert isinstance(ds, gdal.Dataset) - nb = ds.RasterCount - """ + LUT_Wavelenghts = dict({'B':480, + 'G':570, + 'R':660, + 'nIR':850, + 'swIR':1650, + 'swIR1':1650, + 'swIR2':2150 + }) """ Describes a Sensor Configuration """ @@ -78,12 +101,10 @@ class SensorInstrument(QObject): assert self.px_size_x > 0 assert self.px_size_y > 0 - wavelengths = None - #todo: find wavelength - if wavelengths is not None: - assert len(wavelengths) == self.nb - - self.wavelengths = wavelengths + #find wavelength + wl, wlu = parseWavelength(refLyr) + self.wavelengths = np.asarray(wl) + self.wavelengthUnits = wlu if sensor_name is None: id = (self.nb, self.px_size_x, self.px_size_y) @@ -95,6 +116,35 @@ class SensorInstrument(QObject): self.hashvalue = hash(','.join(self.bandNames)) + + def bandClosestToWavelength(self, wl, wl_unit='nm'): + """ + Returns the band number (>=1) of the band closest to wavelength wl + :param wl: + :param wl_unit: + :return: + """ + if not self.wavelengthsDefined(): + return None + + if wl in SensorInstrument.LUT_Wavelenghts.keys(): + wl_unit = 'nm' + wl = SensorInstrument.LUT_Wavelenghts[wl] + + wl = float(wl) + if self.wavelengthUnits != wl_unit: + wl = convertMetricUnit(wl, wl_unit, self.wavelengthUnits) + + + return np.argmin(np.abs(self.wavelengths - wl))+1 + + + + + def wavelengthsDefined(self): + return self.wavelengths is not None and \ + self.wavelengthUnits is not None + def __eq__(self, other): return self.nb == other.nb and \ self.px_size_x == other.px_size_x and \ @@ -358,15 +408,15 @@ class TimeSeriesDatum(QObject): class TimeSeries(QObject): - datumAdded = pyqtSignal(TimeSeriesDatum) + sigTimeSeriesDatumAdded = pyqtSignal(TimeSeriesDatum) #fire when a new sensor configuration is added - sensorAdded = pyqtSignal(SensorInstrument, name='sensorAdded') - - changed = pyqtSignal() - progress = pyqtSignal(int,int,int, name='progress') - closed = pyqtSignal() - error = pyqtSignal(object) + sigSensorAdded = pyqtSignal(SensorInstrument) + sigSensorRemoved = pyqtSignal(SensorInstrument) + sigChanged = pyqtSignal() + sigProgress = pyqtSignal(int, int, int, name='progress') + sigClosed = pyqtSignal() + sigError = pyqtSignal(object) def __init__(self, imageFiles=None, maskFiles=None): QObject.__init__(self) @@ -465,7 +515,7 @@ class TimeSeries(QObject): def _callback_error(self, error): six.print_(error, file=sys.stderr) - self.error.emit(error) + self.sigError.emit(error) self._callback_progress() def _callback_spatialchips(self, results): @@ -474,12 +524,12 @@ class TimeSeries(QObject): def _callback_progress(self): self._callback_progress_done += 1 - self.progress.emit(0, self._callback_progress_done, self._callback_progress_max) + self.sigProgress.emit(0, self._callback_progress_done, self._callback_progress_max) if self._callback_progress_done >= self._callback_progress_max: self._callback_progress_done = 0 self._callback_progress_max = 0 - self.progress.emit(0,0,1) + self.sigProgress.emit(0, 0, 1) def getSpatialChips_parallel(self, bbWkt, srsWkt, TSD_band_list, ncpu=1): assert type(bbWkt) is str and type(srsWkt) is str @@ -529,7 +579,7 @@ class TimeSeries(QObject): assert isinstance(files, list) l = len(files) - self.progress.emit(0,0,l) + self.sigProgress.emit(0, 0, l) for i, file in enumerate(files): try: @@ -537,10 +587,10 @@ class TimeSeries(QObject): except: pass - self.progress.emit(0,i+1,l) + self.sigProgress.emit(0, i + 1, l) - self.progress.emit(0,0,1) - self.changed.emit() + self.sigProgress.emit(0, 0, 1) + self.sigChanged.emit() def addMask(self, pathMsk, raise_errors=True, mask_value=0, exclude_mask_value=True, _quiet=False): print('Add mask {}...'.format(pathMsk)) @@ -551,7 +601,7 @@ class TimeSeries(QObject): TSD = self.data[date] if not _quiet: - self.changed.emit() + self.sigChanged.emit() return TSD.setMask(pathMsk, raise_errors=raise_errors, mask_value=mask_value, exclude_mask_value=exclude_mask_value) else: @@ -568,13 +618,13 @@ class TimeSeries(QObject): def clear(self): self.Sensors.clear() del self.data[:] - self.changed.emit() + self.sigChanged.emit() def removeDates(self, TSDs): for TSD in TSDs: self.removeTSD(TSD, _quiet=True) - self.changed.emit() + self.sigChanged.emit() def removeTSD(self, TSD, _quiet=False): @@ -584,8 +634,9 @@ class TimeSeries(QObject): self.data.pop(TSD, None) if len(self.Sensors[S]) == 0: self.Sensors.pop(S) + self.sigSensorRemoved(S) if not _quiet: - self.changed.emit() + self.sigChanged.emit() @@ -614,9 +665,9 @@ class TimeSeries(QObject): #insert sorted bisect.insort(self.data, TSD) #self.data[TSD] = TSD - self.datumAdded.emit(TSD) + self.sigTimeSeriesDatumAdded.emit(TSD) if sensorAdded: - self.sensorAdded.emit(TSD.sensor) + self.sigSensorAdded.emit(TSD.sensor) except: @@ -636,13 +687,13 @@ class TimeSeries(QObject): l = len(files) assert l > 0 - self.progress.emit(0,0,l) + self.sigProgress.emit(0, 0, l) for i, file in enumerate(files): self.addFile(file, _quiet=True) - self.progress.emit(0,i+1,l) + self.sigProgress.emit(0, i + 1, l) - self.progress.emit(0,0,1) - self.changed.emit() + self.sigProgress.emit(0, 0, 1) + self.sigChanged.emit() def __len__(self): @@ -715,6 +766,40 @@ regYYYYDOY = re.compile(r'(19|20)\d{5}') regYYYYMMDD = re.compile(r'(19|20)\d{2}-\d{2}-\d{2}') regYYYY = re.compile(r'(19|20)\d{2}') + + + +def parseWavelength(lyr): + wl = None + wlu = None + assert isinstance(lyr, QgsRasterLayer) + md = [l.split('=') for l in str(lyr.metadata()).splitlines() if 'wavelength' in l.lower()] + #see http://www.harrisgeospatial.com/docs/ENVIHeaderFiles.html for supported wavelength units + regWLU = re.compile('((micro|nano|centi)meters)|(um|nm|mm|cm|m|GHz|MHz)') + for kv in md: + key, value = kv + key = key.lower() + if key == 'center wavelength': + tmp = re.findall('\d*\.\d+|\d+', value) #find floats + if len(tmp) == 0: + tmp = re.findall('\d+', value) #find integers + if len(tmp) == lyr.bandCount(): + wl = [float(w) for w in tmp] + + if key == 'wavelength units': + match = regWLU.search(value) + if match: + wlu = match.group() + + names = ['nanometers','micrometers','millimeters','centimeters','decimenters'] + si = ['nm','um','mm','cm','dm'] + if wlu in names: + wlu = si[names.index(wlu)] + + return wl, wlu + + + def parseAcquisitionDate(text): match = regLandsatSceneID.search(text) if match: @@ -742,3 +827,9 @@ def getDateTime64FromDOY(year, doy): if type(doy) is str: doy = int(doy) return np.datetime64('{:04d}-01-01'.format(year)) + np.timedelta64(doy-1, 'D') + + +if __name__ == '__main__': + + print convertMetricUnit(100, 'cm', 'm') + print convertMetricUnit(1, 'm', 'um') \ No newline at end of file -- GitLab