def __init__(self, layerDef, creditVisibility=1):
        QgsPluginLayer.__init__(self, TileLayer.LAYER_TYPE, layerDef.title)
        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)

        # set standard/custom crs
        self.setCrs(self.CRS_3857)
        try:
            crs = None
            if layerDef.epsg_crs_id is not None:
                crs = QgsCoordinateReferenceSystem(layerDef.epsg_crs_id, QgsCoordinateReferenceSystem.EpsgCrsId)
            if layerDef.postgis_crs_id is not None:
                crs = QgsCoordinateReferenceSystem(layerDef.postgis_crs_id, QgsCoordinateReferenceSystem.PostgisCrsId)
            if layerDef.custom_proj is not None:
                # create form proj4 str
                custom_crs = QgsCoordinateReferenceSystem()
                custom_crs.createFromProj4(layerDef.custom_proj)
                # try to search in db
                searched = custom_crs.findMatchingProj()
                if searched:
                    crs = QgsCoordinateReferenceSystem(searched, QgsCoordinateReferenceSystem.InternalCrsId)
                else:
                    # create custom and use it
                    custom_crs.saveAsUserCRS('quickmapservices %s' % layerDef.title)
                    searched = custom_crs.findMatchingProj()
                    if searched:
                        crs = QgsCoordinateReferenceSystem(searched, QgsCoordinateReferenceSystem.InternalCrsId)
                    else:
                        crs = custom_crs

            if crs:
                self.setCrs(crs)
        except:
            msg = self.tr("Custom crs can't be set for layer {0}!").format(layerDef.title)
            self.showBarMessage(msg, QgsMessageBar.WARNING, 4)

        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.setGrayscaleRender(LayerDefaultSettings.GRAYSCALE_RENDER)
        self.setBrigthness(LayerDefaultSettings.BRIGTNESS)
        self.setContrast(LayerDefaultSettings.CONTRAST)

        self.transparency = 1

        # downloader
        self.downloader = Downloader(self)
        self.downloader.userAgent = "Mozilla/5.0"
        self.downloader.default_cache_expiration = 24
        self.downloader.max_connection = 4
        self.downloader.replyFinished.connect(self.networkReplyFinished)

        #network
        self.downloadTimeout = 60000

        # multi-thread rendering
        self.eventLoop = None
        self.fetchRequest.connect(self.fetchRequestSlot)
        if iface:
            self.showMessage.connect(self.showStatusMessageSlot)
            self.showBarMessage.connect(self.showBarMessageSlot)
