Source code for tuiview.geolinkedviewers


"""
Contains the GeolinkedViewers class.
"""
# This file is part of 'TuiView' - a simple Raster viewer
# Copyright (C) 2012  Sam Gillingham
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import math
import json
from PyQt5.QtCore import QObject, QTimer, Qt, QEventLoop, pyqtSignal
from PyQt5.QtWidgets import QApplication

from . import viewerwindow
from . import pluginmanager
from .querywindow import QueryDockWidget


[docs]class GeolinkedViewers(QObject): """ Class that manages a collection of ViewerWindows that have their widgets geolinked. """ # signals newViewerCreated = pyqtSignal(viewerwindow.ViewerWindow, name='newViewerCreated') "signal emitted when a new viewer window is created" def __init__(self, loadPlugins=True): QObject.__init__(self) # need to keep a reference to keep the python objects alive # otherwise they are deleted before they are shown self.viewers = [] # load plugins if asked if loadPlugins: self.pluginmanager = pluginmanager.PluginManager() self.pluginmanager.loadPlugins() # do the init action self.pluginmanager.callAction(pluginmanager.PLUGIN_ACTION_INIT, self) else: self.pluginmanager = None # set up a timer so we can periodically remove viewer # instances when they are no longer open to save memory # Usually, in PyQt you don't have such a 'dynamic' # number of sub windows. self.timer = QTimer(self) self.timer.timeout.connect(self.cleanUp) self.timer.start(10000) # 10 secs
[docs] @staticmethod def getViewerList(screen=None): """ Gets the list of current viewer windows from Qt. Pass in a screen to restrict to the viewers on that screen """ viewers = [] for viewer in QApplication.topLevelWidgets(): if (isinstance(viewer, viewerwindow.ViewerWindow) and viewer.isVisible()): if screen is not None: winHandle = viewer.windowHandle() screen2 = winHandle.screen() if screen2 is not None and screen.name() != screen2.name(): continue viewers.append(viewer) return viewers
[docs] def cleanUp(self): "remove any viewers that are no longer in the activelist" activeviewers = self.getViewerList() # remove any viewers that are no longer in the activelist # (they must have been closed) # they should now be cleaned up by Python and memory released self.viewers = [viewer for viewer in self.viewers if viewer in activeviewers]
[docs] def closeAll(self): """ Call this to close all geolinked viewers """ for viewer in self.viewers: viewer.close() self.viewers = []
[docs] def setActiveToolAll(self, tool, senderid): """ sets the specified tool as active on all the viewers """ for viewer in self.viewers: viewer.viewwidget.setActiveTool(tool, senderid)
[docs] def setQueryPointAll(self, senderid, easting, northing, color, size=None, cursor=None): """ Calls setQueryPoint on all the widgets """ for viewer in self.viewers: viewer.viewwidget.setQueryPoint(senderid, easting, northing, color, size, cursor)
[docs] def removeQueryPointAll(self, senderid): """ Calls removeQueryPoint on all the widgets """ for viewer in self.viewers: viewer.viewwidget.removeQueryPoint(senderid)
[docs] def newViewer(self, filename=None, stretch=None): """ Call this to create a new geolinked viewer. Returns the created ViewerWindow instance. """ newviewer = viewerwindow.ViewerWindow() newviewer.show() # connect signals self.connectSignals(newviewer) # open the file if we have one if filename is not None: newviewer.addRasterInternal(filename, stretch) self.viewers.append(newviewer) # call any plugins if self.pluginmanager is not None: self.pluginmanager.callAction( pluginmanager.PLUGIN_ACTION_NEWVIEWER, newviewer) # emit a signal so that application can do any customisation self.newViewerCreated.emit(newviewer) # return it return newviewer
[docs] def connectSignals(self, newviewer): """ Connects the appropriate signals for the new viewer """ # connect to the signal the widget sends when moved # sends new easting, northing and id() of the widget. newviewer.viewwidget.geolinkMove.connect(self.onMove) # the signal when a new query point is chosen # on a widget. Sends easting, northing and id() of the widget newviewer.viewwidget.geolinkQueryPoint.connect(self.onQuery) # signal for request for new window newviewer.newWindowSig.connect(self.onNewWindow) # signal for request for windows to be tiled newviewer.tileWindowsSig.connect(self.onTileWindows) # signal for new query window been opened newviewer.newQueryWindowSig.connect(self.onNewQueryWindow) # signal for closing all windows newviewer.closeAllWindowsSig.connect(self.closeAll) # signal for request to write viewers state to a file newviewer.writeViewersState.connect(self.writeViewersState) # signal for request to read viewers state from file newviewer.readViewersState.connect(self.readViewersState)
[docs] def onNewWindow(self): """ Called when the user requests a new window """ newviewer = viewerwindow.ViewerWindow() newviewer.show() # connect signals self.connectSignals(newviewer) self.viewers.append(newviewer) # call any plugins if self.pluginmanager is not None: self.pluginmanager.callAction( pluginmanager.PLUGIN_ACTION_NEWVIEWER, newviewer) # emit a signal so that application can do any customisation self.newViewerCreated.emit(newviewer) return newviewer
[docs] def onNewQueryWindow(self, querywindow): """ Called when the viewer starts a new query window """ # call any plugins if self.pluginmanager is not None: self.pluginmanager.callAction( pluginmanager.PLUGIN_ACTION_NEWQUERY, querywindow)
[docs] def getDesktopSize(self, screen): """ Called at the start of the tiling operation. Default implementation just gets the size of the desktop. if overridden, return a QRect """ if screen is None: return QApplication.desktop().availableGeometry() else: return screen.availableGeometry()
[docs] def onTileWindows(self, nxside, nyside, screen): """ Called when the user wants the windows to be tiled """ # get the dimensions of the desktop desktop = self.getDesktopSize(screen) # getViewerList returns a temporary list so we can stuff around with it viewerList = self.getViewerList(screen) # do they want full auto? if nxside == 0 and nyside == 0: # find the number of viewers along each side nxside = math.sqrt(len(self.viewers)) # round up - we may end up with gaps nxside = int(math.ceil(nxside)) nyside = int(math.ceil(len(self.viewers) / float(nxside))) elif nxside == 0 and nyside != 0: # guess nxside nxside = int(math.ceil(len(self.viewers) / float(nyside))) elif nxside != 0 and nyside == 0: # guess yxside nyside = int(math.ceil(len(self.viewers) / float(nxside))) # size of each viewer window viewerwidth = int(desktop.width() / nxside) viewerheight = int(desktop.height() / nyside) # there is a problem where resize() doesn't include the frame # area so we have to calculate it ourselves. This is the best # I could come up with geom = self.viewers[0].geometry() framegeom = self.viewers[0].frameGeometry() framewidth = framegeom.width() - geom.width() frameheight = framegeom.height() - geom.height() # now resize and move the viewers xcount = 0 ycount = 0 while len(viewerList) > 0: # work out the location we will use and find the viewer closest xloc = desktop.x() + viewerwidth * xcount yloc = desktop.y() + viewerheight * ycount def viewerKey(a): xdist = abs(a.x() - xloc) ydist = abs(a.y() - yloc) return math.sqrt(xdist * xdist + ydist * ydist) # sort by distance from this location viewerList = sorted(viewerList, key=viewerKey) # closest viewer = viewerList.pop(0) # remove any maximised states - window manager will not let # use resize state = viewer.windowState() if (state & Qt.WindowMaximized) == Qt.WindowMaximized: viewer.setWindowState(state ^ Qt.WindowMaximized) # resize takes the area without the frame so we correct for that viewer.resize(viewerwidth - framewidth, viewerheight - frameheight) # remember that taskbar etc mean that we might not want # to start at 0,0 viewer.move(xloc, yloc) xcount += 1 if xcount >= nxside: xcount = 0 ycount += 1
[docs] def onMove(self, obj): """ Called when a widget signals it has moved. Move all the other widgets. A GeolinkInfo object is passed. Sends the id() of the widget and uses this to not move the original widget """ # paint any windows that are ready QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) for viewer in self.getViewerList(): # we use the id() of the widget to # identify them. if id(viewer.viewwidget) != obj.senderid: viewer.viewwidget.doGeolinkMove(obj.easting, obj.northing, obj.metresperwinpix) # paint any windows that are ready QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
[docs] def onQuery(self, obj): """ Called when a widget signals the query point has moved. Notify the other widgets. A GeolinkInfo object is passed. Sends the id() of the widget and uses this not to notify the original widget """ for viewer in self.getViewerList(): # we use the id() of the widget to # identify them. if id(viewer.viewwidget) != obj.senderid: viewer.viewwidget.doGeolinkQueryPoint(obj.easting, obj.northing)
[docs] def writeViewersState(self, fileobj): """ Gets the state of all the viewers (location, layers etc) as a json encoded string and write it to fileobj """ from .viewerlayers import ViewerQueryPointLayer, ViewerFeatureVectorLayer viewers = self.getViewerList() # see if we can get a GeolinkInfo # should all be the same since geolinked geolinkStr = 'None' for viewer in viewers: info = viewer.viewwidget.getGeolinkInfo() if info is not None: geolinkStr = info.toString() break s = json.dumps({'name': 'tuiview', 'nviewers': len(viewers), 'geolink': geolinkStr}) + '\n' fileobj.write(s) for viewer in viewers: pos = viewer.pos() # we have to be careful since not all layer types # are saved. Must be a better way... nlayers = 0 for layer in viewer.viewwidget.layers.layers: if (not isinstance(layer, ViewerQueryPointLayer) and not isinstance(layer, ViewerFeatureVectorLayer)): nlayers += 1 viewerDict = {'nlayers': nlayers, 'x': pos.x(), 'y': pos.y(), 'width': viewer.width(), 'height': viewer.height()} winHandle = viewer.windowHandle() # save which screen this is on screen = winHandle.screen() if screen is not None: viewerDict['screen'] = screen.name() # querywindow situation query_wins = viewer.findChildren(QueryDockWidget) if len(query_wins) > 0: query_win = query_wins[0] # check it's visible - can still exist after hidden. if query_win.isVisible(): qpos = query_win.pos() query_win_data = {'x': qpos.x(), 'y': qpos.y(), 'width': query_win.width(), 'height': query_win.height()} if query_win.lastqi is not None: query_win_data['easting'] = query_win.lastqi.easting query_win_data['northing'] = query_win.lastqi.northing viewerDict['querywindow'] = query_win_data # TODO: maybe other dock widgets (profile etc?) s = json.dumps(viewerDict) + '\n' fileobj.write(s) # now get the layers to write themselves out viewer.viewwidget.layers.toFile(fileobj)
[docs] def readViewersState(self, fileobj): """ Reads viewer state from the fileobj and restores viewers """ from . import viewerwidget headerDict = json.loads(fileobj.readline()) if 'name' not in headerDict or headerDict['name'] != 'tuiview': raise ValueError('File not written by tuiview') geolinkStr = headerDict['geolink'] if geolinkStr != 'None': geolink = viewerwidget.GeolinkInfo.fromString(geolinkStr) else: geolink = None # get all the screens connected screenDict = {} screens = QApplication.screens() for screen in screens: screenDict[screen.name()] = screen # set this if we have a valid query window with a valid location query_easting_northing = (None, None) query_viewer = None # so we can grab the last one for n in range(headerDict['nviewers']): viewer = self.onNewWindow() viewerDict = json.loads(fileobj.readline()) viewer.addLayersFromJSONFile(fileobj, viewerDict['nlayers']) if 'screen' in viewerDict: winHandle = viewer.windowHandle() screenName = viewerDict['screen'] if screenName in screenDict: screen = screenDict[screenName] winHandle.setScreen(screen) # do this last in case only makes sense on new window viewer.move(viewerDict['x'], viewerDict['y']) viewer.resize(viewerDict['width'], viewerDict['height']) # any sub windows? if 'querywindow' in viewerDict: query_win_data = viewerDict['querywindow'] # the best way to do this ended up by invoking the action # as the button state ended up being correct etc viewer.queryAct.setChecked(True) # should be only one child of this type qw = viewer.findChildren(QueryDockWidget)[0] qw.move(query_win_data['x'], query_win_data['y']) qw.resize(query_win_data['width'], query_win_data['height']) # any location? if 'easting' in query_win_data and 'northing' in query_win_data: query_easting_northing = (query_win_data['easting'], query_win_data['northing']) query_viewer = viewer # do now so all the windows get it (using the last one) qeasting, qnorthing = query_easting_northing if qeasting is not None: query_viewer.viewwidget.newQueryPoint(qeasting, qnorthing) # set the location if any if geolink is not None: self.onMove(geolink)