class TileLayer(QgsPluginLayer): LAYER_TYPE = "TileLayer" MAX_TILE_COUNT = 256 DEFAULT_BLEND_MODE = "SourceOver" DEFAULT_SMOOTH_RENDER = True # PyQt signals statusSignal = pyqtSignal(str, int) messageBarSignal = pyqtSignal(str, str, int, int) 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 self.tiles = None # set attribution property self.setAttribution(layerDef.attribution) # set custom properties self.setCustomProperty("title", layerDef.title) self.setCustomProperty("credit", layerDef.attribution) 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) # set crs if plugin.crs3857 is None: # create a QgsCoordinateReferenceSystem instance if plugin has no instance yet plugin.crs3857 = QgsCoordinateReferenceSystem(3857) self.setCrs(plugin.crs3857) # set extent if layerDef.bbox: self.setExtent(BoundingBox.degreesToMercatorMeters(layerDef.bbox).toQgsRectangle()) else: self.setExtent(QgsRectangle(-layerDef.TSIZE1, -layerDef.TSIZE1, layerDef.TSIZE1, layerDef.TSIZE1)) # set styles self.setTransparency(0) self.setBlendModeByName(self.DEFAULT_BLEND_MODE) self.setSmoothRender(self.DEFAULT_SMOOTH_RENDER) # downloader self.maxConnections = HonestAccess.maxConnections(layerDef.serviceUrl) self.cacheExpiry = QSettings().value("/qgis/defaultTileExpiry", 24, type=int) self.userAgent = "QGIS/{0} TileLayerPlugin/{1}".format(QGis.QGIS_VERSION, self.plugin.VERSION) # will be overwritten in QgsNetworkAccessManager::createRequest() since 2.2 self.downloader = Downloader(self, self.maxConnections, self.cacheExpiry, self.userAgent) # TOS violation warning if HonestAccess.restrictedByTOS(layerDef.serviceUrl): QMessageBox.warning(None, u"{0} - {1}".format(self.tr("TileLayerPlugin"), layerDef.title), self.tr("Access to the service is restricted by the TOS. Please follow the TOS.")) # multi-thread rendering if self.iface: self.statusSignal.connect(self.showStatusMessageSlot) self.messageBarSignal.connect(self.showMessageBarSlot) self.setValid(True) 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): extent = renderContext.extent() if extent.isEmpty() or extent.width() == float("inf"): qDebug("Drawing is skipped because map extent is empty or inf.") return True map2pixel = renderContext.mapToPixel() mupp = map2pixel.mapUnitsPerPixel() rotation = map2pixel.mapRotation() painter = renderContext.painter() viewport = painter.viewport() isWebMercator = True transform = renderContext.coordinateTransform() if transform: isWebMercator = transform.destCRS().postgisSrid() == 3857 # frame layer isn't drawn if the CRS is not web mercator or map is rotated if self.layerDef.serviceUrl[0] == ":" and "frame" in self.layerDef.serviceUrl: # or "number" in self.layerDef.serviceUrl: msg = "" if not isWebMercator: msg = self.tr("Frame layer is not drawn if the CRS is not EPSG:3857") elif rotation: msg = self.tr("Frame layer is not drawn if map is rotated") if msg: self.showMessageBar(msg, QgsMessageBar.INFO, 2) return True if not isWebMercator: # get extent in project CRS cx, cy = 0.5 * viewport.width(), 0.5 * viewport.height() center = map2pixel.toMapCoordinatesF(cx, cy) mapExtent = RotatedRect(center, mupp * viewport.width(), mupp * viewport.height(), rotation) if transform: transform = QgsCoordinateTransform(transform.destCRS(), transform.sourceCrs()) geometry = QgsGeometry.fromPolyline([map2pixel.toMapCoordinatesF(cx - 0.5, cy), map2pixel.toMapCoordinatesF(cx + 0.5, cy)]) geometry.transform(transform) mupp = geometry.length() # get bounding box of the extent in EPSG:3857 geometry = mapExtent.geometry() geometry.transform(transform) extent = geometry.boundingBox() else: qDebug("Drawing is skipped because CRS transformation is not ready.") return True elif rotation: # get bounding box of the extent mapExtent = RotatedRect(extent.center(), mupp * viewport.width(), mupp * viewport.height(), rotation) extent = mapExtent.boundingBox() # calculate zoom level tile_mpp1 = self.layerDef.TSIZE1 / self.layerDef.TILE_SIZE zoom = int(math.ceil(math.log(tile_mpp1 / mupp, 2) + 1)) zoom = max(0, min(zoom, self.layerDef.zmax)) #zoom = max(self.layerDef.zmin, zoom) # 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.showMessageBar(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.showMessageBar(msg, QgsMessageBar.WARNING, 4) return True continue # zoom level has been determined break 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 a Tiles object and a list of urls to fetch tile image data tiles = Tiles(zoom, ulx, uly, lrx, lry, self.layerDef) urls = [] cachedTiles = self.tiles 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 cachedTiles and zoom == cachedTiles.zoom and url in cachedTiles.tiles: data = cachedTiles.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 not found self.tiles = tiles if len(urls) > 0: # fetch tile data files = self.fetchFiles(urls, renderContext) for url, data in files.items(): tiles.setImageData(url, data) if self.iface: stats = self.downloader.stats() allCacheHits = cacheHits + stats["cacheHits"] msg = self.tr("{0} files downloaded. {1} caches hit.").format(stats["downloaded"], allCacheHits) barmsg = None if self.downloader.errorStatus != Downloader.NO_ERROR: if self.downloader.errorStatus == Downloader.TIMEOUT_ERROR: barmsg = self.tr("Download Timeout - {0}").format(self.name()) elif stats["errors"] > 0: msg += self.tr(" {0} files failed.").format(stats["errors"]) if stats["successed"] + allCacheHits == 0: barmsg = self.tr("Failed to download all {0} files. - {1}").format(stats["errors"], self.name()) self.showStatusMessage(msg, 5000) if barmsg: self.showMessageBar(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) # do not start drawing tiles if rendering has been stopped if renderContext.renderingStopped(): self.log("draw(): renderingStopped!") painter.restore() return True # draw tiles if isWebMercator and rotation == 0: self.drawTiles(renderContext, tiles) # self.drawTilesDirectly(renderContext, tiles) else: # reproject tiles self.drawTilesOnTheFly(renderContext, mapExtent, tiles) # restore old state painter.setOpacity(oldOpacity) if self.smoothRender: painter.setRenderHint(QPainter.SmoothPixmapTransform, oldSmoothRenderHint) # draw credit on the bottom right corner if self.creditVisibility and self.layerDef.attribution: 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.attribution) bgRect = QRect(textRect.left() - paddingH, textRect.top() - paddingV, textRect.width() + 2 * paddingH, textRect.height() + 2 * paddingV) painter.fillRect(bgRect, QColor(240, 240, 240, 150)) painter.drawText(rect, Qt.AlignBottom | Qt.AlignRight, self.layerDef.attribution) # restore painter state painter.restore() 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("drawTiles: {0} - {1}".format(str(extent), str(rect))) def drawTilesOnTheFly(self, renderContext, mapExtent, tiles, sdx=1.0, sdy=1.0): if not hasGdal: msg = self.tr("Rotation/Reprojection requires python-gdal") self.showMessageBar(msg, QgsMessageBar.INFO, 2) return transform = renderContext.coordinateTransform() if transform: sourceCrs = transform.sourceCrs() destCrs = transform.destCRS() else: sourceCrs = destCrs = self.crs() # create image from 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()] # source raster dataset driver = gdal.GetDriverByName("MEM") tile_ds = driver.Create("", image.width(), image.height(), 1, gdal.GDT_UInt32) tile_ds.SetProjection(str(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) # target raster size - if smoothing is enabled, create raster of twice each of width and height of viewport size # in order to get high quality image oversampl = 2 if self.smoothRender else 1 painter = renderContext.painter() viewport = painter.viewport() width, height = viewport.width() * oversampl, viewport.height() * oversampl # target raster dataset canvas_ds = driver.Create("", width, height, 1, gdal.GDT_UInt32) canvas_ds.SetProjection(str(destCrs.toWkt())) canvas_ds.SetGeoTransform(mapExtent.geotransform(width, height, is_grid_point=False)) # 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)) 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): from debuginfo import drawDebugInformation drawDebugInformation(self, renderContext, zoom, xmin, ymin, xmax, ymax) 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 networkReplyFinished(self, url): # show progress stats = self.downloader.stats() msg = self.tr("{0} of {1} files downloaded.").format(stats["downloaded"], stats["total"]) errors = stats["errors"] if errors: msg += self.tr(" {0} files failed.").format(errors) self.showStatusMessage(msg) def readXml(self, node): self.readCustomProperties(node) self.layerDef.title = self.customProperty("title", "") self.layerDef.attribution = self.customProperty("credit", "") if self.layerDef.attribution == "": self.layerDef.attribution = 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", 0))) self.setBlendModeByName(self.customProperty("blendMode", self.DEFAULT_BLEND_MODE)) self.setSmoothRender(int(self.customProperty("smoothRender", self.DEFAULT_SMOOTH_RENDER))) self.creditVisibility = int(self.customProperty("creditVisibility", 1)) # max connections of downloader self.maxConnections = HonestAccess.maxConnections(self.layerDef.serviceUrl) 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("Attribution"), self.layerDef.attribution)) 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 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 def showStatusMessage(self, msg, timeout=0): self.statusSignal.emit(msg, timeout) #TODO: use QMetaObject.invokeMethod def showStatusMessageSlot(self, msg, timeout): self.iface.mainWindow().statusBar().showMessage(msg, timeout) def showMessageBar(self, text, level=QgsMessageBar.INFO, duration=0, title=None): if title is None: title = self.plugin.pluginName self.messageBarSignal.emit(title, text, level, duration) def showMessageBarSlot(self, title, text, level, duration): self.iface.messageBar().pushMessage(title, text, level, duration) 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
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)