class TileLayer(QgsPluginLayer):

    CRS_3857 = QgsCoordinateReferenceSystem(3857)

    LAYER_TYPE = "PyTiledLayer"
    MAX_TILE_COUNT = 256
    CHANGE_SCALE_VALUE = 0.30

    allRepliesFinished = pyqtSignal()
    fetchRequest = pyqtSignal(list)
    showMessage = pyqtSignal(str, int)

    def __init__(self, layerDef, creditVisibility=1):
        QgsPluginLayer.__init__(self, TileLayer.LAYER_TYPE, layerDef.title)
        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)

        # set standard/custom crs
        self.setCrs(self.CRS_3857)
        try:
            crs = None
            if layerDef.epsg_crs_id is not None:
                crs = QgsCoordinateReferenceSystem(layerDef.epsg_crs_id, QgsCoordinateReferenceSystem.EpsgCrsId)
            if layerDef.postgis_crs_id is not None:
                crs = QgsCoordinateReferenceSystem(layerDef.postgis_crs_id, QgsCoordinateReferenceSystem.PostgisCrsId)
            if layerDef.custom_proj is not None:
                # create form proj4 str
                custom_crs = QgsCoordinateReferenceSystem()
                custom_crs.createFromProj4(layerDef.custom_proj)
                # try to search in db
                searched = custom_crs.findMatchingProj()
                if searched:
                    crs = QgsCoordinateReferenceSystem(searched, QgsCoordinateReferenceSystem.InternalCrsId)
                else:
                    # create custom and use it
                    custom_crs.saveAsUserCRS('quickmapservices %s' % layerDef.title)
                    searched = custom_crs.findMatchingProj()
                    if searched:
                        crs = QgsCoordinateReferenceSystem(searched, QgsCoordinateReferenceSystem.InternalCrsId)
                    else:
                        crs = custom_crs

            if crs:
                self.setCrs(crs)
        except:
            msg = self.tr("Custom crs can't be set for layer {0}!").format(layerDef.title)
            self.showBarMessage(msg, QgsMessageBar.WARNING, 4)

        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.setGrayscaleRender(LayerDefaultSettings.GRAYSCALE_RENDER)
        self.setBrigthness(LayerDefaultSettings.BRIGTNESS)
        self.setContrast(LayerDefaultSettings.CONTRAST)

        self.transparency = 1

        # downloader
        self.downloader = Downloader(self)
        self.downloader.userAgent = "Mozilla/5.0"
        self.downloader.default_cache_expiration = 24
        self.downloader.max_connection = 4
        self.downloader.replyFinished.connect(self.networkReplyFinished)

        #network
        self.downloadTimeout = 60000

        # multi-thread rendering
        self.eventLoop = None
        self.fetchRequest.connect(self.fetchRequestSlot)
        if iface:
            self.showMessage.connect(self.showStatusMessageSlot)
            self.showBarMessage.connect(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 setGrayscaleRender(self, isGrayscale):
        self.grayscaleRender = isGrayscale
        self.setCustomProperty("grayscaleRender", 1 if isGrayscale else 0)

    def setBrigthness(self, brigthness):
        self.brigthness = brigthness
        self.setCustomProperty("brigthness", brigthness)

    def setContrast(self, contrast):
        self.contrast = contrast
        self.setCustomProperty("contrast", contrast)


    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 = 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:
            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

        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 list(files.keys()):
                    self.tiles.setImageData(url, files[url])

                if 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 not renderContext.coordinateTransform():
                # 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)

        # 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()
        if self.grayscaleRender:
            QgsImageOperation.convertToGrayscale(image)
        if self.brigthness != LayerDefaultSettings.BRIGTNESS or self.contrast != LayerDefaultSettings.CONTRAST:
            QgsImageOperation.adjustBrightnessContrast(image, self.brigthness, self.contrast)

        # 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()
        if self.grayscaleRender:
            QgsImageOperation.convertToGrayscale(image)
        if self.brigthness != LayerDefaultSettings.BRIGTNESS or self.contrast != LayerDefaultSettings.CONTRAST:
            QgsImageOperation.adjustBrightnessContrast(image, self.brigthness, self.contrast)

        # 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 list(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 = 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" % 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 = 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 = iface.mapCanvas().mapSettings()
        return mapSettings.destinationCrs().postgisSrid() == 3857

    def networkReplyFinished(self, url, error, isFromCache):
        if iface is None or isFromCache:
            return
        unfinishedCount = self.downloader.unfinishedCount()
        if unfinishedCount == 0:
            self.allRepliesFinished.emit()

        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))
        self.setGrayscaleRender(int(self.customProperty("grascaleRender", LayerDefaultSettings.GRAYSCALE_RENDER)))
        self.setBrigthness(int(self.customProperty("brigthness", LayerDefaultSettings.BRIGTNESS)))
        self.setContrast(float(self.customProperty("contrast", LayerDefaultSettings.CONTRAST)))

        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
        self.allRepliesFinished.connect(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.fetchRequest.emit(urls)

        # wait for the fetch to finish
        tick = 0
        interval = 500
        timeoutTick = self.downloadTimeout / 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)  #
        self.allRepliesFinished.disconnect(eventLoop.quit)

        self.logT("TileLayer.fetchFiles() ends")
        return files

    def fetchRequestSlot(self, urls):
        self.logT("TileLayer.fetchRequest()")
        self.downloader.fetchFilesAsync(urls, self.downloadTimeout)

    def showStatusMessage(self, msg, timeout=0):
        self.showMessage.emit(msg, timeout)

    def showStatusMessageSlot(self, msg, timeout):
        iface.mainWindow().statusBar().showMessage(msg, timeout)

    def showBarMessage(self, text, level=QgsMessageBar.INFO, duration=0, title=None):
        pass

    def showBarMessageSlot(self, title, text, level, duration):
        iface.messageBar().pushMessage(title, text, level, duration)