# -*- coding: utf-8 -*- """ *************************************************************************** --------------------- Date : 27.03.2018 Copyright : (C) 2018 by Benjamin Jakimow Email : benjamin jakimow at geo dot hu-berlin dot de *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * *************************************************************************** """ # noinspection PyPep8Naming import sys, os, re, collections from qgis import * from qgis.core import * from qgis.gui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * import pyqtgraph.opengl as gl from pyqtgraph import functions as fn from OpenGL.GL import * import OpenGL.GLUT from pyqtgraph.opengl import * from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem from pyqtgraph.Vector import Vector from eotimeseriesviewer.temporalprofiles import * DT_SELECTION = 200 class AxisGrid3D(GLGraphicsItem): def __init__(self, *args, **kwds): super(AxisGrid3D, self).__init__(*args, **kwds) self.antialias = True self.mRangesMin = np.asarray([0,0,0], dtype=np.float64) self.mRangesMax = np.asarray([1,1,1], dtype=np.float64) self.mSteps = np.asarray([10,10,10], dtype=np.int) self.mVisibility = np.ones((3), dtype=np.bool) self.mColor = QColor('grey') self.mDims = ['xy', 'xz', 'yz'] def setColor(self, color): self.mColor = QColor(color) def set(self, dim, v0=None, v1=None, steps=None, visible=None, skipUpdate=False): assert isinstance(dim, str) dim = dim.lower() assert dim in self.mDims i = self.mDims.index(dim) if v0 is not None: self.mRangesMin[i] = v0 if v1 is not None: self.mRangesMax[i] = v1 if isinstance(steps, int): self.mSteps[i] = steps if isinstance(visible, bool): self.mVisibility[i] = visible if not skipUpdate: self.update() def setXY(self, **kwds): self.set('xy', **kwds) def setXZ(self, **kwds): self.set('xz', **kwds) def setYZ(self, **kwds): self.set('yz', **kwds) def setMinRanges(self, ranges): self.mRangesMin[:] = ranges def setMaxRanges(self, ranges): self.mRangesMax[:] = ranges def paint(self): self.setupGLState() if self.antialias: glEnable(GL_LINE_SMOOTH) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) glBegin(GL_LINES) #T = self.transform() #origin of axis values valMin = Vector(*self.mRangesMin) #max extent of axis values valMax = Vector(*self.mRangesMax) valRange = valMax - valMin if valRange.x() <= 0: valRange.setX(1) if valRange.y() <= 0: valRange.setY(1) if valRange.z() <= 0: valRange.setZ(1) stepSize = valRange / Vector(self.mSteps) valuesX = np.arange(valMin.x(), valMax.x()+1.00001*stepSize.x(), stepSize.x()) valuesY = np.arange(valMin.y(), valMax.y()+1.00001*stepSize.y(), stepSize.y()) valuesZ = np.arange(valMin.z(), valMax.z()+1.00001*stepSize.z(), stepSize.z()) c = fn.glColor(self.mColor) glColor4f(*c) if self.mVisibility[0]: #show XY for x in valuesX: glVertex3f(x, valuesY[0], valMin.z()) glVertex3f(x, valuesY[-1], valMin.z()) for y in valuesY: glVertex3f(valuesX[0], y, valMin.z()) glVertex3f(valuesX[-1], y, valMin.z()) if self.mVisibility[1]: # show XZ for x in valuesX: glVertex3f(x, valMin.y(), valuesZ[0]) glVertex3f(x, valMin.y(), valuesZ[-1]) for z in valuesZ: glVertex3f(valuesX[0], valMin.y(), z) glVertex3f(valuesX[-1], valMin.y(), z) if self.mVisibility[2]: # show YZ for y in valuesY: glVertex3f(valMin.x(), y, valuesZ[0]) glVertex3f(valMin.x(), y, valuesZ[-1]) for z in valuesZ: glVertex3f(valMin.x(), valuesY[0], z) glVertex3f(valMin.x(), valuesY[-1], z) glEnd() class Label3D(GLGraphicsItem): def __init__(self, label='', *args, **kwds): super(Label3D, self).__init__(*args, **kwds) self.mLabel = label self.mIsVisible = True self.mPos =np.asarray([0,0,0], dtype=np.float) def setPos(self, x,y,z): self.mPos[0] = x self.mPos[1] = y self.mPos[2] = z def setText(self, text): assert isinstance(text, str) self.mLabel = text def text(self): return self.mLabel def setVisible(self, b): assert isinstance(b, bool) self.mIsVisible = b self.update() def isVisible(self): return self.mIsVisible def paint(self, *args, **kwds): s = "" if False: self.setupGLState() #glBegin(GL_LINES) glEnable(GL_LINE_SMOOTH) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) #glBegin(GL_LINES) glPushMatrix() x,y,z= self.mPos glTranslatef(1,0,0) #glScale() from OpenGL.GLUT import glutStrokeCharacter,glutStrokeWidth, GLUT_STROKE_ROMAN w = 0 text = self.mLabel.encode('utf-8') for c in text: w += glutStrokeWidth(GLUT_STROKE_ROMAN, c) glRotate(1,0,1,0) glScale(0.1,0.1,0.1) glTranslatef(-w / 2., -w/5., -w/2.) for c in text: glutStrokeCharacter(GLUT_STROKE_ROMAN, c) glPopMatrix() #glEnd() class ViewWidget3D(GLViewWidget): def __init__(self, parent=None): super(ViewWidget3D, self).__init__(parent) self.mousePos = QPoint(-1, -1) self.setBackgroundColor(QColor('black')) self.setMouseTracking(True) self.mDataMinRanges = np.asarray([0, 0, 0]) self.mDataMaxRanges = np.asarray([1, 1, 1]) self.mDataSpan = self.mDataMaxRanges - self.mDataMinRanges self.mScale = np.asarray([1.,1.,1.]) self.mDataN = 0 self.glAxes = Axis3D() from pyqtgraph.Transform3D import Transform3D self.mItemTransformation = Transform3D() #self.glGridItemXY = AxisGrid3D() #self.glGridItemXZ = AxisGrid3D() #self.glGridItemYZ = AxisGrid3D() self.glGridItem = AxisGrid3D() #self.glGridItemXZ.setVisible(False) #self.glGridItemYZ.setVisible(False) x, y, z = self.glAxes.size() #self.glGridItemYZ.rotate(-90, 0, 1, 0) #self.glGridItemXZ.rotate( 90, 1, 0, 0) # self.glGridItemXY.scale(x/10,y/10, 1) # self.glGridItemXZ.scale(x/10,z/10, 1) # self.glGridItemYZ.scale(y/10,z/10, 1) #self.mBasicItems = [self.glGridItemXY, self.glGridItemXZ, self.glGridItemYZ, self.glAxes] self.mBasicItems = [self.glAxes, self.glGridItem] for item in self.mBasicItems: if item == self.glAxes: item.setDepthValue(-10) else: item.setDepthValue(0) self.addItem(item) # draw grid/axis after surfaces since they may be translucent """ def setDataRangeX(self, x0, x1): assert x0 < x1 self.mDataMinRanges[0] = x0 self.mDataMaxRanges[0] = x1 def setDataRangeY(self, y0, y1): assert y0 < y1 self.mDataMinRanges[0] = y0 self.mDataMaxRanges[0] = y1 def setDataRangeZ(self, z0, z1): assert z0 < z1 self.mDataMinRanges[0] = z0 self.mDataMaxRanges[0] = z1 """ def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None): if distance is not None: self.opts['distance'] = distance if elevation is not None: self.opts['elevation'] = elevation if azimuth is not None: self.opts['azimuth'] = azimuth if pos is not None: if not isinstance(pos, QVector3D): pos = Vector(pos) self.opts['center'] = pos def resetCamera(self): # self.mDataMinRanges self.updateDataRanges() self.resetScaling() x,y,z = self.mDataMaxRanges self.setCameraPosition([1.,0.5,0.5], distance=10, elevation=10, azimuth=10) self.update() def clearItems(self): to_remove = [i for i in self.items if i not in self.mBasicItems] for i in to_remove: self.items.remove(i) i._setView(None) self.update() def paintGL(self, *args, **kwds): GLViewWidget.paintGL(self, *args, **kwds) self.qglColor(Qt.white) self.renderAnnotations() def zoomToFull(self): x = y = z = 0 for item in self.items: if item not in self.mBasicItems: pos = item.pos #self.setCameraPosition(pos=pos, distance=10) break s = "" def updateDataRanges(self): """ Re-calcuates the data ranges of the added plot items. Calls this before re-scaling the transformation matrix. """ x0 = x1 = y0 = y1 = z0 = z1 = n = None if hasattr(self, 'items'): for item in self.items: if item not in self.mBasicItems and hasattr(item, 'pos'): pos = item.pos if x0 is None: n = pos.shape[0] x0 = pos[:, 0].min() y0 = pos[:, 1].min() z0 = pos[:, 2].min() x1 = pos[:, 0].max() y1 = pos[:, 1].max() z1 = pos[:, 2].max() else: n = max(n, pos.shape[0]) x0 = min(x0, pos[:, 0].min()) y0 = min(y0, pos[:, 1].min()) z0 = min(z0, pos[:, 2].min()) x1 = max(x1, pos[:, 0].max()) y1 = max(y1, pos[:, 1].max()) z1 = max(z1, pos[:, 2].max()) if x1 is not None: self.mDataMinRanges = np.asarray([x0, y0, z0]) self.mDataMaxRanges = np.asarray([x1, y1, z1]) self.mDataSpan = self.mDataMaxRanges - self.mDataMinRanges #avoid division by zero and provide a minimum range of 1/10000 self.mDataSpan = np.where(self.mDataSpan == 0, np.ones((3), dtype=np.float64)/10000., self.mDataSpan) self.mDataMaxRanges = self.mDataMinRanges + self.mDataSpan self.mDataN = n self.glAxes.setMinRanges(self.mDataMinRanges) self.glAxes.setMaxRanges(self.mDataMaxRanges) self.glGridItem.setMinRanges(self.mDataMinRanges) self.glGridItem.setMaxRanges(self.mDataMaxRanges) def resetScaling(self): t = pg.Transform3D() scale = np.asarray([0.9, 1.0, 0.8]) / np.asarray(self.mDataSpan) # scale to 0-1 t.scale(*scale) t.translate(*(-1 * np.asarray(self.mDataMinRanges))) # set axis origin to 0:0:0 vMin = t*Vector(self.mDataMinRanges) vMax = t*Vector(self.mDataMaxRanges) #pos = (self.mDataMinRanges+self.mDataMaxRanges)*0.5 #self.setCameraPosition(pos=Vector(*pos)*t) #self.setCameraPosition(pos=t*Vector(*pos)) #self.setCameraPosition(pos=Vector(*pos)) self.setItemTransform(t) #self.setCameraPosition(pos=Vector(0.5,0.5,0.5)) def setItemTransform(self, transform): assert isinstance(transform, pg.Transform3D) self.mItemTransformation = transform for item in self.items: item.setTransform(transform) def itemTransformation(self): return self.mItemTransformation def addItems(self, items): """Adds a list of items to this plot""" for item in items: assert isinstance(item, GLGraphicsItem) if hasattr(item, 'initializeGL'): self.makeCurrent() try: item.initializeGL() except: self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item)) item._setView(self) self.items.extend(items) self.updateDataRanges() self.update() def removeItems(self, items): for item in items: if item in self.items: self.items.remove(item) item._setView(None) self.updateDataRanges() self.update() def mouseMoveEvent(self, ev): assert isinstance(ev, QMouseEvent) """ Allow Shift to Move and Ctrl to Pan. Example taken from https://gist.github.com/blink1073/7406607 """ shift = ev.modifiers() & QtCore.Qt.ShiftModifier ctrl = ev.modifiers() & QtCore.Qt.ControlModifier if shift: y = ev.pos().y() if not hasattr(self, '_prev_zoom_pos') or not self._prev_zoom_pos: self._prev_zoom_pos = y return dy = y - self._prev_zoom_pos def delta(): return -dy * 5 ev.delta = delta self._prev_zoom_pos = y self.wheelEvent(ev) elif ctrl: pos = ev.pos().x(), ev.pos().y() if not hasattr(self, '_prev_pan_pos') or not self._prev_pan_pos: self._prev_pan_pos = pos return dx = pos[0] - self._prev_pan_pos[0] dy = pos[1] - self._prev_pan_pos[1] self.pan(dx, dy, 0, relative=True) self._prev_pan_pos = pos else: super(ViewWidget3D, self).mouseMoveEvent(ev) # items = self.itemsAt((pos.x(), pos.y(), 3, 3)) def mousePressEvent(self, event): super(ViewWidget3D, self).mousePressEvent(event) self.mousePos = event.pos() if event.button() == Qt.RightButton: self.select = True else: self.select = False try: glColorSelected = fn.glColor(QColor('red')) for item in self.itemsAt((self.mousePos.x(), self.mousePos.y(), 3, 3)): if isinstance(item, GLLinePlotItem): c = item.color item.setData(color=glColorSelected) QTimer.singleShot(DT_SELECTION, lambda item=item, c=c: item.setData(color=c)) except: pass def renderAnnotations(self): if self.glAxes.visible(): x0, y0, z0 = self.glAxes.rangeMinima() x1, y1, z1 = self.glAxes.rangeMaxima() dx, dy, dz = self.glAxes.rangeSpan() T = self.glAxes.transform() #transform d = 0.1 V0 = T * Vector(*self.glAxes.rangeMinima()) V1 = T * Vector(*self.glAxes.rangeMaxima()) dx = V1.x() - V0.x() dy = V1.y() - V0.y() dz = V1.z() - V0.z() sx = V1.x() + 0.1*dx sy = V1.y() + 0.1*dy sz = V1.z() + 0.1*dz if x1 is not None: #self.renderText(x1 + d*dx, 0, 0, self.glAxes.mLabels[0]) self.renderText(sx, 0, 0, self.glAxes.mLabels[0]) if y1 is not None: #self.renderText(0, y1 + d*dy, 0, self.glAxes.mLabels[1]) self.renderText(0, sy, 0, self.glAxes.mLabels[1]) if z1 is not None: #self.renderText(0, 0, z1 + d*dz, self.glAxes.mLabels[2]) self.renderText(0, 0, sz, self.glAxes.mLabels[2]) if True: #set axes origin l = '{} {} {}'.format(x0,y0,z0) self.renderText(V0.x()-0.1*dx,V0.y()-0.1*dy, V0.z()-0.1*dz, l) # self.renderText(0.8, 0.8, 0.8, 'text 3D') # self.renderText(5, 10, 'text 2D fixed') self.qglColor(Qt.darkYellow) self.renderText(5, 10, '(3D Mode still experimental)') def contextMenuEvent(self, event): assert isinstance(event, QContextMenuEvent) menu = QMenu() a = menu.addAction('Reset Camera') a.triggered.connect(self.resetCamera) menu.addSeparator() # define grid options m = menu.addMenu('Grids') def gridVisibility(b): for d in ['XY','XZ','YZ']: self.glGridItem.set(d, visible=b) self.glGridItem.update() a = m.addAction('Show All') a.setCheckable(False) a.triggered.connect(lambda: gridVisibility(True)) a = m.addAction('Hide All') a.setCheckable(False) a.triggered.connect(lambda: gridVisibility(False)) m.addSeparator() for i, dim in enumerate(['XY','XZ','YZ']): a = m.addAction(dim) a.setCheckable(True) a.setChecked(self.glGridItem.mVisibility[i]) a.toggled.connect(lambda b, dim=dim:self.glGridItem.set(dim,visible=b)) m = menu.addMenu('Axes') a = m.addAction('Show All') a.setCheckable(False) a.triggered.connect(lambda : self.glAxes.setAxes('xyz', visible=True)) a = m.addAction('Hide All') a.setCheckable(False) a.triggered.connect(lambda: self.glAxes.setAxes('xyz', visible=False)) m.addSeparator() a = m.addAction('X') a.setCheckable(True) a.setChecked(self.glAxes.mVisibility[0]) a.toggled.connect(lambda b: self.glAxes.setX(visible=b)) a = m.addAction('Y') a.setCheckable(True) a.setChecked(self.glAxes.mVisibility[1]) a.toggled.connect(lambda b: self.glAxes.setY(visible=b)) a = m.addAction('Z') a.setCheckable(True) a.setChecked(self.glAxes.mVisibility[2]) a.toggled.connect(lambda b: self.glAxes.setZ(visible=b)) menuLabels = menu.addMenu('Labels') frame = QFrame() layout = QGridLayout() frame.setLayout(layout) names = ['X','Y','Z'] for i, label in enumerate(self.glAxes.labels()): dim = names[i] layout.addWidget(QLabel(dim), i,0) tb = QLineEdit() tb.setText(label) tb.textChanged.connect(lambda t, dim=dim : self.glAxes.setAxes(dim, label=t)) layout.addWidget(tb,i,1) layout.setSpacing(1) layout.setMargin(1) frame.setMinimumSize(layout.sizeHint()) wa = QWidgetAction(menuLabels) wa.setDefaultWidget(frame) menuLabels.addAction(wa) menu.exec_(self.mapToGlobal(event.pos())) class GLTextItem(GLGraphicsItem): def __init__(self, X=None, Y=None, Z=None, text=None): GLGraphicsItem.__init__(self) self.text = text self.X = X self.Y = Y self.Z = Z def setGLViewWidget(self, GLViewWidget): self.GLViewWidget = GLViewWidget def setText(self, text): self.text = text self.update() def setX(self, X): self.X = X self.update() def setY(self, Y): self.Y = Y self.update() def setZ(self, Z): self.Z = Z self.update() def paint(self): self.GLViewWidget.qglColor(Qt.white) self.GLViewWidget.renderText(self.X, self.Y, self.Z, self.text) class Axis3D(GLAxisItem): def __init__(self, *args, **kwds): super(Axis3D, self).__init__(*args, **kwds) self.mRanges = np.asarray([[0, 1], [0, 1], [0, 1]]) self.mColors = [QColor('white'), QColor('white'), QColor('white')] self.mVisibility = [True, True, True] self.mLabels = ['X', 'Y', 'Z'] def rangeMinima(self): return self.mRanges[:, 0] def rangeMaxima(self): return self.mRanges[:, 1] def setMinRanges(self, ranges): self.mRanges[:,0] = ranges def setMaxRanges(self, ranges): self.mRanges[:,1] = ranges def rangeSpan(self): return self.mRanges[:,1] - self.mRanges[:,0] def setAxes(self, ax, vMin=None, vMax=None, label=None, color=None, visible=None): for c in ax: i = ['x', 'y', 'z'].index(c.lower()) if vMin is not None: self.mRanges[i][0] = vMin if vMax is not None: self.mRanges[i][1] = vMax if color is not None: self.mColors[i] = color if label is not None: self.mLabels[i] = label if visible is not None: self.mVisibility[i] = visible self.update() def setLabels(self, x,y,z): self.mLabels = [x,y,z] def labels(self): return self.mLabels[:] def setX(self, **kwds): self.setAxes('x', **kwds) def setY(self, **kwds): self.setAxes('y', **kwds) def setZ(self, **kwds): self.setAxes('z', **kwds) def paint(self): self.setupGLState() if self.antialias: glEnable(GL_LINE_SMOOTH) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) x0, y0, z0 = self.rangeMinima() x1, y1, z1 = self.rangeMaxima() #V0 = Vector(*self.rangeMinima()) #V1 = Vector(*self.rangeMaxima()) #T = self.transform() #V0 = V0*T #V1 = V1*T #x0,y0,z0 = V0.x(), V0.y(), V0.z() #x1, y1, z1 = V1.x(), V1.y(), V1.z() glLineWidth(3.0) glBegin(GL_LINES) if self.mVisibility[0]: glColor4f(*fn.glColor(self.mColors[0])) glVertex3f(x0, y0, z0) glVertex3f(x1, y0, z0) if self.mVisibility[1]: glColor4f(*fn.glColor(self.mColors[1])) glVertex3f(x0, y0, z0) glVertex3f(x0, y1, z0) if self.mVisibility[2]: glColor4f(*fn.glColor(self.mColors[2])) glVertex3f(x0, y0, z0) glVertex3f(x0, y0, z1) glEnd() glLineWidth(1.0)