Commit 6b879b44 authored by Benjamin Jakimow's avatar Benjamin Jakimow
Browse files

cleanup repo

modified bitbucket-pipelines.yml

EOTimeSeriesViewer:
- added / modifed QgisInterface actions

mapvisualization.py:
- modified context menu of MapViewLayerTreeViewMenuProvider
- MapView: added currentMapCanvas logic

create_plugin.py: added option -z --zipfilename to create none-generic plugin zipfiles
parent 9b8e6d25
......@@ -29,23 +29,7 @@ pipelines:
- export PYTHONPATH="${PYTHONPATH}:$(pwd)"
- python3 -m pip install -r requirements_dev.txt
- python3 scripts/setup_repository.py
- set +e # remove or set -e to exit at first error
- python3 -m coverage run --rcfile=.coveragec tests/test_fileFormatLoading.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_inmemorydata.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_labeling.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_layerproperties.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_main.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_mapcanvas.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_maptools.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_mapvisualization.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_qgis_environment.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_qgis_interaction.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_resources.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_settings.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_stackedbandinput.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_temporalprofiles.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_timeseries.py
- python3 -m coverage run --rcfile=.coveragec --append tests/test_utils.py
- python3 -m coverage run -m unittest discover -s tests
- python3 -m coverage report
definitions:
......
......@@ -543,6 +543,9 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
assert isinstance(self.ui.mActionSelectFeatures, QAction)
initMapToolAction(self.ui.mActionAddFeature, MapTools.AddFeature)
self.ui.mActionZoomToLayer.triggered.connect(self.onZoomToLayer)
self.ui.optionSelectFeaturesRectangle.triggered.connect(self.onSelectFeatureOptionTriggered)
self.ui.optionSelectFeaturesPolygon.triggered.connect(self.onSelectFeatureOptionTriggered)
self.ui.optionSelectFeaturesFreehand.triggered.connect(self.onSelectFeatureOptionTriggered)
......@@ -817,18 +820,36 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
def close(self):
self.ui.close()
def actionZoomActualSize(self):
def actionCopyLayerStyle(self) -> QAction:
return self.ui.mActionCopyLayerStyle
def actionOpenTable(self) -> QAction:
return self.ui.mActionOpenTable
def actionZoomActualSize(self) -> QAction:
return self.ui.actionZoomPixelScale
def actionZoomFullExtent(self):
def actionZoomFullExtent(self) -> QAction:
return self.ui.actionZoomFullExtent
def actionZoomIn(self):
def actionZoomToLayer(self) -> QAction:
return self.ui.mActionZoomToLayer
def actionZoomIn(self) -> QAction:
return self.ui.actionZoomIn
def actionZoomOut(self):
def actionZoomOut(self) -> QAction:
return self.ui.actionZoomOut
def actionPasteLayerStyle(self) -> QAction:
return self.ui.mActionPasteLayerStyle
def actionLayerProperties(self) -> QAction:
return self.ui.mActionLayerProperties
def actionToggleEditing(self) -> QAction:
return self.ui.mActionToggleEditing
def setCurrentLayer(self, layer:QgsMapLayer):
self.mapWidget().setCurrentLayer(layer)
self.updateCurrentLayerActions()
......@@ -866,6 +887,12 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
"""
return self.mMapLayerStore
def onZoomToLayer(self):
c = self.currentLayer()
if isinstance(c, QgsMapLayer):
self.setSpatialExtent(SpatialExtent.fromLayer(c))
def onMoveToFeature(self, layer:QgsMapLayer, feature:QgsFeature):
"""
Move the spatial center of map visualization to `feature`.
......@@ -1378,10 +1405,8 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
"""
return self.mTimeSeries
# noinspection PyMethodMayBeStatic
def tr(self, message):
"""Get the translation for a string using Qt translation API.
We implement this ourselves since we do not inherit QObject.
:param message: String for translation.
......@@ -1393,18 +1418,17 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
# noinspection PyTypeChecker,PyArgumentList,PyCallByClass
return QCoreApplication.translate('HUBTSV', message)
def unload(self):
"""Removes the plugin menu item and icon """
self.iface.removeToolBarIcon(self.action)
def updateCurrentLayerActions(self, *args):
"""
Enables/disables actions and buttons that relate to the current layer and its current state
"""
debugLog('updateCurrentLayerActions')
layer = self.currentLayer()
debugLog('updateCurrentLayerActions: {}'.format(str(layer)))
isVector = isinstance(layer, QgsVectorLayer)
hasSelectedFeatures = False
for mv in self.mapViews():
......
......@@ -684,7 +684,7 @@ class MapCanvas(QgsMapCanvas):
self.update()
return
for lyr in self.mMapView.layers():
for lyr in self.mMapView.visibleLayers():
assert isinstance(lyr, QgsMapLayer)
if isinstance(lyr, SensorProxyLayer):
......@@ -946,10 +946,8 @@ class MapCanvas(QgsMapCanvas):
a.setEnabled(b)
a.setToolTip('Copy the current layer style to clipboard')
a.triggered.connect(lambda *args, lyr=refRasterLayer: pasteStyleToClipboard(lyr))
a = menu.addAction('Paste Style')
a.setEnabled(False)
clipBoardMime = QApplication.clipboard().mimeData()
if isinstance(clipBoardMime, QMimeData) and 'application/qgis.style' in clipBoardMime.formats():
......@@ -962,7 +960,6 @@ class MapCanvas(QgsMapCanvas):
m = menu.addMenu('Layers...')
visibleLayers = viewPortRasterLayers + viewPortVectorLayers
for mapLayer in visibleLayers:
#sub = m.addMenu(mapLayer.name())
if isinstance(mapLayer, SensorProxyLayer):
......@@ -1197,7 +1194,11 @@ class MapCanvas(QgsMapCanvas):
se = self.spatialExtent()
self.stretchToExtent(se, stretchType='linear_minmax', p=0.05)
def stretchToExtent(self, spatialExtent:SpatialExtent, stretchType='linear_minmax', layer:QgsRasterLayer=None, **stretchArgs):
def stretchToExtent(self,
spatialExtent:SpatialExtent = None,
stretchType='linear_minmax',
layer:QgsRasterLayer = None,
**stretchArgs):
"""
:param spatialExtent: rectangle to get the image statistics for
:param stretchType: ['linear_minmax' (default), 'gaussian']
......@@ -1282,7 +1283,6 @@ class MapCanvas(QgsMapCanvas):
newRenderer = r.clone()
if newRenderer is not None:
if isinstance(layer, SensorProxyLayer):
......
......@@ -65,109 +65,6 @@ KEY_SENSOR_LAYER = 'eotsv/sensorlayer'
def equalTextFormats(tf1:QgsTextFormat, tf2:QgsTextFormat) -> True:
return tf1.toMimeData().text() == tf2.toMimeData().text()
class MapViewLayerTreeViewMenuProvider(QgsLayerTreeViewMenuProvider):
def __init__(self, mapView, view: QgsLayerTreeView, canvas: QgsMapCanvas):
super(MapViewLayerTreeViewMenuProvider, self).__init__()
assert isinstance(view, QgsLayerTreeView)
assert isinstance(canvas, QgsMapCanvas)
self.mLayerTreeView = view
self.mDummyCanvas = canvas
self.mDefActions = QgsLayerTreeViewDefaultActions(self.mLayerTreeView)
self.mMapView = mapView
self.actionAddGroup = self.mDefActions.actionAddGroup()
self.actionRename = self.mDefActions.actionRenameGroupOrLayer()
self.actionRemove = self.mDefActions.actionRemoveGroupOrLayer()
#self.actionZoomToLayer = self.mDefActions.actionZoomToGroup(self.mDummyCanvas)
self.actionCheckAndAllChildren = self.mDefActions.actionCheckAndAllChildren()
self.actionShowFeatureCount = self.mDefActions.actionShowFeatureCount()
#self.actionZoomToLayer = self.mDefActions.actionZoomToLayer(self.mDummyCanvas)
#self.actionZoomToSelected = self.mDefActions.actionZoomToSelection(self.mDummyCanvas)
#self.actionZoomToGroup = self.mDefActions.actionZoomToGroup(self.mDummyCanvas)
self.actionAddEOTSVSpectralProfiles = QAction('Add Spectral Profile Layer')
self.actionAddEOTSVTemporalProfiles = QAction('Add Temporal Profile Layer')
def mapView(self):
return self.mMapView
def layerTreeView(self) -> QgsLayerTreeView:
return self.mLayerTreeView
def layerTree(self) -> QgsLayerTree:
return self.layerTreeModel().rootGroup()
def layerTreeModel(self) -> QgsLayerTreeModel:
return self.layerTreeView().model()
def createContextMenu(self) -> QMenu:
model = self.layerTreeModel()
ltree = self.layerTree()
view = self.layerTreeView()
g = view.currentGroupNode()
l = view.currentLayer()
i = view.currentIndex()
menu = QMenu(view)
isSensorGroup = isinstance(g, QgsLayerTreeGroup) and g.customProperty(KEY_SENSOR_GROUP) in [True, 'true']
if isinstance(self.mapView(), MapView):isSensorLayer = isinstance(l, QgsRasterLayer) and l.customProperty(KEY_SENSOR_LAYER) in [True, 'true']
self.actionRemove.setEnabled(not (isSensorGroup or isSensorLayer))
self.actionAddGroup.setEnabled(not (isSensorGroup or isSensorLayer))
menu.addAction(self.actionAddGroup)
menu.addAction(self.actionRename)
menu.addAction(self.actionRemove)
menu.addSeparator()
menu.addAction(self.actionAddEOTSVSpectralProfiles)
menu.addAction(self.actionAddEOTSVTemporalProfiles)
menu.addSeparator()
centerCanvas = None
visibleCanvases = self.mapView().visibleMapCanvases()
if len(visibleCanvases) > 0:
i = int(len(visibleCanvases) / 2)
centerCanvas = visibleCanvases[i]
a = menu.addAction('Set Properties')
a.triggered.connect(lambda *args, canvas = centerCanvas, lyr = l: self.onSetLayerProperties(lyr, canvas))
a.setEnabled(isinstance(centerCanvas, MapCanvas))
if isinstance(l, QgsVectorLayer):
a = menu.addAction('Open Attribute Table')
a.triggered.connect(lambda *args, lyr=l: self.showAttributeTable(lyr))
from .externals.qps.layerproperties import pasteStyleFromClipboard, pasteStyleToClipboard
a = menu.addAction('Copy Style')
a.setToolTip('Copy the current layer style to clipboard')
a.triggered.connect(lambda *args, c=centerCanvas, lyr=l: pasteStyleToClipboard(lyr))
a = menu.addAction('Paste Style')
a.setEnabled('application/qgis.style' in QApplication.clipboard().mimeData().formats())
a.triggered.connect(lambda *args, lyr=l: pasteStyleFromClipboard(lyr))
#a = menu.addAction('Settings')
#from qps.layerproperties import showLayerPropertiesDialog
#a.triggered.connect(lambda *args, lyr=l:showLayerPropertiesDialog(lyr, self._canvas))
return menu
def showAttributeTable(self, lyr: QgsVectorLayer):
from eotimeseriesviewer.main import EOTimeSeriesViewer
tsv = EOTimeSeriesViewer.instance()
if isinstance(tsv, EOTimeSeriesViewer):
tsv.showAttributeTable(lyr)
s = ""
def onSetLayerProperties(self, lyr:QgsRasterLayer, canvas:QgsMapCanvas):
if isinstance(canvas, MapCanvas):
canvas.onSetLayerProperties(lyr)
class MapViewLayerTreeModel(QgsLayerTreeModel):
"""
Layer Tree as shown in a MapView
......@@ -251,8 +148,8 @@ class MapView(QFrame):
self.mMapLayerTreeViewMenuProvider = MapViewLayerTreeViewMenuProvider(self, self.mLayerTreeView, self.mDummyCanvas)
# register some actions that interact with other GUI elements
self.mMapLayerTreeViewMenuProvider.actionAddEOTSVSpectralProfiles.triggered.connect(self.addSpectralProfileLayer)
self.mMapLayerTreeViewMenuProvider.actionAddEOTSVTemporalProfiles.triggered.connect(self.addTemporalProfileLayer)
#self.mMapLayerTreeViewMenuProvider.actionAddEOTSVSpectralProfiles.triggered.connect(self.addSpectralProfileLayer)
#self.mMapLayerTreeViewMenuProvider.actionAddEOTSVTemporalProfiles.triggered.connect(self.addTemporalProfileLayer)
self.mLayerTreeView.setMenuProvider(self.mMapLayerTreeViewMenuProvider)
self.mLayerTreeView.currentLayerChanged.connect(self.setCurrentLayer)
......@@ -272,7 +169,6 @@ class MapView(QFrame):
fixMenuButtons(self)
def setName(self, name:str):
self.setTitle(name)
......@@ -345,14 +241,14 @@ class MapView(QFrame):
:return:
"""
assert layer is None or isinstance(layer, QgsMapLayer)
if layer in self.layers():
self.mLayerTreeView.setCurrentLayer(layer)
self.mCurrentLayer = layer
if layer not in self.mSensorLayerList:
for c in self.mapCanvases():
c.setCurrentLayer(layer)
else:
s = ""
if layer not in self.mSensorLayerList:
for c in self.mapCanvases():
c.setCurrentLayer(layer)
else:
s = ""
......@@ -438,6 +334,9 @@ class MapView(QFrame):
"""
return not self.actionToggleMapViewHidden.isChecked()
def mapWidget(self):
return self.mMapWidget
def mapCanvases(self) -> typing.List[MapCanvas]:
"""
Returns the MapCanvases related to this map view. Requires that this mapview was added to a MapWidget
......@@ -552,6 +451,16 @@ class MapView(QFrame):
for mapCanvas in self.mapCanvases():
mapCanvas.setStyleSheet(styleOff)
def currentMapCanvas(self) -> MapCanvas:
if not isinstance(self.mMapWidget, MapWidget):
return None
canvases = sorted(self.mMapWidget.mapViewCanvases(self),
key = lambda c: c.property(KEY_LAST_CLICKED))
if len(canvases) == 0:
return None
else:
return canvases[-1]
def currentLayer(self) -> QgsMapLayer:
"""
Returns the current map layer, i.e. that selected in the map layer tree view
......@@ -677,6 +586,179 @@ class MapView(QFrame):
assert isinstance(sensor, SensorInstrument)
return sensor in self.sensors()
class MapViewLayerTreeViewMenuProvider(QgsLayerTreeViewMenuProvider):
def __init__(self, mapView, view: QgsLayerTreeView, canvas: QgsMapCanvas):
super(MapViewLayerTreeViewMenuProvider, self).__init__()
assert isinstance(view, QgsLayerTreeView)
assert isinstance(canvas, QgsMapCanvas)
self.mLayerTreeView: QgsLayerTreeView = view
self.mDummyCanvas: QgsMapCanvas = canvas
#self.mDefActions = QgsLayerTreeViewDefaultActions(self.mLayerTreeView)
self.mMapView: MapView = mapView
#self.actionAddGroup = self.mDefActions.actionAddGroup()
#self.actionRename = self.mDefActions.actionRenameGroupOrLayer()
#self.actionRemove = self.mDefActions.actionRemoveGroupOrLayer()
#self.actionZoomToLayer = self.mDefActions.actionZoomToGroup(self.mDummyCanvas)
#self.actionCheckAndAllChildren = self.mDefActions.actionCheckAndAllChildren()
#self.actionShowFeatureCount = self.mDefActions.actionShowFeatureCount()
#self.actionZoomToLayer = self.mDefActions.actionZoomToLayer(self.mDummyCanvas)
#self.actionZoomToSelected = self.mDefActions.actionZoomToSelection(self.mDummyCanvas)
#self.actionZoomToGroup = self.mDefActions.actionZoomToGroup(self.mDummyCanvas)
#self.actionAddEOTSVSpectralProfiles = QAction('Add Spectral Profile Layer')
#self.actionAddEOTSVTemporalProfiles = QAction('Add Temporal Profile Layer')
def mapView(self) -> MapView:
return self.mMapView
def layerTreeView(self) -> QgsLayerTreeView:
return self.mLayerTreeView
def layerTree(self) -> QgsLayerTree:
return self.layerTreeModel().rootGroup()
def layerTreeModel(self) -> QgsLayerTreeModel:
return self.layerTreeView().model()
def onRemoveLayers(self):
selected = self.layerTreeView().selectedLayers()
for l in selected:
if not isinstance(l, SensorProxyLayer):
self.mapView().mLayerTree.removeLayer(l)
def onSetCanvasCRS(self):
s = ""
lyr = self.layerTreeView()
def onZoomToLayer(self, layer:QgsMapLayer):
extent = SpatialExtent.fromLayer(layer)
if isinstance(extent, SpatialExtent):
extent = extent.toCrs(self.mapView().mapWidget().crs())
self.mapView().mapWidget().setSpatialExtent(extent)
def onZoomActualSize(self):
current = self.mapView().currentLayer()
if isinstance(current, QgsRasterLayer):
s = ""
def onStretchToExtent(self):
current = self.mapView().currentLayer()
canvas = self.mapView().currentMapCanvas()
if not isinstance(canvas, MapCanvas):
return
if isinstance(current, SensorProxyLayer):
for l in canvas.layers():
if isinstance(l, SensorProxyLayer) and l.sensor() == current.sensor():
canvas.stretchToExtent(layer=current)
break
elif isinstance(current, QgsRasterLayer):
canvas.stretchToExtent(layer=current)
def createContextMenu(self) -> QMenu:
model = self.layerTreeModel()
ltree = self.layerTree()
view = self.layerTreeView()
currentGroup = view.currentGroupNode()
currentLayer = view.currentLayer()
currentCanvas = self.mapView().currentMapCanvas()
isSensorGroup = isinstance(currentGroup, QgsLayerTreeGroup) and currentGroup.customProperty(KEY_SENSOR_GROUP) in [True, 'true']
isSensorLayer = isinstance(currentLayer, SensorProxyLayer)
mv = self.mapView().mapWidget()
from eotimeseriesviewer.main import EOTimeSeriesViewer
eotsv = EOTimeSeriesViewer.instance()
if not isinstance(eotsv, EOTimeSeriesViewer):
return
menu = QMenu(view)
assert isinstance(mv, MapWidget)
if isinstance(currentLayer, QgsMapLayer):
# zoom to layer
menu.addAction(eotsv.actionZoomToLayer())
# rename layer
#
# zoom to native resolution
# in this case not using a map tool but the current layer
ref = eotsv.actionZoomActualSize()
a = menu.addAction(ref.text())
a.setIcon(ref.icon())
a.triggered.connect(self.onZoomActualSize)
if isinstance(currentLayer, QgsRasterLayer):
a = menu.addAction('&Stretch Using Current Extent')
a.triggered.connect(self.onStretchToExtent)
# ----
menu.addSeparator()
a = menu.addAction('Add Spectral Library Layer')
a.triggered.connect(self.mapView().addSpectralProfileLayer)
a = menu.addAction('Add Temporal Profile Layer')
a.triggered.connect(self.mapView().addTemporalProfileLayer)
# ----
menu.addSeparator()
# duplicate layer
# remove layer
a = menu.addAction('Remove layer')
a.setToolTip('Remove layer(s)')
a.triggered.connect(self.onRemoveLayers)
menu.addSeparator()
if isinstance(currentLayer, QgsVectorLayer):
menu.addAction(eotsv.actionOpenTable())
menu.addAction(eotsv.actionToggleEditing())
if isinstance(currentLayer, QgsRasterLayer) and not isinstance(currentLayer, SensorProxyLayer):
pass
menu.addSeparator()
# ----------
# set CRS
action = menu.addAction('Set layer CRS to map canvas')
action.triggered.connect(self.onSetCanvasCRS)
# ----
# Export...
# ------
menu.addSeparator()
# Styles...
menu.addAction(eotsv.actionPasteLayerStyle())
menu.addAction(eotsv.actionCopyLayerStyle())
# Properties
menu.addAction(eotsv.actionLayerProperties())
menu.addSeparator()
menu.addSeparator()
return menu
def vectorLayerTools(self) -> VectorLayerTools:
from eotimeseriesviewer.main import EOTimeSeriesViewer
return EOTimeSeriesViewer.instance().mVectorLayerTools
def showAttributeTable(self, lyr: QgsVectorLayer):
from eotimeseriesviewer.main import EOTimeSeriesViewer
tsv = EOTimeSeriesViewer.instance()
if isinstance(tsv, EOTimeSeriesViewer):
tsv.showAttributeTable(lyr)
s = ""
def onSetLayerProperties(self, lyr:QgsRasterLayer, canvas:QgsMapCanvas):
if isinstance(canvas, MapCanvas):
canvas.onSetLayerProperties(lyr)
class MapViewListModel(QAbstractListModel):
"""
A model to store a list of map views.
......
......@@ -190,6 +190,7 @@
<addaction name="actionZoomIn"/>
<addaction name="actionZoomOut"/>
<addaction name="actionZoomPixelScale"/>
<addaction name="mActionZoomToLayer"/>
<addaction name="actionZoomFullExtent"/>
<addaction name="optionMoveCenter"/>
</widget>
......@@ -889,6 +890,42 @@
<string>Deselect selected features.</string>
</property>
</action>
<action name="mActionCopyLayerStyle">
<property name="text">
<string>Copy Layer Style</string>
</property>
</action>
<action name="mActionOpenTable">
<property name="icon">
<iconset resource="../../../QGIS/images/images.qrc">
<normaloff>:/images/themes/default/mActionOpenTable.svg</normaloff>:/images/themes/default/mActionOpenTable.svg</iconset>
</property>
<property name="text">
<string>Open Table</string>
</property>
<property name="toolTip">
<string>Opens the attribute table</string>
</property>
</action>
<action name="mActionZoomToLayer">
<property name="icon">
<iconset resource="../../../QGIS/images/images.qrc">
<normaloff>:/images/themes/default/mActionZoomToLayer.svg</normaloff>:/images/themes/default/mActionZoomToLayer.svg</iconset>
</property>
<property name="text">
<string>ZoomToLayer</string>
</property>
</action>
<action name="mActionPasteLayerStyle">
<property name="text">
<string>Paste Layer Style</string>
</property>
</action>
<action name="mActionLayerProperties">
<property name="text">
<string>Properties...</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
......
[unittest]
start-dir = tests
[test-result]
always-on = True
descriptions = True
\ No newline at end of file
# Configuration file for plugin builder tool
# Sane defaults for your plugin generated by the Plugin Builder are
# already set below.
#
# As you add Python source files and UI files to your plugin, add
# them to the appropriate [files] section below.
[plugin]
# Name of the plugin. This is the name of the directory that will
# be created in C:/.qgis2/python/plugins
name: timeseriesviewerplugin
[files]
# Python files that should be deployed with the plugin
# BJ: root dir only
python_files:
__init__.py
# The main dialog file that is loaded (not compiled)
# BJ: we load all *ui files during runtime
main_dialog:
# Other ui files for your dialogs (these will be compiled)
# BJ: we load all *ui files during runtime