def fetchFiles(self, urls, renderContext): downloader = Downloader(None, self.maxConnections, self.cacheExpiry, self.userAgent) downloader.moveToThread(QgsApplication.instance().thread()) downloader.timer.moveToThread(QgsApplication.instance().thread()) self.logT("TileLayer.fetchFiles() starts") # create a QEventLoop object that belongs to the current worker thread eventLoop = QEventLoop() downloader.allRepliesFinished.connect(eventLoop.quit) if self.iface: # for download progress downloader.replyFinished.connect(self.networkReplyFinished) self.downloader = downloader # create a timer to watch whether rendering is stopped watchTimer = QTimer() watchTimer.timeout.connect(eventLoop.quit) # fetch files QMetaObject.invokeMethod(self.downloader, "fetchFilesAsync", Qt.QueuedConnection, Q_ARG(list, urls), Q_ARG(int, self.plugin.downloadTimeout)) # wait for the fetch to finish tick = 0 interval = 500 timeoutTick = self.plugin.downloadTimeout * 1000 / interval watchTimer.start(interval) while tick < timeoutTick: # run event loop for 0.5 seconds at maximum eventLoop.exec_() if downloader.unfinishedCount() == 0 or renderContext.renderingStopped(): break tick += 1 watchTimer.stop() if downloader.unfinishedCount() > 0: downloader.abort(False) if tick == timeoutTick: downloader.errorStatus = Downloader.TIMEOUT_ERROR self.log("fetchFiles(): timeout") # watchTimer.timeout.disconnect(eventLoop.quit) # downloader.allRepliesFinished.disconnect(eventLoop.quit) self.logT("TileLayer.fetchFiles() ends") return downloader.fetchedFiles
class TileLayer(QgsPluginLayer): LAYER_TYPE = "PyTiledLayer" MAX_TILE_COUNT = 256 CHANGE_SCALE_VALUE = 0.30 def __init__(self, plugin, layerDef, creditVisibility=1): QgsPluginLayer.__init__(self, TileLayer.LAYER_TYPE, layerDef.title) self.plugin = plugin self.iface = plugin.iface self.layerDef = layerDef self.creditVisibility = 1 if creditVisibility else 0 # set custom properties self.setCustomProperty("title", layerDef.title) self.setCustomProperty("credit", layerDef.credit) # TODO: need to remove self.setCustomProperty("serviceUrl", layerDef.serviceUrl) self.setCustomProperty("yOriginTop", layerDef.yOriginTop) self.setCustomProperty("zmin", layerDef.zmin) self.setCustomProperty("zmax", layerDef.zmax) if layerDef.bbox: self.setCustomProperty("bbox", layerDef.bbox.toString()) self.setCustomProperty("creditVisibility", self.creditVisibility) # create a QgsCoordinateReferenceSystem instance if plugin has no instance yet if plugin.crs3857 is None: plugin.crs3857 = QgsCoordinateReferenceSystem(3857) self.setCrs(plugin.crs3857) if layerDef.bbox: self.setExtent( BoundingBox.degreesToMercatorMeters( layerDef.bbox).toQgsRectangle()) else: self.setExtent( QgsRectangle(-layerDef.TSIZE1, -layerDef.TSIZE1, layerDef.TSIZE1, layerDef.TSIZE1)) self.setValid(True) self.tiles = None self.useLastZoomForPrint = False self.canvasLastZoom = 0 self.setTransparency(LayerDefaultSettings.TRANSPARENCY) self.setBlendModeByName(LayerDefaultSettings.BLEND_MODE) self.setSmoothRender(LayerDefaultSettings.SMOOTH_RENDER) self.downloader = Downloader(self) self.downloader.userAgent = "QGIS/{0} QuickMapServices Plugin".format( QGis.QGIS_VERSION ) # , self.plugin.VERSION) # not written since QGIS 2.2 self.downloader.DEFAULT_CACHE_EXPIRATION = QSettings().value( "/qgis/defaultTileExpiry", 24, type=int) QObject.connect(self.downloader, SIGNAL("replyFinished(QString, int, int)"), self.networkReplyFinished) # multi-thread rendering self.eventLoop = None QObject.connect(self, SIGNAL("fetchRequest(QStringList)"), self.fetchRequest) if self.iface: QObject.connect(self, SIGNAL("showMessage(QString, int)"), self.showStatusMessageSlot) QObject.connect( self, SIGNAL("showBarMessage(QString, QString, int, int)"), self.showBarMessageSlot) def setBlendModeByName(self, modeName): self.blendModeName = modeName blendMode = getattr(QPainter, "CompositionMode_" + modeName, 0) self.setBlendMode(blendMode) self.setCustomProperty("blendMode", modeName) def setTransparency(self, transparency): self.transparency = transparency self.setCustomProperty("transparency", transparency) def setSmoothRender(self, isSmooth): self.smoothRender = isSmooth self.setCustomProperty("smoothRender", 1 if isSmooth else 0) def setCreditVisibility(self, visible): self.creditVisibility = visible self.setCustomProperty("creditVisibility", 1 if visible else 0) def draw(self, renderContext): #import pydevd #pydevd.settrace('localhost', port=9921, stdoutToServer=True, stderrToServer=True, suspend=False) self.renderContext = renderContext extent = renderContext.extent() if extent.isEmpty() or extent.width() == float("inf"): qDebug("Drawing is skipped because map extent is empty or inf.") return True mapSettings = self.iface.mapCanvas().mapSettings() painter = renderContext.painter() isDpiEqualToCanvas = painter.device().logicalDpiX( ) == mapSettings.outputDpi() if isDpiEqualToCanvas or not self.useLastZoomForPrint: # calculate zoom level tile_mpp1 = self.layerDef.TSIZE1 / self.layerDef.TILE_SIZE viewport_mpp = extent.width() / painter.viewport().width() lg = math.log(float(tile_mpp1) / float(viewport_mpp), 2) zoom = int(math.modf( lg)[1]) + 1 * (math.modf(lg)[0] > self.CHANGE_SCALE_VALUE) + 1 zoom = max(0, min(zoom, self.layerDef.zmax)) # zoom = max(self.layerDef.zmin, zoom) else: # for print composer output image, use last zoom level of map item on print composer (or map canvas) zoom = self.canvasLastZoom # zoom limit if zoom < self.layerDef.zmin: if self.plugin.navigationMessagesEnabled: msg = self.tr( "Current zoom level ({0}) is smaller than zmin ({1}): {2}" ).format(zoom, self.layerDef.zmin, self.layerDef.title) self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True while True: # calculate tile range (yOrigin is top) size = self.layerDef.TSIZE1 / 2**(zoom - 1) matrixSize = 2**zoom ulx = max(0, int( (extent.xMinimum() + self.layerDef.TSIZE1) / size)) uly = max(0, int( (self.layerDef.TSIZE1 - extent.yMaximum()) / size)) lrx = min(int((extent.xMaximum() + self.layerDef.TSIZE1) / size), matrixSize - 1) lry = min(int((self.layerDef.TSIZE1 - extent.yMinimum()) / size), matrixSize - 1) # bounding box limit if self.layerDef.bbox: trange = self.layerDef.bboxDegreesToTileRange( zoom, self.layerDef.bbox) ulx = max(ulx, trange.xmin) uly = max(uly, trange.ymin) lrx = min(lrx, trange.xmax) lry = min(lry, trange.ymax) if lrx < ulx or lry < uly: # tile range is out of the bounding box return True # tile count limit tileCount = (lrx - ulx + 1) * (lry - uly + 1) if tileCount > self.MAX_TILE_COUNT: # as tile count is over the limit, decrease zoom level zoom -= 1 # if the zoom level is less than the minimum, do not draw if zoom < self.layerDef.zmin: msg = self.tr( "Tile count is over limit ({0}, max={1})").format( tileCount, self.MAX_TILE_COUNT) self.showBarMessage(msg, QgsMessageBar.WARNING, 4) return True continue # zoom level has been determined break # frame isn't drawn not in web mercator isWebMercator = self.isProjectCrsWebMercator() if not isWebMercator and self.layerDef.serviceUrl[0] == ":": if "frame" in self.layerDef.serviceUrl: # or "number" in self.layerDef.serviceUrl: msg = self.tr("Frame layer is drawn only in EPSG:3857") self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True self.logT("TileLayer.draw: {0} {1} {2} {3} {4}".format( zoom, ulx, uly, lrx, lry)) # save painter state painter.save() # set pen and font painter.setPen(Qt.black) font = QFont(painter.font()) font.setPointSize(10) painter.setFont(font) if self.layerDef.serviceUrl[0] == ":": painter.setBrush(QBrush(Qt.NoBrush)) self.drawDebugInfo(renderContext, zoom, ulx, uly, lrx, lry) else: # create Tiles class object and throw url into it tiles = Tiles(zoom, ulx, uly, lrx, lry, self.layerDef) urls = [] cacheHits = 0 for ty in range(uly, lry + 1): for tx in range(ulx, lrx + 1): data = None url = self.layerDef.tileUrl(zoom, tx, ty) if self.tiles and zoom == self.tiles.zoom and url in self.tiles.tiles: data = self.tiles.tiles[url].data tiles.addTile(url, Tile(zoom, tx, ty, data)) if data is None: urls.append(url) elif data: # memory cache exists cacheHits += 1 # else: # tile was not found (Downloader.NOT_FOUND=0) self.tiles = tiles if len(urls) > 0: # fetch tile data files = self.fetchFiles(urls) for url in files.keys(): self.tiles.setImageData(url, files[url]) if self.iface: cacheHits += self.downloader.cacheHits downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits msg = self.tr( "{0} files downloaded. {1} caches hit.").format( downloadedCount, cacheHits) barmsg = None if self.downloader.errorStatus != Downloader.NO_ERROR: if self.downloader.errorStatus == Downloader.TIMEOUT_ERROR: barmsg = self.tr("Download Timeout - {}").format( self.name()) else: msg += self.tr(" {} files failed.").format( self.downloader.fetchErrors) if self.downloader.fetchSuccesses == 0: barmsg = self.tr( "Failed to download all {0} files. - {1}" ).format(self.downloader.fetchErrors, self.name()) self.showStatusMessage(msg, 5000) if barmsg: self.showBarMessage(barmsg, QgsMessageBar.WARNING, 4) # apply layer style oldOpacity = painter.opacity() painter.setOpacity(0.01 * (100 - self.transparency)) oldSmoothRenderHint = painter.testRenderHint( QPainter.SmoothPixmapTransform) if self.smoothRender: painter.setRenderHint(QPainter.SmoothPixmapTransform) # draw tiles if isWebMercator: # no need to reproject tiles self.drawTiles(renderContext, self.tiles) # self.drawTilesDirectly(renderContext, self.tiles) else: # reproject tiles self.drawTilesOnTheFly(renderContext, self.tiles) # restore layer style painter.setOpacity(oldOpacity) if self.smoothRender: painter.setRenderHint(QPainter.SmoothPixmapTransform, oldSmoothRenderHint) # draw credit on the bottom right corner if self.creditVisibility and self.layerDef.credit: margin, paddingH, paddingV = (3, 4, 3) # scale scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) scale = max(scaleX, scaleY) painter.scale(scale, scale) visibleSWidth = painter.viewport().width() * scaleX / scale visibleSHeight = painter.viewport().height() * scaleY / scale rect = QRect(0, 0, visibleSWidth - margin, visibleSHeight - margin) textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) painter.fillRect(bgRect, QColor(240, 240, 240, 150)) # 197, 234, 243, 150)) painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) if 0: # debug_mode: # draw plugin icon image = QImage( os.path.join(os.path.dirname(QFile.decodeName(__file__)), "icon_old.png")) painter.drawImage(5, 5, image) self.logT("TileLayer.draw() ends") # restore painter state painter.restore() if isDpiEqualToCanvas: # save zoom level for printing (output with different dpi from map canvas) self.canvasLastZoom = zoom return True def drawTiles(self, renderContext, tiles, sdx=1.0, sdy=1.0): # create an image that has the same resolution as the tiles image = tiles.image() # tile extent to pixel map2pixel = renderContext.mapToPixel() extent = tiles.extent() topLeft = map2pixel.transform(extent.xMinimum(), extent.yMaximum()) bottomRight = map2pixel.transform(extent.xMaximum(), extent.yMinimum()) rect = QRectF(QPointF(topLeft.x() * sdx, topLeft.y() * sdy), QPointF(bottomRight.x() * sdx, bottomRight.y() * sdy)) # draw the image on the map canvas renderContext.painter().drawImage(rect, image) self.log("Tiles extent: " + str(extent)) self.log("Draw into canvas rect: " + str(rect)) def drawTilesOnTheFly(self, renderContext, tiles, sdx=1.0, sdy=1.0): if not hasGdal: msg = self.tr("Reprojection requires python-gdal") self.showBarMessage(msg, QgsMessageBar.INFO, 2) return transform = renderContext.coordinateTransform() if not transform: return # create an image that has the same resolution as the tiles image = tiles.image() # tile extent extent = tiles.extent() geotransform = [ extent.xMinimum(), extent.width() / image.width(), 0, extent.yMaximum(), 0, -extent.height() / image.height() ] driver = gdal.GetDriverByName("MEM") tile_ds = driver.Create("", image.width(), image.height(), 1, gdal.GDT_UInt32) tile_ds.SetProjection(str(transform.sourceCrs().toWkt())) tile_ds.SetGeoTransform(geotransform) # QImage to raster ba = image.bits().asstring(image.numBytes()) tile_ds.GetRasterBand(1).WriteRaster(0, 0, image.width(), image.height(), ba) # canvas extent m2p = renderContext.mapToPixel() viewport = renderContext.painter().viewport() width = viewport.width() height = viewport.height() extent = QgsRectangle(m2p.toMapCoordinatesF(0, 0), m2p.toMapCoordinatesF(width, height)) geotransform = [ extent.xMinimum(), extent.width() / width, 0, extent.yMaximum(), 0, -extent.height() / height ] canvas_ds = driver.Create("", width, height, 1, gdal.GDT_UInt32) canvas_ds.SetProjection(str(transform.destCRS().toWkt())) canvas_ds.SetGeoTransform(geotransform) # reproject image gdal.ReprojectImage(tile_ds, canvas_ds) # raster to QImage ba = canvas_ds.GetRasterBand(1).ReadRaster(0, 0, width, height) reprojected_image = QImage(ba, width, height, QImage.Format_ARGB32_Premultiplied) # draw the image on the map canvas rect = QRectF(QPointF(0, 0), QPointF(viewport.width() * sdx, viewport.height() * sdy)) renderContext.painter().drawImage(rect, reprojected_image) def drawTilesDirectly(self, renderContext, tiles, sdx=1.0, sdy=1.0): p = renderContext.painter() for url, tile in tiles.tiles.items(): self.log("Draw tile: zoom: %d, x:%d, y:%d, data:%s" % (tile.zoom, tile.x, tile.y, str(tile.data))) rect = self.getTileRect(renderContext, tile.zoom, tile.x, tile.y, sdx, sdy) if tile.data: image = QImage() image.loadFromData(tile.data) p.drawImage(rect, image) def drawDebugInfo(self, renderContext, zoom, ulx, uly, lrx, lry): painter = renderContext.painter() scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) painter.scale(scaleX, scaleY) if "frame" in self.layerDef.serviceUrl: self.drawFrames(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) if "number" in self.layerDef.serviceUrl: self.drawNumbers(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) if "info" in self.layerDef.serviceUrl: self.drawInfo(renderContext, zoom, ulx, uly, lrx, lry) def drawFrame(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() # p.drawRect(rect) # A slash appears on the top-right tile without Antialiasing render hint. pts = [ rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft(), rect.topLeft() ] for i in range(4): p.drawLine(pts[i], pts[i + 1]) def drawFrames(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawFrame(renderContext, zoom, x, y, sdx, sdy) def drawNumber(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() if not self.layerDef.yOriginTop: y = (2**zoom - 1) - y p.drawText(rect, Qt.AlignCenter, "(%d, %d)\nzoom: %d" % (x, y, zoom)) def drawNumbers(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawNumber(renderContext, zoom, x, y, sdx, sdy) def drawInfo(self, renderContext, zoom, xmin, ymin, xmax, ymax): # debug information mapSettings = self.iface.mapCanvas().mapSettings() lines = [] lines.append("TileLayer") lines.append( " zoom: %d, tile matrix extent: (%d, %d) - (%d, %d), tile count: %d * %d" % (zoom, xmin, ymin, xmax, ymax, xmax - xmin, ymax - ymin)) extent = renderContext.extent() lines.append(" map extent (renderContext): %s" % extent.toString()) lines.append(" map center: %lf, %lf" % (extent.center().x(), extent.center().y())) lines.append(" map size: %f, %f" % (extent.width(), extent.height())) lines.append(" map extent (map canvas): %s" % self.iface.mapCanvas().extent().toString()) m2p = renderContext.mapToPixel() painter = renderContext.painter() viewport = painter.viewport() mapExtent = QgsRectangle( m2p.toMapCoordinatesF(0, 0), m2p.toMapCoordinatesF(viewport.width(), viewport.height())) lines.append(" map extent (calculated): %s" % mapExtent.toString()) lines.append(" viewport size (pixel): %d, %d" % (viewport.width(), viewport.height())) lines.append(" window size (pixel): %d, %d" % (painter.window().width(), painter.window().height())) lines.append(" outputSize (pixel): %d, %d" % (mapSettings.outputSize().width(), mapSettings.outputSize().height())) device = painter.device() lines.append(" deviceSize (pixel): %f, %f" % (device.width(), device.height())) lines.append(" logicalDpi: %f, %f" % (device.logicalDpiX(), device.logicalDpiY())) lines.append(" outputDpi: %f" % mapSettings.outputDpi()) lines.append(" mapToPixel: %s" % m2p.showParameters()) lines.append(" meters per pixel: %f" % (extent.width() / viewport.width())) lines.append(" scaleFactor: %f" % renderContext.scaleFactor()) lines.append(" rendererScale: %f" % renderContext.rendererScale()) scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) lines.append(" scale: %f, %f" % (scaleX, scaleY)) # draw information textRect = painter.boundingRect(QRect(QPoint(0, 0), viewport.size()), Qt.AlignLeft, "Q") for i, line in enumerate(lines): painter.drawText(10, (i + 1) * textRect.height(), line) self.log(line) # diagonal painter.drawLine( QPointF(0, 0), QPointF(painter.viewport().width(), painter.viewport().height())) painter.drawLine(QPointF(painter.viewport().width(), 0), QPointF(0, painter.viewport().height())) # credit label margin, paddingH, paddingV = (3, 4, 3) credit = "This is credit" rect = QRect(0, 0, painter.viewport().width() - margin, painter.viewport().height() - margin) textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, credit) bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) painter.drawRect(bgRect) painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, credit) def getScaleToVisibleExtent(self, renderContext): mapSettings = self.iface.mapCanvas().mapSettings() painter = renderContext.painter() if painter.device().logicalDpiX() == mapSettings.outputDpi(): return 1.0, 1.0 # scale should be 1.0 in rendering on map canvas extent = renderContext.extent() ct = renderContext.coordinateTransform() if ct: # FIX ME: want to get original visible extent in project CRS or visible view size in pixels # extent = ct.transformBoundingBox(extent) #xmax, ymin = extent.xMaximum(), extent.yMinimum() pt1 = ct.transform(extent.xMaximum(), extent.yMaximum()) pt2 = ct.transform(extent.xMaximum(), extent.yMinimum()) pt3 = ct.transform(extent.xMinimum(), extent.yMinimum()) xmax, ymin = min(pt1.x(), pt2.x()), max(pt2.y(), pt3.y()) else: xmax, ymin = extent.xMaximum(), extent.yMinimum() bottomRight = renderContext.mapToPixel().transform(xmax, ymin) viewport = painter.viewport() scaleX = bottomRight.x() / viewport.width() scaleY = bottomRight.y() / viewport.height() return scaleX, scaleY def getTileRect(self, renderContext, zoom, x, y, sdx=1.0, sdy=1.0, toInt=True): """ get tile pixel rect in the render context """ r = self.layerDef.getTileRect(zoom, x, y) map2pix = renderContext.mapToPixel() topLeft = map2pix.transform(r.xMinimum(), r.yMaximum()) bottomRight = map2pix.transform(r.xMaximum(), r.yMinimum()) if toInt: return QRect( QPoint(round(topLeft.x() * sdx), round(topLeft.y() * sdy)), QPoint(round(bottomRight.x() * sdx), round(bottomRight.y() * sdy))) else: return QRectF( QPointF(topLeft.x() * sdx, topLeft.y() * sdy), QPointF(bottomRight.x() * sdx, bottomRight.y() * sdy)) def isProjectCrsWebMercator(self): mapSettings = self.iface.mapCanvas().mapSettings() return mapSettings.destinationCrs().postgisSrid() == 3857 def networkReplyFinished(self, url, error, isFromCache): if self.iface is None or isFromCache: return unfinishedCount = self.downloader.unfinishedCount() if unfinishedCount == 0: self.emit(SIGNAL("allRepliesFinished()")) downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits totalCount = self.downloader.finishedCount() + unfinishedCount msg = self.tr("{0} of {1} files downloaded.").format( downloadedCount, totalCount) if self.downloader.fetchErrors: msg += self.tr(" {} files failed.").format( self.downloader.fetchErrors) self.showStatusMessage(msg) def readXml(self, node): self.readCustomProperties(node) self.layerDef.title = self.customProperty("title", "") self.layerDef.credit = self.customProperty("credit", "") if self.layerDef.credit == "": self.layerDef.credit = self.customProperty( "providerName", "") # for compatibility with 0.11 self.layerDef.serviceUrl = self.customProperty("serviceUrl", "") self.layerDef.yOriginTop = int(self.customProperty("yOriginTop", 1)) self.layerDef.zmin = int( self.customProperty("zmin", TileDefaultSettings.ZMIN)) self.layerDef.zmax = int( self.customProperty("zmax", TileDefaultSettings.ZMAX)) bbox = self.customProperty("bbox", None) if bbox: self.layerDef.bbox = BoundingBox.fromString(bbox) self.setExtent( BoundingBox.degreesToMercatorMeters( self.layerDef.bbox).toQgsRectangle()) # layer style self.setTransparency( int( self.customProperty("transparency", LayerDefaultSettings.TRANSPARENCY))) self.setBlendModeByName( self.customProperty("blendMode", LayerDefaultSettings.BLEND_MODE)) self.setSmoothRender( int( self.customProperty("smoothRender", LayerDefaultSettings.SMOOTH_RENDER))) self.creditVisibility = int(self.customProperty("creditVisibility", 1)) return True def writeXml(self, node, doc): element = node.toElement() element.setAttribute("type", "plugin") element.setAttribute("name", TileLayer.LAYER_TYPE) return True def readSymbology(self, node, errorMessage): return False def writeSymbology(self, node, doc, errorMessage): return False def metadata(self): lines = [] fmt = u"%s:\t%s" lines.append(fmt % (self.tr("Title"), self.layerDef.title)) lines.append(fmt % (self.tr("Credit"), self.layerDef.credit)) lines.append(fmt % (self.tr("URL"), self.layerDef.serviceUrl)) lines.append(fmt % (self.tr("yOrigin"), u"%s (yOriginTop=%d)" % (("Bottom", "Top")[self.layerDef.yOriginTop], self.layerDef.yOriginTop))) if self.layerDef.bbox: extent = self.layerDef.bbox.toString() else: extent = self.tr("Not set") lines.append(fmt % (self.tr("Zoom range"), "%d - %d" % (self.layerDef.zmin, self.layerDef.zmax))) lines.append(fmt % (self.tr("Layer Extent"), extent)) return "\n".join(lines) def log(self, msg): if debug_mode: qDebug(msg) def logT(self, msg): if debug_mode: qDebug("%s: %s" % (str(threading.current_thread()), msg)) def dump(self, detail=False, bbox=None): pass # functions for multi-thread rendering def fetchFiles(self, urls): self.logT("TileLayer.fetchFiles() starts") # create a QEventLoop object that belongs to the current thread (if ver. > 2.1, it is render thread) eventLoop = QEventLoop() self.logT("Create event loop: " + str(eventLoop)) # DEBUG QObject.connect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) # create a timer to watch whether rendering is stopped watchTimer = QTimer() watchTimer.timeout.connect(eventLoop.quit) # send a fetch request to the main thread self.emit(SIGNAL("fetchRequest(QStringList)"), urls) # wait for the fetch to finish tick = 0 interval = 500 timeoutTick = self.plugin.downloadTimeout * 1000 / interval watchTimer.start(interval) while tick < timeoutTick: # run event loop for 0.5 seconds at maximum eventLoop.exec_() if debug_mode: qDebug("watchTimerTick: %d" % tick) qDebug("unfinished downloads: %d" % self.downloader.unfinishedCount()) if self.downloader.unfinishedCount( ) == 0 or self.renderContext.renderingStopped(): break tick += 1 watchTimer.stop() if tick == timeoutTick and self.downloader.unfinishedCount() > 0: self.log("fetchFiles timeout") # self.showBarMessage("fetchFiles timeout", duration=5) #DEBUG self.downloader.abort() self.downloader.errorStatus = Downloader.TIMEOUT_ERROR files = self.downloader.fetchedFiles watchTimer.timeout.disconnect(eventLoop.quit) # QObject.disconnect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) self.logT("TileLayer.fetchFiles() ends") return files def fetchRequest(self, urls): self.logT("TileLayer.fetchRequest()") self.downloader.fetchFilesAsync(urls, self.plugin.downloadTimeout) def showStatusMessage(self, msg, timeout=0): self.emit(SIGNAL("showMessage(QString, int)"), msg, timeout) def showStatusMessageSlot(self, msg, timeout): self.iface.mainWindow().statusBar().showMessage(msg, timeout) def showBarMessage(self, text, level=QgsMessageBar.INFO, duration=0, title=None): if title is None: title = self.plugin.pluginName self.emit(SIGNAL("showBarMessage(QString, QString, int, int)"), title, text, level, duration) def showBarMessageSlot(self, title, text, level, duration): self.iface.messageBar().pushMessage(title, text, level, duration)
class TileLayer(QgsPluginLayer): LAYER_TYPE = "TileLayer" MAX_TILE_COUNT = 256 def __init__(self, plugin, layerDef, creditVisibility=1): QgsPluginLayer.__init__(self, TileLayer.LAYER_TYPE, layerDef.title) self.plugin = plugin self.iface = plugin.iface self.layerDef = layerDef self.creditVisibility = 1 if creditVisibility else 0 # set custom properties self.setCustomProperty("title", layerDef.title) self.setCustomProperty("credit", layerDef.credit) self.setCustomProperty("serviceUrl", layerDef.serviceUrl) self.setCustomProperty("yOriginTop", layerDef.yOriginTop) self.setCustomProperty("zmin", layerDef.zmin) self.setCustomProperty("zmax", layerDef.zmax) if layerDef.bbox: self.setCustomProperty("bbox", layerDef.bbox.toString()) self.setCustomProperty("creditVisibility", self.creditVisibility) # create a QgsCoordinateReferenceSystem instance if plugin has no instance yet if plugin.crs3857 is None: plugin.crs3857 = QgsCoordinateReferenceSystem(3857) self.setCrs(plugin.crs3857) if layerDef.bbox: self.setExtent(BoundingBox.degreesToMercatorMeters(layerDef.bbox).toQgsRectangle()) else: self.setExtent(QgsRectangle(-layerDef.TSIZE1, -layerDef.TSIZE1, layerDef.TSIZE1, layerDef.TSIZE1)) self.setValid(True) self.tiles = None self.useLastZoomForPrint = False self.canvasLastZoom = 0 self.setTransparency(LayerDefaultSettings.TRANSPARENCY) self.setBlendModeByName(LayerDefaultSettings.BLEND_MODE) self.setSmoothRender(LayerDefaultSettings.SMOOTH_RENDER) self.downloader = Downloader(self) self.downloader.userAgent = "QGIS/{0} TileLayerPlugin/{1}".format(QGis.QGIS_VERSION, self.plugin.VERSION) # not written since QGIS 2.2 self.downloader.DEFAULT_CACHE_EXPIRATION = QSettings().value("/qgis/defaultTileExpiry", 24, type=int) QObject.connect(self.downloader, SIGNAL("replyFinished(QString, int, int)"), self.networkReplyFinished) # multi-thread rendering self.eventLoop = None QObject.connect(self, SIGNAL("fetchRequest(QStringList)"), self.fetchRequest) if self.iface: QObject.connect(self, SIGNAL("showMessage(QString, int)"), self.showStatusMessageSlot) QObject.connect(self, SIGNAL("showBarMessage(QString, QString, int, int)"), self.showBarMessageSlot) def setBlendModeByName(self, modeName): self.blendModeName = modeName blendMode = getattr(QPainter, "CompositionMode_" + modeName, 0) self.setBlendMode(blendMode) self.setCustomProperty("blendMode", modeName) def setTransparency(self, transparency): self.transparency = transparency self.setCustomProperty("transparency", transparency) def setSmoothRender(self, isSmooth): self.smoothRender = isSmooth self.setCustomProperty("smoothRender", 1 if isSmooth else 0) def setCreditVisibility(self, visible): self.creditVisibility = visible self.setCustomProperty("creditVisibility", 1 if visible else 0) def draw(self, renderContext): self.renderContext = renderContext extent = renderContext.extent() if extent.isEmpty() or extent.width() == float("inf"): qDebug("Drawing is skipped because map extent is empty or inf.") return True mapSettings = self.iface.mapCanvas().mapSettings() if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer() if self.plugin.apiChanged27 and mapSettings.rotation(): if self.plugin.navigationMessagesEnabled: msg = self.tr("TileLayerPlugin doesn't support map rotation.") self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True painter = renderContext.painter() isDpiEqualToCanvas = painter.device().logicalDpiX() == mapSettings.outputDpi() if isDpiEqualToCanvas or not self.useLastZoomForPrint: # calculate zoom level tile_mpp1 = self.layerDef.TSIZE1 / self.layerDef.TILE_SIZE viewport_mpp = extent.width() / painter.viewport().width() zoom = int(math.ceil(math.log(tile_mpp1 / viewport_mpp, 2) + 1)) zoom = max(0, min(zoom, self.layerDef.zmax)) #zoom = max(self.layerDef.zmin, zoom) else: # for print composer output image, use last zoom level of map item on print composer (or map canvas) zoom = self.canvasLastZoom # zoom limit if zoom < self.layerDef.zmin: if self.plugin.navigationMessagesEnabled: msg = self.tr("Current zoom level ({0}) is smaller than zmin ({1}): {2}").format(zoom, self.layerDef.zmin, self.layerDef.title) self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True while True: # calculate tile range (yOrigin is top) size = self.layerDef.TSIZE1 / 2 ** (zoom - 1) matrixSize = 2 ** zoom ulx = max(0, int((extent.xMinimum() + self.layerDef.TSIZE1) / size)) uly = max(0, int((self.layerDef.TSIZE1 - extent.yMaximum()) / size)) lrx = min(int((extent.xMaximum() + self.layerDef.TSIZE1) / size), matrixSize - 1) lry = min(int((self.layerDef.TSIZE1 - extent.yMinimum()) / size), matrixSize - 1) # bounding box limit if self.layerDef.bbox: trange = self.layerDef.bboxDegreesToTileRange(zoom, self.layerDef.bbox) ulx = max(ulx, trange.xmin) uly = max(uly, trange.ymin) lrx = min(lrx, trange.xmax) lry = min(lry, trange.ymax) if lrx < ulx or lry < uly: # tile range is out of the bounding box return True # tile count limit tileCount = (lrx - ulx + 1) * (lry - uly + 1) if tileCount > self.MAX_TILE_COUNT: # as tile count is over the limit, decrease zoom level zoom -= 1 # if the zoom level is less than the minimum, do not draw if zoom < self.layerDef.zmin: msg = self.tr("Tile count is over limit ({0}, max={1})").format(tileCount, self.MAX_TILE_COUNT) self.showBarMessage(msg, QgsMessageBar.WARNING, 4) return True continue # zoom level has been determined break # frame isn't drawn not in web mercator isWebMercator = self.isProjectCrsWebMercator() if not isWebMercator and self.layerDef.serviceUrl[0] == ":": if "frame" in self.layerDef.serviceUrl: # or "number" in self.layerDef.serviceUrl: msg = self.tr("Frame layer is drawn only in EPSG:3857") self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True self.logT("TileLayer.draw: {0} {1} {2} {3} {4}".format(zoom, ulx, uly, lrx, lry)) # save painter state painter.save() # set pen and font painter.setPen(Qt.black) font = QFont(painter.font()) font.setPointSize(10) painter.setFont(font) if self.layerDef.serviceUrl[0] == ":": painter.setBrush(QBrush(Qt.NoBrush)) self.drawDebugInfo(renderContext, zoom, ulx, uly, lrx, lry) else: # create Tiles class object and throw url into it tiles = Tiles(zoom, ulx, uly, lrx, lry, self.layerDef) urls = [] cacheHits = 0 for ty in range(uly, lry + 1): for tx in range(ulx, lrx + 1): data = None url = self.layerDef.tileUrl(zoom, tx, ty) if self.tiles and zoom == self.tiles.zoom and url in self.tiles.tiles: data = self.tiles.tiles[url].data tiles.addTile(url, Tile(zoom, tx, ty, data)) if data is None: urls.append(url) elif data: # memory cache exists cacheHits += 1 #else: # tile was not found (Downloader.NOT_FOUND=0) self.tiles = tiles if len(urls) > 0: # fetch tile data if self.plugin.apiChanged23: files = self.fetchFiles(urls) else: files = self.downloader.fetchFiles(urls, self.plugin.downloadTimeout) for url in files.keys(): self.tiles.setImageData(url, files[url]) if self.iface: cacheHits += self.downloader.cacheHits downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits msg = self.tr("{0} files downloaded. {1} caches hit.").format(downloadedCount, cacheHits) barmsg = None if self.downloader.errorStatus != Downloader.NO_ERROR: if self.downloader.errorStatus == Downloader.TIMEOUT_ERROR: barmsg = self.tr("Download Timeout - {}").format(self.name()) else: msg += self.tr(" {} files failed.").format(self.downloader.fetchErrors) if self.downloader.fetchSuccesses == 0: barmsg = self.tr("Failed to download all {0} files. - {1}").format(self.downloader.fetchErrors, self.name()) self.showStatusMessage(msg, 5000) if barmsg: self.showBarMessage(barmsg, QgsMessageBar.WARNING, 4) # apply layer style oldOpacity = painter.opacity() painter.setOpacity(0.01 * (100 - self.transparency)) oldSmoothRenderHint = painter.testRenderHint(QPainter.SmoothPixmapTransform) if self.smoothRender: painter.setRenderHint(QPainter.SmoothPixmapTransform) # draw tiles if isWebMercator: # no need to reproject tiles self.drawTiles(renderContext, self.tiles) #self.drawTilesDirectly(renderContext, self.tiles) else: # reproject tiles self.drawTilesOnTheFly(renderContext, self.tiles) # restore layer style painter.setOpacity(oldOpacity) if self.smoothRender: painter.setRenderHint(QPainter.SmoothPixmapTransform, oldSmoothRenderHint) # draw credit on the bottom right corner if self.creditVisibility and self.layerDef.credit: margin, paddingH, paddingV = (3, 4, 3) # scale scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) scale = max(scaleX, scaleY) painter.scale(scale, scale) visibleSWidth = painter.viewport().width() * scaleX / scale visibleSHeight = painter.viewport().height() * scaleY / scale rect = QRect(0, 0, visibleSWidth - margin, visibleSHeight - margin) textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) painter.fillRect(bgRect, QColor(240, 240, 240, 150)) #197, 234, 243, 150)) painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) if 0: #debug_mode: # draw plugin icon image = QImage(os.path.join(os.path.dirname(QFile.decodeName(__file__)), "icon_old.png")) painter.drawImage(5, 5, image) self.logT("TileLayer.draw() ends") # restore painter state painter.restore() if isDpiEqualToCanvas: # save zoom level for printing (output with different dpi from map canvas) self.canvasLastZoom = zoom return True def drawTiles(self, renderContext, tiles, sdx=1.0, sdy=1.0): # create an image that has the same resolution as the tiles image = tiles.image() # tile extent to pixel map2pixel = renderContext.mapToPixel() extent = tiles.extent() topLeft = map2pixel.transform(extent.xMinimum(), extent.yMaximum()) bottomRight = map2pixel.transform(extent.xMaximum(), extent.yMinimum()) rect = QRectF(QPointF(topLeft.x() * sdx, topLeft.y() * sdy), QPointF(bottomRight.x() * sdx, bottomRight.y() * sdy)) # draw the image on the map canvas renderContext.painter().drawImage(rect, image) self.log("Tiles extent: " + str(extent)) self.log("Draw into canvas rect: " + str(rect)) def drawTilesOnTheFly(self, renderContext, tiles, sdx=1.0, sdy=1.0): if not hasGdal: msg = self.tr("Reprojection requires python-gdal") self.showBarMessage(msg, QgsMessageBar.INFO, 2) return transform = renderContext.coordinateTransform() if not transform: return # create an image that has the same resolution as the tiles image = tiles.image() # tile extent extent = tiles.extent() geotransform = [extent.xMinimum(), extent.width() / image.width(), 0, extent.yMaximum(), 0, -extent.height() / image.height()] driver = gdal.GetDriverByName("MEM") tile_ds = driver.Create("", image.width(), image.height(), 1, gdal.GDT_UInt32) tile_ds.SetProjection(str(transform.sourceCrs().toWkt())) tile_ds.SetGeoTransform(geotransform) # QImage to raster ba = image.bits().asstring(image.numBytes()) tile_ds.GetRasterBand(1).WriteRaster(0, 0, image.width(), image.height(), ba) # canvas extent m2p = renderContext.mapToPixel() viewport = renderContext.painter().viewport() width = viewport.width() height = viewport.height() extent = QgsRectangle(m2p.toMapCoordinatesF(0, 0), m2p.toMapCoordinatesF(width, height)) geotransform = [extent.xMinimum(), extent.width() / width, 0, extent.yMaximum(), 0, -extent.height() / height] canvas_ds = driver.Create("", width, height, 1, gdal.GDT_UInt32) canvas_ds.SetProjection(str(transform.destCRS().toWkt())) canvas_ds.SetGeoTransform(geotransform) # reproject image gdal.ReprojectImage(tile_ds, canvas_ds) # raster to QImage ba = canvas_ds.GetRasterBand(1).ReadRaster(0, 0, width, height) reprojected_image = QImage(ba, width, height, QImage.Format_ARGB32_Premultiplied) # draw the image on the map canvas rect = QRectF(QPointF(0, 0), QPointF(viewport.width() * sdx, viewport.height() * sdy)) renderContext.painter().drawImage(rect, reprojected_image) def drawTilesDirectly(self, renderContext, tiles, sdx=1.0, sdy=1.0): p = renderContext.painter() for url, tile in tiles.tiles.items(): self.log("Draw tile: zoom: %d, x:%d, y:%d, data:%s" % (tile.zoom, tile.x, tile.y, str(tile.data))) rect = self.getTileRect(renderContext, tile.zoom, tile.x, tile.y, sdx, sdy) if tile.data: image = QImage() image.loadFromData(tile.data) p.drawImage(rect, image) def drawDebugInfo(self, renderContext, zoom, ulx, uly, lrx, lry): painter = renderContext.painter() scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) painter.scale(scaleX, scaleY) if "frame" in self.layerDef.serviceUrl: self.drawFrames(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) if "number" in self.layerDef.serviceUrl: self.drawNumbers(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) if "info" in self.layerDef.serviceUrl: self.drawInfo(renderContext, zoom, ulx, uly, lrx, lry) def drawFrame(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() #p.drawRect(rect) # A slash appears on the top-right tile without Antialiasing render hint. pts = [rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft(), rect.topLeft()] for i in range(4): p.drawLine(pts[i], pts[i+1]) def drawFrames(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawFrame(renderContext, zoom, x, y, sdx, sdy) def drawNumber(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() if not self.layerDef.yOriginTop: y = (2 ** zoom - 1) - y p.drawText(rect, Qt.AlignCenter, "(%d, %d)\nzoom: %d" % (x, y, zoom)); def drawNumbers(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawNumber(renderContext, zoom, x, y, sdx, sdy) def drawInfo(self, renderContext, zoom, xmin, ymin, xmax, ymax): # debug information mapSettings = self.iface.mapCanvas().mapSettings() if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer() lines = [] lines.append("TileLayer") lines.append(" zoom: %d, tile matrix extent: (%d, %d) - (%d, %d), tile count: %d * %d" % (zoom, xmin, ymin, xmax, ymax, xmax - xmin, ymax - ymin) ) extent = renderContext.extent() lines.append(" map extent (renderContext): %s" % extent.toString() ) lines.append(" map center: %lf, %lf" % (extent.center().x(), extent.center().y() ) ) lines.append(" map size: %f, %f" % (extent.width(), extent.height() ) ) lines.append(" map extent (map canvas): %s" % self.iface.mapCanvas().extent().toString() ) m2p = renderContext.mapToPixel() painter = renderContext.painter() viewport = painter.viewport() mapExtent = QgsRectangle(m2p.toMapCoordinatesF(0, 0), m2p.toMapCoordinatesF(viewport.width(), viewport.height())) lines.append(" map extent (calculated): %s" % mapExtent.toString() ) lines.append(" viewport size (pixel): %d, %d" % (viewport.width(), viewport.height() ) ) lines.append(" window size (pixel): %d, %d" % (painter.window().width(), painter.window().height() ) ) lines.append(" outputSize (pixel): %d, %d" % (mapSettings.outputSize().width(), mapSettings.outputSize().height() ) ) device = painter.device() lines.append(" deviceSize (pixel): %f, %f" % (device.width(), device.height() ) ) lines.append(" logicalDpi: %f, %f" % (device.logicalDpiX(), device.logicalDpiY())) lines.append(" outputDpi: %f" % mapSettings.outputDpi() ) lines.append(" mapToPixel: %s" % m2p.showParameters() ) lines.append(" meters per pixel: %f" % (extent.width() / viewport.width())) lines.append(" scaleFactor: %f" % renderContext.scaleFactor()) lines.append(" rendererScale: %f" % renderContext.rendererScale()) scaleX, scaleY = self.getScaleToVisibleExtent(renderContext) lines.append(" scale: %f, %f" % (scaleX, scaleY)) # draw information textRect = painter.boundingRect(QRect(QPoint(0, 0), viewport.size()), Qt.AlignLeft, "Q") for i, line in enumerate(lines): painter.drawText(10, (i + 1) * textRect.height(), line) self.log(line) # diagonal painter.drawLine(QPointF(0, 0), QPointF(painter.viewport().width(), painter.viewport().height())) painter.drawLine(QPointF(painter.viewport().width(), 0), QPointF(0, painter.viewport().height())) # credit label margin, paddingH, paddingV = (3, 4, 3) credit = "This is credit" rect = QRect(0, 0, painter.viewport().width() - margin, painter.viewport().height() - margin) textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, credit) bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) painter.drawRect(bgRect) painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, credit) def getScaleToVisibleExtent(self, renderContext): mapSettings = self.iface.mapCanvas().mapSettings() if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer() painter = renderContext.painter() if painter.device().logicalDpiX() == mapSettings.outputDpi(): return 1.0, 1.0 # scale should be 1.0 in rendering on map canvas extent = renderContext.extent() ct = renderContext.coordinateTransform() if ct: # FIX ME: want to get original visible extent in project CRS or visible view size in pixels #extent = ct.transformBoundingBox(extent) #xmax, ymin = extent.xMaximum(), extent.yMinimum() pt1 = ct.transform(extent.xMaximum(), extent.yMaximum()) pt2 = ct.transform(extent.xMaximum(), extent.yMinimum()) pt3 = ct.transform(extent.xMinimum(), extent.yMinimum()) xmax, ymin = min(pt1.x(), pt2.x()), max(pt2.y(), pt3.y()) else: xmax, ymin = extent.xMaximum(), extent.yMinimum() bottomRight = renderContext.mapToPixel().transform(xmax, ymin) viewport = painter.viewport() scaleX = bottomRight.x() / viewport.width() scaleY = bottomRight.y() / viewport.height() return scaleX, scaleY def getTileRect(self, renderContext, zoom, x, y, sdx=1.0, sdy=1.0, toInt=True): """ get tile pixel rect in the render context """ r = self.layerDef.getTileRect(zoom, x, y) map2pix = renderContext.mapToPixel() topLeft = map2pix.transform(r.xMinimum(), r.yMaximum()) bottomRight = map2pix.transform(r.xMaximum(), r.yMinimum()) if toInt: return QRect(QPoint(round(topLeft.x() * sdx), round(topLeft.y() * sdy)), QPoint(round(bottomRight.x() * sdx), round(bottomRight.y() * sdy))) else: return QRectF(QPointF(topLeft.x() * sdx, topLeft.y() * sdy), QPointF(bottomRight.x() * sdx, bottomRight.y() * sdy)) def isProjectCrsWebMercator(self): mapSettings = self.iface.mapCanvas().mapSettings() if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer() return mapSettings.destinationCrs().postgisSrid() == 3857 def networkReplyFinished(self, url, error, isFromCache): if self.iface is None or isFromCache: return unfinishedCount = self.downloader.unfinishedCount() if unfinishedCount == 0: self.emit(SIGNAL("allRepliesFinished()")) downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits totalCount = self.downloader.finishedCount() + unfinishedCount msg = self.tr("{0} of {1} files downloaded.").format(downloadedCount, totalCount) if self.downloader.fetchErrors: msg += self.tr(" {} files failed.").format(self.downloader.fetchErrors) self.showStatusMessage(msg) def readXml(self, node): self.readCustomProperties(node) self.layerDef.title = self.customProperty("title", "") self.layerDef.credit = self.customProperty("credit", "") if self.layerDef.credit == "": self.layerDef.credit = self.customProperty("providerName", "") # for compatibility with 0.11 self.layerDef.serviceUrl = self.customProperty("serviceUrl", "") self.layerDef.yOriginTop = int(self.customProperty("yOriginTop", 1)) self.layerDef.zmin = int(self.customProperty("zmin", TileDefaultSettings.ZMIN)) self.layerDef.zmax = int(self.customProperty("zmax", TileDefaultSettings.ZMAX)) bbox = self.customProperty("bbox", None) if bbox: self.layerDef.bbox = BoundingBox.fromString(bbox) self.setExtent(BoundingBox.degreesToMercatorMeters(self.layerDef.bbox).toQgsRectangle()) # layer style self.setTransparency(int(self.customProperty("transparency", LayerDefaultSettings.TRANSPARENCY))) self.setBlendModeByName(self.customProperty("blendMode", LayerDefaultSettings.BLEND_MODE)) self.setSmoothRender(int(self.customProperty("smoothRender", LayerDefaultSettings.SMOOTH_RENDER))) self.creditVisibility = int(self.customProperty("creditVisibility", 1)) return True def writeXml(self, node, doc): element = node.toElement(); element.setAttribute("type", "plugin") element.setAttribute("name", TileLayer.LAYER_TYPE); return True def readSymbology(self, node, errorMessage): return False def writeSymbology(self, node, doc, errorMessage): return False def metadata(self): lines = [] fmt = u"%s:\t%s" lines.append(fmt % (self.tr("Title"), self.layerDef.title)) lines.append(fmt % (self.tr("Credit"), self.layerDef.credit)) lines.append(fmt % (self.tr("URL"), self.layerDef.serviceUrl)) lines.append(fmt % (self.tr("yOrigin"), u"%s (yOriginTop=%d)" % (("Bottom", "Top")[self.layerDef.yOriginTop], self.layerDef.yOriginTop))) if self.layerDef.bbox: extent = self.layerDef.bbox.toString() else: extent = self.tr("Not set") lines.append(fmt % (self.tr("Zoom range"), "%d - %d" % (self.layerDef.zmin, self.layerDef.zmax))) lines.append(fmt % (self.tr("Layer Extent"), extent)) return "\n".join(lines) def log(self, msg): if debug_mode: qDebug(msg) def logT(self, msg): if debug_mode: qDebug("%s: %s" % (str(threading.current_thread()), msg)) def dump(self, detail=False, bbox=None): pass # functions for multi-thread rendering def fetchFiles(self, urls): self.logT("TileLayer.fetchFiles() starts") # create a QEventLoop object that belongs to the current thread (if ver. > 2.1, it is render thread) eventLoop = QEventLoop() self.logT("Create event loop: " + str(eventLoop)) #DEBUG QObject.connect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) # create a timer to watch whether rendering is stopped watchTimer = QTimer() watchTimer.timeout.connect(eventLoop.quit) # send a fetch request to the main thread self.emit(SIGNAL("fetchRequest(QStringList)"), urls) # wait for the fetch to finish tick = 0 interval = 500 timeoutTick = self.plugin.downloadTimeout * 1000 / interval watchTimer.start(interval) while tick < timeoutTick: # run event loop for 0.5 seconds at maximum eventLoop.exec_() if debug_mode: qDebug("watchTimerTick: %d" % tick) qDebug("unfinished downloads: %d" % self.downloader.unfinishedCount()) if self.downloader.unfinishedCount() == 0 or self.renderContext.renderingStopped(): break tick += 1 watchTimer.stop() if tick == timeoutTick and self.downloader.unfinishedCount() > 0: self.log("fetchFiles timeout") #self.showBarMessage("fetchFiles timeout", duration=5) #DEBUG self.downloader.abort() self.downloader.errorStatus = Downloader.TIMEOUT_ERROR files = self.downloader.fetchedFiles watchTimer.timeout.disconnect(eventLoop.quit) # QObject.disconnect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) self.logT("TileLayer.fetchFiles() ends") return files def fetchRequest(self, urls): self.logT("TileLayer.fetchRequest()") self.downloader.fetchFilesAsync(urls, self.plugin.downloadTimeout) def showStatusMessage(self, msg, timeout=0): self.emit(SIGNAL("showMessage(QString, int)"), msg, timeout) def showStatusMessageSlot(self, msg, timeout): self.iface.mainWindow().statusBar().showMessage(msg, timeout) def showBarMessage(self, text, level=QgsMessageBar.INFO, duration=0, title=None): if title is None: title = self.plugin.pluginName self.emit(SIGNAL("showBarMessage(QString, QString, int, int)"), title, text, level, duration) def showBarMessageSlot(self, title, text, level, duration): self.iface.messageBar().pushMessage(title, text, level, duration)
class TileLayer(QgsPluginLayer): LAYER_TYPE = "TileLayer" MAX_TILE_COUNT = 256 RENDER_HINT = QPainter.SmoothPixmapTransform #QPainter.Antialiasing def __init__(self, plugin, layerDef, creditVisibility=1, pseudo_mercator=None): QgsPluginLayer.__init__(self, TileLayer.LAYER_TYPE, layerDef.title) self.plugin = plugin self.iface = plugin.iface self.layerDef = layerDef self.creditVisibility = 1 if creditVisibility else 0 # set custom properties self.setCustomProperty("title", layerDef.title) self.setCustomProperty("credit", layerDef.credit) self.setCustomProperty("serviceUrl", layerDef.serviceUrl) self.setCustomProperty("yOriginTop", layerDef.yOriginTop) self.setCustomProperty("zmin", layerDef.zmin) self.setCustomProperty("zmax", layerDef.zmax) if layerDef.bbox: self.setCustomProperty("bbox", layerDef.bbox.toString()) self.setCustomProperty("creditVisibility", self.creditVisibility) if pseudo_mercator is None: pseudo_mercator = QgsCoordinateReferenceSystem(3857) self.setCrs(pseudo_mercator) if layerDef.bbox: self.setExtent(BoundingBox.degreesToMercatorMeters(layerDef.bbox).toQgsRectangle()) else: self.setExtent(QgsRectangle(-layerDef.TSIZE1, -layerDef.TSIZE1, layerDef.TSIZE1, layerDef.TSIZE1)) self.setValid(True) self.tiles = None self.setTransparency(LayerDefaultSettings.TRANSPARENCY) self.setBlendModeByName(LayerDefaultSettings.BLEND_MODE) self.downloader = Downloader(self) self.downloader.userAgent = "QGIS/{0} TileLayerPlugin/{1}".format(QGis.QGIS_VERSION, self.plugin.VERSION) # not written since QGIS 2.2 self.downloader.DEFAULT_CACHE_EXPIRATION = QSettings().value("/qgis/defaultTileExpiry", 24, type=int) QObject.connect(self.downloader, SIGNAL("replyFinished(QString, int, int)"), self.networkReplyFinished) # multi-thread rendering self.eventLoop = None QObject.connect(self, SIGNAL("fetchRequest(QStringList)"), self.fetchRequest) if self.iface: QObject.connect(self, SIGNAL("showMessage(QString, int)"), self.showStatusMessageSlot) QObject.connect(self, SIGNAL("showBarMessage(QString, QString, int, int)"), self.showBarMessageSlot) def setBlendModeByName(self, modeName): self.blendModeName = modeName blendMode = getattr(QPainter, "CompositionMode_" + modeName, 0) self.setBlendMode(blendMode) self.setCustomProperty("blendMode", modeName) def setTransparency(self, transparency): self.transparency = transparency self.setCustomProperty("transparency", transparency) def setCreditVisibility(self, visible): self.creditVisibility = visible self.setCustomProperty("creditVisibility", 1 if visible else 0) def draw(self, renderContext): self.renderContext = renderContext if renderContext.extent().isEmpty(): qDebug("Drawing is skipped because map extent is empty.") return True painter = renderContext.painter() if not self.isCurrentCrsSupported(): if self.plugin.navigationMessagesEnabled: msg = self.tr("TileLayer is available in EPSG:3857") self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True mapSettings = self.iface.mapCanvas().mapSettings() if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer() isDpiEqualToCanvas = renderContext.painter().device().logicalDpiX() == mapSettings.outputDpi() if isDpiEqualToCanvas: # calculate zoom level mpp1 = self.layerDef.TSIZE1 / self.layerDef.TILE_SIZE zoom = int(math.ceil(math.log(mpp1 / renderContext.mapToPixel().mapUnitsPerPixel(), 2) + 1)) zoom = max(0, min(zoom, self.layerDef.zmax)) #zoom = max(self.layerDef.zmin, zoom) else: # for print composer output image, use last zoom level of map item on print composer (or map canvas) zoom = self.canvasLastZoom # calculate tile range (yOrigin is top) size = self.layerDef.TSIZE1 / 2 ** (zoom - 1) matrixSize = 2 ** zoom ulx = max(0, int((renderContext.extent().xMinimum() + self.layerDef.TSIZE1) / size)) uly = max(0, int((self.layerDef.TSIZE1 - renderContext.extent().yMaximum()) / size)) lrx = min(int((renderContext.extent().xMaximum() + self.layerDef.TSIZE1) / size), matrixSize - 1) lry = min(int((self.layerDef.TSIZE1 - renderContext.extent().yMinimum()) / size), matrixSize - 1) # bounding box limit if self.layerDef.bbox: trange = self.layerDef.bboxDegreesToTileRange(zoom, self.layerDef.bbox) ulx = max(ulx, trange.xmin) uly = max(uly, trange.ymin) lrx = min(lrx, trange.xmax) lry = min(lry, trange.ymax) if lrx < ulx or lry < uly: # the tile range is out of bounding box return True # zoom limit if zoom < self.layerDef.zmin: if self.plugin.navigationMessagesEnabled: msg = self.tr("Current zoom level ({0}) is smaller than zmin ({1}): {2}").format(zoom, self.layerDef.zmin, self.layerDef.title) self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True # tile count limit tileCount = (lrx - ulx + 1) * (lry - uly + 1) if tileCount > self.MAX_TILE_COUNT: msg = self.tr("Tile count is over limit ({0}, max={1})").format(tileCount, self.MAX_TILE_COUNT) self.showBarMessage(msg, QgsMessageBar.WARNING, 4) return True # save painter state painter.save() pt = renderContext.mapToPixel().transform(renderContext.extent().xMaximum(), renderContext.extent().yMinimum()) scaleX = pt.x() / painter.viewport().size().width() scaleY = pt.y() / painter.viewport().size().height() painter.scale(scaleX, scaleY) if debug_mode: self.logT("TileLayer.draw()") qDebug("Bottom-right of extent (pixel): %f, %f" % (pt.x(), pt.y())) # Top-left is (0, 0) qDebug("Calculated scale: %f, %f" % (scaleX, scaleY)) # set pen and font painter.setPen(Qt.black) font = QFont(painter.font()) font.setPointSize(12) painter.setFont(font) if self.layerDef.serviceUrl[0] == ":": painter.setBrush(QBrush(Qt.NoBrush)) self.drawDebugInfo(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) else: # create Tiles class object and throw url into it tiles = Tiles(zoom, ulx, uly, lrx, lry, self.layerDef) urls = [] cacheHits = 0 for ty in range(uly, lry + 1): for tx in range(ulx, lrx + 1): data = None url = self.layerDef.tileUrl(zoom, tx, ty) if self.tiles and zoom == self.tiles.zoom and url in self.tiles.tiles: data = self.tiles.tiles[url].data tiles.addTile(url, Tile(zoom, tx, ty, data)) if data is None: urls.append(url) elif data: # memory cache exists cacheHits += 1 #else: # tile was not found (Downloader.NOT_FOUND=0) self.tiles = tiles if len(urls) > 0: # fetch tile data if self.plugin.apiChanged23: files = self.fetchFiles(urls) else: files = self.downloader.fetchFiles(urls, self.plugin.downloadTimeout) for url in files.keys(): self.tiles.setImageData(url, files[url]) if self.iface: cacheHits += self.downloader.cacheHits downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits msg = self.tr("{0} files downloaded. {1} caches hit.").format(downloadedCount, cacheHits) barmsg = None if self.downloader.errorStatus != Downloader.NO_ERROR: if self.downloader.errorStatus == Downloader.TIMEOUT_ERROR: barmsg = self.tr("Download Timeout - {}").format(self.name()) else: msg += self.tr(" {} files failed.").format(self.downloader.fetchErrors) if self.downloader.fetchSuccesses == 0: barmsg = self.tr("Failed to download all {0} files. - {1}").format(self.downloader.fetchErrors, self.name()) self.showStatusMessage(msg, 5000) if barmsg: self.showBarMessage(barmsg, QgsMessageBar.WARNING, 4) # apply layer style oldStyle = self.prepareStyle(painter) # draw tiles self.drawTiles(renderContext, self.tiles, 1.0 / scaleX, 1.0 / scaleY) #self.drawTilesDirectly(renderContext, self.tiles, 1.0 / scaleX, 1.0 / scaleY) # restore layer style self.restoreStyle(painter, oldStyle) # draw credit on the bottom right corner if self.creditVisibility and self.layerDef.credit != "": margin, paddingH, paddingV = (5, 4, 3) canvasSize = painter.viewport().size() rect = QRect(0, 0, canvasSize.width() - margin, canvasSize.height() - margin) textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) painter.fillRect(bgRect, QColor(240, 240, 240, 150)) #197, 234, 243, 150)) painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) if debug_mode: #painter.fillRect(rect, QColor(240, 240, 240, 200)) qDebug("credit text rect: " + str(textRect)) if 0: #debug_mode: # draw plugin icon image = QImage(os.path.join(os.path.dirname(QFile.decodeName(__file__)), "icon_old.png")) painter.drawImage(5, 5, image) self.logT("TileLayer.draw() ends") # restore painter state painter.restore() if isDpiEqualToCanvas: # save zoom level for printing (output with different dpi from map canvas) self.canvasLastZoom = zoom return True def drawTiles(self, renderContext, tiles, sdx=1.0, sdy=1.0): # create an image that has the same resolution as the tiles image = tiles.image() # tile extent to pixel map2pixel = renderContext.mapToPixel() extent = tiles.extent() topLeft = map2pixel.transform(extent.topLeft().x(), extent.topLeft().y()) bottomRight = map2pixel.transform(extent.bottomRight().x(), extent.bottomRight().y()) rect = QRect(QPoint(round(topLeft.x() * sdx), round(topLeft.y() * sdy)), QPoint(round(bottomRight.x() * sdx), round(bottomRight.y() * sdy))) # draw the image on the map canvas renderContext.painter().drawImage(rect, image) self.log("Tiles extent: " + str(extent)) self.log("Draw into canvas rect: " + str(rect)) def drawTilesDirectly(self, renderContext, tiles, sdx=1.0, sdy=1.0): p = renderContext.painter() for url, tile in tiles.tiles.items(): self.log("Draw tile: zoom: %d, x:%d, y:%d, data:%s" % (tile.zoom, tile.x, tile.y, str(tile.data))) rect = self.getTileRect(renderContext, tile.zoom, tile.x, tile.y, sdx, sdy) if tile.data: image = QImage() image.loadFromData(tile.data) p.drawImage(rect, image) def prepareStyle(self, painter): oldRenderHints = 0 if self.RENDER_HINT is not None: oldRenderHints = painter.renderHints() painter.setRenderHint(self.RENDER_HINT, True) oldOpacity = painter.opacity() painter.setOpacity(0.01 * (100 - self.transparency)) return [oldRenderHints, oldOpacity] def restoreStyle(self, painter, oldStyles): if self.RENDER_HINT is not None: painter.setRenderHints(oldStyles[0]) painter.setOpacity(oldStyles[1]) def drawDebugInfo(self, renderContext, zoom, ulx, uly, lrx, lry, sdx, sdy): if "frame" in self.layerDef.serviceUrl: self.drawFrames(renderContext, zoom, ulx, uly, lrx, lry, sdx, sdy) if "number" in self.layerDef.serviceUrl: self.drawNumbers(renderContext, zoom, ulx, uly, lrx, lry, sdx, sdy) if "info" in self.layerDef.serviceUrl: self.drawInfo(renderContext, zoom, ulx, uly, lrx, lry) def drawFrame(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() p.drawRect(rect) def drawFrames(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawFrame(renderContext, zoom, x, y, sdx, sdy) def drawNumber(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() if not self.layerDef.yOriginTop: y = (2 ** zoom - 1) - y p.drawText(rect, Qt.AlignCenter, "(%d, %d)\nzoom: %d" % (x, y, zoom)); def drawNumbers(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawNumber(renderContext, zoom, x, y, sdx, sdy) def drawInfo(self, renderContext, zoom, xmin, ymin, xmax, ymax): mapSettings = self.iface.mapCanvas().mapSettings() if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer() lines = [] lines.append("TileLayer") lines.append(" zoom: %d, tile matrix extent: (%d, %d) - (%d, %d), tile count: %d * %d" % (zoom, xmin, ymin, xmax, ymax, xmax - xmin, ymax - ymin) ) lines.append(" map extent: %s" % renderContext.extent().toString() ) lines.append(" map center: %lf, %lf" % (renderContext.extent().center().x(), renderContext.extent().center().y() ) ) lines.append(" map size: %f, %f" % (renderContext.extent().width(), renderContext.extent().height() ) ) lines.append(" canvas size (pixel): %d, %d" % (renderContext.painter().viewport().size().width(), renderContext.painter().viewport().size().height() ) ) lines.append(" logicalDpiX: %f" % renderContext.painter().device().logicalDpiX() ) lines.append(" outputDpi: %f" % mapSettings.outputDpi() ) lines.append(" mapToPixel: %s" % renderContext.mapToPixel().showParameters() ) p = renderContext.painter() textRect = p.boundingRect(QRect(QPoint(0, 0), p.viewport().size()), Qt.AlignLeft, "Q") for i, line in enumerate(lines): p.drawText(10, (i + 1) * textRect.height(), line) self.log(line) def getTileRect(self, renderContext, zoom, x, y, sdx=1.0, sdy=1.0): """ get tile pixel rect in the render context """ r = self.layerDef.getTileRect(zoom, x, y) map2pix = renderContext.mapToPixel() topLeft = map2pix.transform(r.xMinimum(), r.yMaximum()) bottomRight = map2pix.transform(r.xMaximum(), r.yMinimum()) return QRect(QPoint(round(topLeft.x() * sdx), round(topLeft.y() * sdy)), QPoint(round(bottomRight.x() * sdx), round(bottomRight.y() * sdy))) #return QRectF(QPointF(round(topLeft.x()), round(topLeft.y())), QPointF(round(bottomRight.x()), round(bottomRight.y()))) #return QgsRectangle(topLeft, bottomRight) def isCurrentCrsSupported(self): mapSettings = self.iface.mapCanvas().mapSettings() if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer() return mapSettings.destinationCrs().postgisSrid() == 3857 def networkReplyFinished(self, url, error, isFromCache): if self.iface is None or isFromCache: return unfinishedCount = self.downloader.unfinishedCount() if unfinishedCount == 0: self.emit(SIGNAL("allRepliesFinished()")) downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits totalCount = self.downloader.finishedCount() + unfinishedCount msg = self.tr("{0} of {1} files downloaded.").format(downloadedCount, totalCount) if self.downloader.fetchErrors: msg += self.tr(" {} files failed.").format(self.downloader.fetchErrors) self.showStatusMessage(msg) def readXml(self, node): self.readCustomProperties(node) self.layerDef.title = self.customProperty("title", "") self.layerDef.credit = self.customProperty("credit", "") if self.layerDef.credit == "": self.layerDef.credit = self.customProperty("providerName", "") # for compatibility with 0.11 self.layerDef.serviceUrl = self.customProperty("serviceUrl", "") self.layerDef.yOriginTop = int(self.customProperty("yOriginTop", 1)) self.layerDef.zmin = int(self.customProperty("zmin", TileDefaultSettings.ZMIN)) self.layerDef.zmax = int(self.customProperty("zmax", TileDefaultSettings.ZMAX)) bbox = self.customProperty("bbox", None) if bbox: self.layerDef.bbox = BoundingBox.fromString(bbox) self.setExtent(BoundingBox.degreesToMercatorMeters(self.layerDef.bbox).toQgsRectangle()) # layer style self.setTransparency(int(self.customProperty("transparency", LayerDefaultSettings.TRANSPARENCY))) self.setBlendModeByName(self.customProperty("blendMode", LayerDefaultSettings.BLEND_MODE)) self.creditVisibility = int(self.customProperty("creditVisibility", 1)) return True def writeXml(self, node, doc): element = node.toElement(); element.setAttribute("type", "plugin") element.setAttribute("name", TileLayer.LAYER_TYPE); return True def metadata(self): lines = [] fmt = u"%s:\t%s" lines.append(fmt % (self.tr("Title"), self.layerDef.title)) lines.append(fmt % (self.tr("Credit"), self.layerDef.credit)) lines.append(fmt % (self.tr("URL"), self.layerDef.serviceUrl)) lines.append(fmt % (self.tr("yOrigin"), u"%s (yOriginTop=%d)" % (("Bottom", "Top")[self.layerDef.yOriginTop], self.layerDef.yOriginTop))) if self.layerDef.bbox: extent = self.layerDef.bbox.toString() else: extent = self.tr("Not set") lines.append(fmt % (self.tr("Zoom range"), "%d - %d" % (self.layerDef.zmin, self.layerDef.zmax))) lines.append(fmt % (self.tr("Layer Extent"), extent)) return "\n".join(lines) def log(self, msg): if debug_mode: qDebug(msg) def logT(self, msg): if debug_mode: qDebug("%s: %s" % (str(threading.current_thread()), msg)) def dump(self, detail=False, bbox=None): pass # functions for multi-thread rendering def fetchFiles(self, urls): self.logT("TileLayer.fetchFiles() starts") # create a QEventLoop object that belongs to the current thread (if ver. > 2.1, it is render thread) eventLoop = QEventLoop() self.logT("Create event loop: " + str(eventLoop)) #DEBUG QObject.connect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) # create a timer to watch whether rendering is stopped watchTimer = QTimer() watchTimer.timeout.connect(eventLoop.quit) # send a fetch request to the main thread self.emit(SIGNAL("fetchRequest(QStringList)"), urls) # wait for the fetch to finish tick = 0 interval = 500 timeoutTick = self.plugin.downloadTimeout * 1000 / interval watchTimer.start(interval) while tick < timeoutTick: # run event loop for 0.5 seconds at maximum eventLoop.exec_() if debug_mode: qDebug("watchTimerTick: %d" % tick) qDebug("unfinished downloads: %d" % self.downloader.unfinishedCount()) if self.downloader.unfinishedCount() == 0 or self.renderContext.renderingStopped(): break tick += 1 watchTimer.stop() if tick == timeoutTick and self.downloader.unfinishedCount() > 0: self.log("fetchFiles timeout") self.showBarMessage("fetchFiles timeout", duration=5) #DEBUG self.downloader.abort() self.downloader.errorStatus = Downloader.TIMEOUT_ERROR files = self.downloader.fetchedFiles watchTimer.timeout.disconnect(eventLoop.quit) # QObject.disconnect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) self.logT("TileLayer.fetchFiles() ends") return files def fetchRequest(self, urls): self.logT("TileLayer.fetchRequest()") self.downloader.fetchFilesAsync(urls, self.plugin.downloadTimeout) def showStatusMessage(self, msg, timeout=0): self.emit(SIGNAL("showMessage(QString, int)"), msg, timeout) def showStatusMessageSlot(self, msg, timeout): self.iface.mainWindow().statusBar().showMessage(msg, timeout) def showBarMessage(self, text, level=QgsMessageBar.INFO, duration=0, title=None): if title is None: title = self.plugin.pluginName self.emit(SIGNAL("showBarMessage(QString, QString, int, int)"), title, text, level, duration) def showBarMessageSlot(self, title, text, level, duration): self.iface.messageBar().pushMessage(title, text, level, duration)
class TileLayer(QgsPluginLayer): LAYER_TYPE = "TileLayer" MAX_TILE_COUNT = 256 RENDER_HINT = QPainter.SmoothPixmapTransform #QPainter.Antialiasing def __init__(self, plugin, layerDef, creditVisibility=1, pseudo_mercator=None): QgsPluginLayer.__init__(self, TileLayer.LAYER_TYPE, layerDef.title) self.plugin = plugin self.iface = plugin.iface self.layerDef = layerDef self.creditVisibility = 1 if creditVisibility else 0 # set custom properties self.setCustomProperty("title", layerDef.title) self.setCustomProperty("credit", layerDef.credit) self.setCustomProperty("serviceUrl", layerDef.serviceUrl) self.setCustomProperty("yOriginTop", layerDef.yOriginTop) self.setCustomProperty("zmin", layerDef.zmin) self.setCustomProperty("zmax", layerDef.zmax) if layerDef.bbox: self.setCustomProperty("bbox", layerDef.bbox.toString()) self.setCustomProperty("creditVisibility", self.creditVisibility) if pseudo_mercator is None: pseudo_mercator = QgsCoordinateReferenceSystem(3857) self.setCrs(pseudo_mercator) if layerDef.bbox: self.setExtent( BoundingBox.degreesToMercatorMeters( layerDef.bbox).toQgsRectangle()) else: self.setExtent( QgsRectangle(-layerDef.TSIZE1, -layerDef.TSIZE1, layerDef.TSIZE1, layerDef.TSIZE1)) self.setValid(True) self.tiles = None self.setTransparency(LayerDefaultSettings.TRANSPARENCY) self.setBlendModeByName(LayerDefaultSettings.BLEND_MODE) self.downloader = Downloader(self) self.downloader.userAgent = "QGIS/{0} TileLayerPlugin/{1}".format( QGis.QGIS_VERSION, self.plugin.VERSION) # not written since QGIS 2.2 self.downloader.DEFAULT_CACHE_EXPIRATION = QSettings().value( "/qgis/defaultTileExpiry", 24, type=int) QObject.connect(self.downloader, SIGNAL("replyFinished(QString, int, int)"), self.networkReplyFinished) # multi-thread rendering self.eventLoop = None QObject.connect(self, SIGNAL("fetchRequest(QStringList)"), self.fetchRequest) if self.iface: QObject.connect(self, SIGNAL("showMessage(QString, int)"), self.showStatusMessageSlot) QObject.connect( self, SIGNAL("showBarMessage(QString, QString, int, int)"), self.showBarMessageSlot) def setBlendModeByName(self, modeName): self.blendModeName = modeName blendMode = getattr(QPainter, "CompositionMode_" + modeName, 0) self.setBlendMode(blendMode) self.setCustomProperty("blendMode", modeName) def setTransparency(self, transparency): self.transparency = transparency self.setCustomProperty("transparency", transparency) def setCreditVisibility(self, visible): self.creditVisibility = visible self.setCustomProperty("creditVisibility", 1 if visible else 0) def draw(self, renderContext): self.renderContext = renderContext if renderContext.extent().isEmpty(): qDebug("Drawing is skipped because map extent is empty.") return True painter = renderContext.painter() if not self.isCurrentCrsSupported(): if self.plugin.navigationMessagesEnabled: msg = self.tr("TileLayer is available in EPSG:3857") self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True mapSettings = self.iface.mapCanvas().mapSettings( ) if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer( ) isDpiEqualToCanvas = renderContext.painter().device().logicalDpiX( ) == mapSettings.outputDpi() if isDpiEqualToCanvas: # calculate zoom level mpp1 = self.layerDef.TSIZE1 / self.layerDef.TILE_SIZE zoom = int( math.ceil( math.log( mpp1 / renderContext.mapToPixel().mapUnitsPerPixel(), 2) + 1)) zoom = max(0, min(zoom, self.layerDef.zmax)) #zoom = max(self.layerDef.zmin, zoom) else: # for print composer output image, use last zoom level of map item on print composer (or map canvas) zoom = self.canvasLastZoom # calculate tile range (yOrigin is top) size = self.layerDef.TSIZE1 / 2**(zoom - 1) matrixSize = 2**zoom ulx = max( 0, int((renderContext.extent().xMinimum() + self.layerDef.TSIZE1) / size)) uly = max( 0, int((self.layerDef.TSIZE1 - renderContext.extent().yMaximum()) / size)) lrx = min( int((renderContext.extent().xMaximum() + self.layerDef.TSIZE1) / size), matrixSize - 1) lry = min( int((self.layerDef.TSIZE1 - renderContext.extent().yMinimum()) / size), matrixSize - 1) # bounding box limit if self.layerDef.bbox: trange = self.layerDef.bboxDegreesToTileRange( zoom, self.layerDef.bbox) ulx = max(ulx, trange.xmin) uly = max(uly, trange.ymin) lrx = min(lrx, trange.xmax) lry = min(lry, trange.ymax) if lrx < ulx or lry < uly: # the tile range is out of bounding box return True # zoom limit if zoom < self.layerDef.zmin: if self.plugin.navigationMessagesEnabled: msg = self.tr( "Current zoom level ({0}) is smaller than zmin ({1}): {2}" ).format(zoom, self.layerDef.zmin, self.layerDef.title) self.showBarMessage(msg, QgsMessageBar.INFO, 2) return True # tile count limit tileCount = (lrx - ulx + 1) * (lry - uly + 1) if tileCount > self.MAX_TILE_COUNT: msg = self.tr("Tile count is over limit ({0}, max={1})").format( tileCount, self.MAX_TILE_COUNT) self.showBarMessage(msg, QgsMessageBar.WARNING, 4) return True # save painter state painter.save() pt = renderContext.mapToPixel().transform( renderContext.extent().xMaximum(), renderContext.extent().yMinimum()) scaleX = pt.x() / painter.viewport().size().width() scaleY = pt.y() / painter.viewport().size().height() painter.scale(scaleX, scaleY) if debug_mode: self.logT("TileLayer.draw()") qDebug("Bottom-right of extent (pixel): %f, %f" % (pt.x(), pt.y())) # Top-left is (0, 0) qDebug("Calculated scale: %f, %f" % (scaleX, scaleY)) # set pen and font painter.setPen(Qt.black) font = QFont(painter.font()) font.setPointSize(12) painter.setFont(font) if self.layerDef.serviceUrl[0] == ":": painter.setBrush(QBrush(Qt.NoBrush)) self.drawDebugInfo(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY) else: # create Tiles class object and throw url into it tiles = Tiles(zoom, ulx, uly, lrx, lry, self.layerDef) urls = [] cacheHits = 0 for ty in range(uly, lry + 1): for tx in range(ulx, lrx + 1): data = None url = self.layerDef.tileUrl(zoom, tx, ty) if self.tiles and zoom == self.tiles.zoom and url in self.tiles.tiles: data = self.tiles.tiles[url].data tiles.addTile(url, Tile(zoom, tx, ty, data)) if data is None: urls.append(url) elif data: # memory cache exists cacheHits += 1 #else: # tile was not found (Downloader.NOT_FOUND=0) self.tiles = tiles if len(urls) > 0: # fetch tile data if self.plugin.apiChanged23: files = self.fetchFiles(urls) else: files = self.downloader.fetchFiles( urls, self.plugin.downloadTimeout) for url in files.keys(): self.tiles.setImageData(url, files[url]) if self.iface: cacheHits += self.downloader.cacheHits downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits msg = self.tr( "{0} files downloaded. {1} caches hit.").format( downloadedCount, cacheHits) barmsg = None if self.downloader.errorStatus != Downloader.NO_ERROR: if self.downloader.errorStatus == Downloader.TIMEOUT_ERROR: barmsg = self.tr("Download Timeout - {}").format( self.name()) else: msg += self.tr(" {} files failed.").format( self.downloader.fetchErrors) if self.downloader.fetchSuccesses == 0: barmsg = self.tr( "Failed to download all {0} files. - {1}" ).format(self.downloader.fetchErrors, self.name()) self.showStatusMessage(msg, 5000) if barmsg: self.showBarMessage(barmsg, QgsMessageBar.WARNING, 4) # apply layer style oldStyle = self.prepareStyle(painter) # draw tiles self.drawTiles(renderContext, self.tiles, 1.0 / scaleX, 1.0 / scaleY) #self.drawTilesDirectly(renderContext, self.tiles, 1.0 / scaleX, 1.0 / scaleY) # restore layer style self.restoreStyle(painter, oldStyle) # draw credit on the bottom right corner if self.creditVisibility and self.layerDef.credit != "": margin, paddingH, paddingV = (5, 4, 3) canvasSize = painter.viewport().size() rect = QRect(0, 0, canvasSize.width() - margin, canvasSize.height() - margin) textRect = painter.boundingRect(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) painter.fillRect(bgRect, QColor(240, 240, 240, 150)) #197, 234, 243, 150)) painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.credit) if debug_mode: #painter.fillRect(rect, QColor(240, 240, 240, 200)) qDebug("credit text rect: " + str(textRect)) if 0: #debug_mode: # draw plugin icon image = QImage( os.path.join(os.path.dirname(QFile.decodeName(__file__)), "icon_old.png")) painter.drawImage(5, 5, image) self.logT("TileLayer.draw() ends") # restore painter state painter.restore() if isDpiEqualToCanvas: # save zoom level for printing (output with different dpi from map canvas) self.canvasLastZoom = zoom return True def drawTiles(self, renderContext, tiles, sdx=1.0, sdy=1.0): # create an image that has the same resolution as the tiles image = tiles.image() # tile extent to pixel map2pixel = renderContext.mapToPixel() extent = tiles.extent() topLeft = map2pixel.transform(extent.topLeft().x(), extent.topLeft().y()) bottomRight = map2pixel.transform(extent.bottomRight().x(), extent.bottomRight().y()) rect = QRect( QPoint(round(topLeft.x() * sdx), round(topLeft.y() * sdy)), QPoint(round(bottomRight.x() * sdx), round(bottomRight.y() * sdy))) # draw the image on the map canvas renderContext.painter().drawImage(rect, image) self.log("Tiles extent: " + str(extent)) self.log("Draw into canvas rect: " + str(rect)) def drawTilesDirectly(self, renderContext, tiles, sdx=1.0, sdy=1.0): p = renderContext.painter() for url, tile in tiles.tiles.items(): self.log("Draw tile: zoom: %d, x:%d, y:%d, data:%s" % (tile.zoom, tile.x, tile.y, str(tile.data))) rect = self.getTileRect(renderContext, tile.zoom, tile.x, tile.y, sdx, sdy) if tile.data: image = QImage() image.loadFromData(tile.data) p.drawImage(rect, image) def prepareStyle(self, painter): oldRenderHints = 0 if self.RENDER_HINT is not None: oldRenderHints = painter.renderHints() painter.setRenderHint(self.RENDER_HINT, True) oldOpacity = painter.opacity() painter.setOpacity(0.01 * (100 - self.transparency)) return [oldRenderHints, oldOpacity] def restoreStyle(self, painter, oldStyles): if self.RENDER_HINT is not None: painter.setRenderHints(oldStyles[0]) painter.setOpacity(oldStyles[1]) def drawDebugInfo(self, renderContext, zoom, ulx, uly, lrx, lry, sdx, sdy): if "frame" in self.layerDef.serviceUrl: self.drawFrames(renderContext, zoom, ulx, uly, lrx, lry, sdx, sdy) if "number" in self.layerDef.serviceUrl: self.drawNumbers(renderContext, zoom, ulx, uly, lrx, lry, sdx, sdy) if "info" in self.layerDef.serviceUrl: self.drawInfo(renderContext, zoom, ulx, uly, lrx, lry) def drawFrame(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() p.drawRect(rect) def drawFrames(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawFrame(renderContext, zoom, x, y, sdx, sdy) def drawNumber(self, renderContext, zoom, x, y, sdx, sdy): rect = self.getTileRect(renderContext, zoom, x, y, sdx, sdy) p = renderContext.painter() if not self.layerDef.yOriginTop: y = (2**zoom - 1) - y p.drawText(rect, Qt.AlignCenter, "(%d, %d)\nzoom: %d" % (x, y, zoom)) def drawNumbers(self, renderContext, zoom, xmin, ymin, xmax, ymax, sdx, sdy): for y in range(ymin, ymax + 1): for x in range(xmin, xmax + 1): self.drawNumber(renderContext, zoom, x, y, sdx, sdy) def drawInfo(self, renderContext, zoom, xmin, ymin, xmax, ymax): mapSettings = self.iface.mapCanvas().mapSettings( ) if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer( ) lines = [] lines.append("TileLayer") lines.append( " zoom: %d, tile matrix extent: (%d, %d) - (%d, %d), tile count: %d * %d" % (zoom, xmin, ymin, xmax, ymax, xmax - xmin, ymax - ymin)) lines.append(" map extent: %s" % renderContext.extent().toString()) lines.append(" map center: %lf, %lf" % (renderContext.extent().center().x(), renderContext.extent().center().y())) lines.append( " map size: %f, %f" % (renderContext.extent().width(), renderContext.extent().height())) lines.append(" canvas size (pixel): %d, %d" % (renderContext.painter().viewport().size().width(), renderContext.painter().viewport().size().height())) lines.append(" logicalDpiX: %f" % renderContext.painter().device().logicalDpiX()) lines.append(" outputDpi: %f" % mapSettings.outputDpi()) lines.append(" mapToPixel: %s" % renderContext.mapToPixel().showParameters()) p = renderContext.painter() textRect = p.boundingRect(QRect(QPoint(0, 0), p.viewport().size()), Qt.AlignLeft, "Q") for i, line in enumerate(lines): p.drawText(10, (i + 1) * textRect.height(), line) self.log(line) def getTileRect(self, renderContext, zoom, x, y, sdx=1.0, sdy=1.0): """ get tile pixel rect in the render context """ r = self.layerDef.getTileRect(zoom, x, y) map2pix = renderContext.mapToPixel() topLeft = map2pix.transform(r.xMinimum(), r.yMaximum()) bottomRight = map2pix.transform(r.xMaximum(), r.yMinimum()) return QRect( QPoint(round(topLeft.x() * sdx), round(topLeft.y() * sdy)), QPoint(round(bottomRight.x() * sdx), round(bottomRight.y() * sdy))) #return QRectF(QPointF(round(topLeft.x()), round(topLeft.y())), QPointF(round(bottomRight.x()), round(bottomRight.y()))) #return QgsRectangle(topLeft, bottomRight) def isCurrentCrsSupported(self): mapSettings = self.iface.mapCanvas().mapSettings( ) if self.plugin.apiChanged23 else self.iface.mapCanvas().mapRenderer( ) return mapSettings.destinationCrs().postgisSrid() == 3857 def networkReplyFinished(self, url, error, isFromCache): if self.iface is None or isFromCache: return unfinishedCount = self.downloader.unfinishedCount() if unfinishedCount == 0: self.emit(SIGNAL("allRepliesFinished()")) downloadedCount = self.downloader.fetchSuccesses - self.downloader.cacheHits totalCount = self.downloader.finishedCount() + unfinishedCount msg = self.tr("{0} of {1} files downloaded.").format( downloadedCount, totalCount) if self.downloader.fetchErrors: msg += self.tr(" {} files failed.").format( self.downloader.fetchErrors) self.showStatusMessage(msg) def readXml(self, node): self.readCustomProperties(node) self.layerDef.title = self.customProperty("title", "") self.layerDef.credit = self.customProperty("credit", "") if self.layerDef.credit == "": self.layerDef.credit = self.customProperty( "providerName", "") # for compatibility with 0.11 self.layerDef.serviceUrl = self.customProperty("serviceUrl", "") self.layerDef.yOriginTop = int(self.customProperty("yOriginTop", 1)) self.layerDef.zmin = int( self.customProperty("zmin", TileDefaultSettings.ZMIN)) self.layerDef.zmax = int( self.customProperty("zmax", TileDefaultSettings.ZMAX)) bbox = self.customProperty("bbox", None) if bbox: self.layerDef.bbox = BoundingBox.fromString(bbox) self.setExtent( BoundingBox.degreesToMercatorMeters( self.layerDef.bbox).toQgsRectangle()) # layer style self.setTransparency( int( self.customProperty("transparency", LayerDefaultSettings.TRANSPARENCY))) self.setBlendModeByName( self.customProperty("blendMode", LayerDefaultSettings.BLEND_MODE)) self.creditVisibility = int(self.customProperty("creditVisibility", 1)) return True def writeXml(self, node, doc): element = node.toElement() element.setAttribute("type", "plugin") element.setAttribute("name", TileLayer.LAYER_TYPE) return True def metadata(self): lines = [] fmt = u"%s:\t%s" lines.append(fmt % (self.tr("Title"), self.layerDef.title)) lines.append(fmt % (self.tr("Credit"), self.layerDef.credit)) lines.append(fmt % (self.tr("URL"), self.layerDef.serviceUrl)) lines.append(fmt % (self.tr("yOrigin"), u"%s (yOriginTop=%d)" % (("Bottom", "Top")[self.layerDef.yOriginTop], self.layerDef.yOriginTop))) if self.layerDef.bbox: extent = self.layerDef.bbox.toString() else: extent = self.tr("Not set") lines.append(fmt % (self.tr("Zoom range"), "%d - %d" % (self.layerDef.zmin, self.layerDef.zmax))) lines.append(fmt % (self.tr("Layer Extent"), extent)) return "\n".join(lines) def log(self, msg): if debug_mode: qDebug(msg) def logT(self, msg): if debug_mode: qDebug("%s: %s" % (str(threading.current_thread()), msg)) def dump(self, detail=False, bbox=None): pass # functions for multi-thread rendering def fetchFiles(self, urls): self.logT("TileLayer.fetchFiles() starts") # create a QEventLoop object that belongs to the current thread (if ver. > 2.1, it is render thread) eventLoop = QEventLoop() self.logT("Create event loop: " + str(eventLoop)) #DEBUG QObject.connect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) # create a timer to watch whether rendering is stopped watchTimer = QTimer() watchTimer.timeout.connect(eventLoop.quit) # send a fetch request to the main thread self.emit(SIGNAL("fetchRequest(QStringList)"), urls) # wait for the fetch to finish tick = 0 interval = 500 timeoutTick = self.plugin.downloadTimeout * 1000 / interval watchTimer.start(interval) while tick < timeoutTick: # run event loop for 0.5 seconds at maximum eventLoop.exec_() if debug_mode: qDebug("watchTimerTick: %d" % tick) qDebug("unfinished downloads: %d" % self.downloader.unfinishedCount()) if self.downloader.unfinishedCount( ) == 0 or self.renderContext.renderingStopped(): break tick += 1 watchTimer.stop() if tick == timeoutTick and self.downloader.unfinishedCount() > 0: self.log("fetchFiles timeout") self.showBarMessage("fetchFiles timeout", duration=5) #DEBUG self.downloader.abort() self.downloader.errorStatus = Downloader.TIMEOUT_ERROR files = self.downloader.fetchedFiles watchTimer.timeout.disconnect(eventLoop.quit) # QObject.disconnect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit) self.logT("TileLayer.fetchFiles() ends") return files def fetchRequest(self, urls): self.logT("TileLayer.fetchRequest()") self.downloader.fetchFilesAsync(urls, self.plugin.downloadTimeout) def showStatusMessage(self, msg, timeout=0): self.emit(SIGNAL("showMessage(QString, int)"), msg, timeout) def showStatusMessageSlot(self, msg, timeout): self.iface.mainWindow().statusBar().showMessage(msg, timeout) def showBarMessage(self, text, level=QgsMessageBar.INFO, duration=0, title=None): if title is None: title = self.plugin.pluginName self.emit(SIGNAL("showBarMessage(QString, QString, int, int)"), title, text, level, duration) def showBarMessageSlot(self, title, text, level, duration): self.iface.messageBar().pushMessage(title, text, level, duration)
class TileManager(QObject): # PyQt signals fetchRequestSignal = pyqtSignal(list) def __init__(self, parent, layerDef): QObject.__init__(self, parent) self.parent = parent self.layerDef = layerDef self.tile_collection = None self.cache_hits=0 # downloader maxConnections = layerDef.max_connections cacheExpiry = QSettings().value("/qgis/defaultTileExpiry", 24, type=int) userAgent = "QGIS/{0} IdahoLayerPlugin/{1}".format(QGis.QGIS_VERSION, self.parent.plugin.VERSION) # will be overwritten in QgsNetworkAccessManager::createRequest() since 2.2 self.downloader = Downloader(self.parent, maxConnections, cacheExpiry, userAgent) if self.parent.iface: self.downloader.replyFinished.connect(self.networkReplyFinished) # download progress # multi-thread rendering self.eventLoop = None self.fetchRequestSignal.connect(self.fetchRequestSlot) def update(self, zoom, tile_range): ulx = tile_range.ulx uly = tile_range.uly lrx = tile_range.lrx lry = tile_range.lry # create TilesCollection class object and throw url into it tiles = TilesCollection(zoom, ulx, uly, lrx, lry, self.layerDef) urls = [] self.cache_hits = 0 for ty in range(uly, lry + 1): for tx in range(ulx, lrx + 1): data = None url = self.layerDef.tileUrl(zoom, tx, ty) if self.tile_collection and zoom == self.tile_collection.zoom and url in self.tile_collection.tiles: data = self.tile_collection.tiles[url].pixel_data tiles.add_tile(url, Tile(zoom, tx, ty, data)) if data is None: urls.append(url) elif data: # memory cache exists self.cache_hits += 1 if len(urls) > 0: # fetch tile data processed_requests = self.processedRequests(urls) for url in urls: response = processed_requests.get(url) if response: data = response.pdata if data: tiles.tile_set_pixel_data(url, data) self.tile_collection = tiles def get_tiles(self): return self.tile_collection # functions for multi-thread rendering def processedRequests(self, urls): if not self.parent.plugin.apiChanged23: return self.downloader.fetchFiles(urls, self.parent.plugin.downloadTimeout) # create a QEventLoop object that belongs to the current worker thread eventLoop = QEventLoop() self.downloader.allRepliesFinished.connect(eventLoop.quit) # create a timer to watch whether rendering is stopped watch_timer= QTimer() watch_timer.timeout.connect(eventLoop.quit) # send a fetch request to the main thread self.fetchRequestSignal.emit(urls) # wait for the fetch to finish tick = 0 interval = 500 timeout_tick = self.parent.plugin.downloadTimeout * 1000 / interval watch_timer.start(interval) while tick < timeout_tick: # run event loop for 0.5 seconds at maximum eventLoop.exec_() if self.downloader.unfinishedCount() == 0 or self.parent.renderContext.renderingStopped(): break tick += 1 watch_timer.stop() if tick == timeout_tick and self.downloader.unfinishedCount() > 0: self.downloader.abort() self.downloader.errorStatus = Downloader.TIMEOUT_ERROR requests = self.downloader.requests watch_timer.timeout.disconnect(eventLoop.quit) # self.downloader.allRepliesFinished.disconnect(eventLoop.quit) return requests def fetchRequestSlot(self, urls): unicode_urls = [] for url in urls: unicode_urls.append(QUrl(url).toString()) self.downloader.fetchFilesAsync(unicode_urls, self.parent.plugin.downloadTimeout) def networkReplyFinished(self, url): # show progress stats = self.downloader.stats() failed = "" if stats["failed"]: failed += " {} {}".format(stats["failed"], self.tr("failed")) timedout = "" if stats["timedout"]: timedout += " {} {}".format(stats["timedout"], self.tr("timedout")) errors = "" errors += ", "+ failed+", " if len(failed)>0 else "" errors += ", " + timedout if len(timedout)>0 else "" msg = self.tr("{} {} {} ({} {}, {} {}{})").format(stats["pending"] + stats["incomplete"], self.tr("tiles"), self.tr("pending"), stats["downloaded"], self.tr("downloaded"), stats["from_cache"], self.tr("from cache"), errors) self.parent.showStatusMessage(msg)