def generate_layer_definition(self):
        try:
            service = QSettings().value("/IdahoLayerPlugin/service", "", type=str).strip('/')
            image_id = self.lineEdit_ImageId.text().strip()
            bucket_name = self.lineEdit_Bucket.text().strip()

            username, password, api_key, max_items_to_return = SettingsOps.get_settings()

            query = OAuth2Query.OAuth2Query(username, password, api_key)
            query.log_in()
            if not query.is_login_successful:
                QgsMessageLog.instance().logMessage("Login unsuccessful, verify your account settings.", TAG_NAME, level=QgsMessageLog.CRITICAL)
                raise Exception("Login unsuccessful, verify your account settings.")

            token = query.headers[OAuth2Query.HEADER_AUTHORIZATION].split(' ')[-1]

            props = self.getPropsFromID(service, bucket_name, image_id, token)

            _minlevel = props["levels"][0]
            _maxlevel = props["levels"][-1]
            bbox = BoundingBox(props["extents"][0], props["extents"][1], props["extents"][2], props["extents"][3])

            self.layerdef = IdahoLayerDefinition(service, image_id, bucket_name, _minlevel, _maxlevel, bbox, 3857)
            # set parameters
            self.layerdef.parameters.colorinterpretation = props["color_interp"]
            self.layerdef.parameters.format = props["mimetype"].split('/')[-1]
            self.layerdef.parameters.token = str(token)
            self.layerdef.update()
            self.accept()

        except Exception as e:
            self.plugin.iface.messageBar().pushMessage(self.tr("IdahoLayerPlugin"),
                                                       self.tr("{0}").format(unicode(e)),
                                                       QgsMessageBar.WARNING, 5)
    def readXml(self, node):
        self.readCustomProperties(node)

        service_url = self.customProperty("service_url", "")
        imageid = self.customProperty("imageid", "")
        bucket_name = self.customProperty("bucket_name", "")
        zmin = int(self.customProperty("zmin", TileDefaultSettings.ZMIN))
        zmax = int(self.customProperty("zmax", TileDefaultSettings.ZMAX))
        bbox = BoundingBox.fromString(self.customProperty("bbox", ""))
        epsg = int(self.customProperty("epsg", 3857))
        max_connections = int(self.customProperty("max_connections", 16))

        self.layerDef = IdahoLayerDefinition(service_url=service_url, imageid=imageid, bucket_name=bucket_name,
                                             zmin=zmin, zmax=zmax, bbox=bbox, epsg=epsg,
                                             max_connections=max_connections)

         # set parameters
        self.layerDef.parameters.colorinterpretation = self.customProperty("colorinterpretation", "UNSPECIFIED")
        self.layerDef.parameters.bands = self.customProperty("bands", [0])
        self.layerDef.parameters.format = self.customProperty("format", None)
        self.layerDef.parameters.token = self.customProperty("token", None)
        self.layerDef.parameters.doDRA = self.customProperty("doDRA", None)
        self.layerDef.parameters.lowCutoff = self.customProperty("lowCutoff", None)
        self.layerDef.parameters.highCutoff = self.customProperty("highCutoff", None)
        self.layerDef.update()

        self.tile_manager = TileManager(self, self.layerDef)

        self.setExtent(BoundingBox.asPseudoMercator(self.layerDef.bbox, self.layerDef.epsg).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)))


        return True
class AddLayerDialog(QDialog, FORM_CLASS):
    def __init__(self, plugin):
        QDialog.__init__(self, plugin.iface.mainWindow())
        self.plugin = plugin

        # set up the user interface
        self.setupUi(self)
        self.lineEdit_ImageId.textChanged.connect(self.verifyInput)
        self.lineEdit_Bucket.textChanged.connect(self.verifyInput)
        self.pushButton_Add.setEnabled(False)
        self.pushButton_Add.clicked.connect(self.generate_layer_definition)
        self.pushButton_Close.clicked.connect(self.reject)
        self.pushButton_Settings.clicked.connect(self.settingsClicked)
        self.pushButton_AccountSettings.clicked.connect(self.accountSettingsClicked)
        self.layerdef = None
        # downloader
        maxConnections = 12
        cacheExpiry = QSettings().value("/qgis/defaultTileExpiry", 24, type=int)
        userAgent = "QGIS/{0} IdahoLayerPlugin/{1}".format(QGis.QGIS_VERSION, "0.02")

        settings = QSettings()
        self.lineEdit_Bucket.setText(settings.value("/IdahoLayerPlugin/bucket", "", type=str))
        self.lineEdit_ImageId.setText(settings.value("/IdahoLayerPlugin/imageid", "", type=str))
        self.downloader = Downloader(None, maxConnections, cacheExpiry, userAgent)


    def settingsClicked(self):
        if self.plugin.settings():
            i = 0

    def accountSettingsClicked(self):
        SettingsTool.SettingsTool(self.plugin.iface)

    def verifyInput(self, val):
        image_id = self.lineEdit_ImageId.text().strip()
        bucket_name = self.lineEdit_Bucket.text().strip()

        #todo: fixed length check
        if len(image_id) == 36 and len(bucket_name) >1:
            self.pushButton_Add.setEnabled(True)
        else:
            self.pushButton_Add.setEnabled(False)

    def generate_layer_definition(self):
        try:
            service = QSettings().value("/IdahoLayerPlugin/service", "", type=str).strip('/')
            image_id = self.lineEdit_ImageId.text().strip()
            bucket_name = self.lineEdit_Bucket.text().strip()

            username, password, api_key, max_items_to_return = SettingsOps.get_settings()

            query = OAuth2Query.OAuth2Query(username, password, api_key)
            query.log_in()
            if not query.is_login_successful:
                QgsMessageLog.instance().logMessage("Login unsuccessful, verify your account settings.", TAG_NAME, level=QgsMessageLog.CRITICAL)
                raise Exception("Login unsuccessful, verify your account settings.")

            token = query.headers[OAuth2Query.HEADER_AUTHORIZATION].split(' ')[-1]

            props = self.getPropsFromID(service, bucket_name, image_id, token)

            _minlevel = props["levels"][0]
            _maxlevel = props["levels"][-1]
            bbox = BoundingBox(props["extents"][0], props["extents"][1], props["extents"][2], props["extents"][3])

            self.layerdef = IdahoLayerDefinition(service, image_id, bucket_name, _minlevel, _maxlevel, bbox, 3857)
            # set parameters
            self.layerdef.parameters.colorinterpretation = props["color_interp"]
            self.layerdef.parameters.format = props["mimetype"].split('/')[-1]
            self.layerdef.parameters.token = str(token)
            self.layerdef.update()
            self.accept()

        except Exception as e:
            self.plugin.iface.messageBar().pushMessage(self.tr("IdahoLayerPlugin"),
                                                       self.tr("{0}").format(unicode(e)),
                                                       QgsMessageBar.WARNING, 5)

    def getPropsFromID(self, input_url, bucket_name, idaho_id, token):

        # get the metadata from the server
        url = "{_url}/v1/metadata/{_bucket_name}/{_idaho_id}".format(_url=input_url, _bucket_name=bucket_name, _idaho_id=idaho_id)

        # get the metadata from the service
        req = urllib2.Request(url)
        opener = urllib2.build_opener()
        opener.addheaders = [('Authorization', 'Bearer '+token), ('Content-Type', 'application/json')]
        f = opener.open(req)
        metadata = json.loads(f.read())

         # get the metadata from the server
        url = "{_url}/v1/metadata/{_bucket_name}/{_idaho_id}/image.json".format(_url=input_url, _bucket_name=bucket_name, _idaho_id=idaho_id)

        # get the metadata from the service
        req = urllib2.Request(url)
        opener = urllib2.build_opener()
        opener.addheaders = [('Authorization', 'Bearer '+token), ('Content-Type', 'application/json')]
        f = opener.open(req)
        metadata_image = json.loads(f.read())

        # get a list of the avaible z levels
        levels = metadata.get('tileSets')
        levels = [int(i) for i in levels]
        levels = sorted(levels)

        src = QgsCoordinateReferenceSystem(int(4326), QgsCoordinateReferenceSystem.PostgisCrsId)
        dest = QgsCoordinateReferenceSystem(3857, QgsCoordinateReferenceSystem.PostgisCrsId)
        xfrm = QgsCoordinateTransform(src, dest)

        minX = min(metadata.get('lowerLeftLongitude'),metadata.get('upperRightLongitude'))
        maxX = max(metadata.get('lowerLeftLongitude'),metadata.get('upperRightLongitude'))
        minY = min(metadata.get('lowerLeftLatitude'),metadata.get('upperRightLatitude'))
        maxY = max(metadata.get('lowerLeftLatitude'),metadata.get('upperRightLatitude'))

        rect = QgsRectangle(minX, minY, maxX, maxY)
        dr = xfrm.transform(rect)
        minX = dr.xMinimum()
        maxX = dr.xMaximum()
        minY = dr.yMinimum()
        maxY = dr.yMaximum()

        return {"extents": (minX, minY, maxX, maxY), "levels": levels, "mimetype":  metadata["defaultTileFormat"], "color_interp": metadata_image["colorInterpretation"]}

    def band_combination_string_from_color_interp(self, colorinterp):
        bandcombo = "0"
        try:
            idx = next(index for (index, d) in enumerate(self.colorinterps) if d["name"] == colorinterp)
            bandcombo = self.colorinterps[idx]["value"]
        except Exception as e:
            bandcombo = "0"
        return bandcombo


    def accept(self):
        QDialog.accept(self)

        # save dialog prop
        settings = QSettings()
        settings.setValue("/IdahoLayerPlugin/bucket", self.lineEdit_Bucket.text().strip())
        settings.setValue("/IdahoLayerPlugin/imageid", self.lineEdit_ImageId.text().strip())
 def createLayer(self):
     return IdahoLayer(self.plugin, IdahoLayerDefinition.createEmptyInfo())
class IdahoLayer(QgsPluginLayer):
    LAYER_TYPE = "IdahoLayer"
    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):
        QgsPluginLayer.__init__(self, IdahoLayer.LAYER_TYPE, layerDef.imageid)

        self.plugin = plugin
        self.iface = plugin.iface
        self.layerDef = layerDef
        self.tile_manager = TileManager(self, layerDef)
        self.zoom = 0
        self.tile_range = None

        # set custom properties
        self.setCustomProperty("service_url", layerDef.service_url)
        self.setCustomProperty("bucket_name", layerDef.bucket_name)
        self.setCustomProperty("imageid", layerDef.imageid)
        self.setCustomProperty("zmin", layerDef.zmin)
        self.setCustomProperty("zmax", layerDef.zmax)
        self.setCustomProperty("bbox", layerDef.bbox.toString())
        self.setCustomProperty("epsg", layerDef.epsg)
        self.setCustomProperty("max_connections", layerDef.max_connections)

        # set crs default to Pseudo Mercator
        if plugin.crs3857 is None:
            # create a QgsCoordinateReferenceSystem instance if plugin has no instance yet
            plugin.crs3857 = QgsCoordinateReferenceSystem(3857)

        self.setCrs(plugin.crs3857)

        # set QGIS extent as QgsRectangle
        if not layerDef.epsg:
            layerDef.epsg = 4326
        if layerDef.epsg == 3857 or layerDef.epsg == 900913:
            self.setExtent(layerDef.bbox.toQgsRectangle())
        else:
            self.setExtent(BoundingBox.asPseudoMercator(layerDef.bbox, layerDef.epsg).toQgsRectangle())

        # set styles
        self.setTransparency(0)
        self.setBrightness(0)
        self.setContrast(1.0)
        self.setSaturation(1.0)
        self.setBlendModeByName(self.DEFAULT_BLEND_MODE)
        self.setSmoothRender(self.DEFAULT_SMOOTH_RENDER)


        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 setBrightness(self, brightness):
        self.brightness = brightness
        self.setCustomProperty("brightness", brightness)

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

    def setSaturation(self, saturation):
        self.saturation = saturation
        self.setCustomProperty("saturation", saturation)

    def setSmoothRender(self, isSmooth):
        self.smoothRender = isSmooth
        self.setCustomProperty("smoothRender", 1 if isSmooth else 0)

    def calculate_tile_range(self, extent):

        size = self.layerDef.PSEUDO_MERCATOR_MAX_COORD / 2 ** (self.zoom - 1)
        matrixSize = 2 ** self.zoom
        ulx = max(0, int((extent.xMinimum() + self.layerDef.PSEUDO_MERCATOR_MAX_COORD) / size))
        uly = max(0, int((self.layerDef.PSEUDO_MERCATOR_MAX_COORD - extent.yMaximum()) / size))
        lrx = min(int((extent.xMaximum() + self.layerDef.PSEUDO_MERCATOR_MAX_COORD) / size), matrixSize - 1)
        lry = min(int((self.layerDef.PSEUDO_MERCATOR_MAX_COORD - extent.yMinimum()) / size), matrixSize - 1)

        # bounding box limit
        if self.layerDef.bbox:
            valid = True
            trange = None
            if not self.layerDef.epsg:
                self.layerDef.epsg = 4326
            elif self.layerDef.epsg == 3857 or self.layerDef.epsg == 900913:
                trange = self.layerDef.bboxMercatorToTileRange(self.zoom, self.layerDef.bbox)
            else:
                trange = self.layerDef.epsgToTileRange(self.zoom, self.layerDef.bbox)
            if trange:
                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
                    valid = False

        self.tile_range = TileRange(ulx=ulx, uly=uly, lrx=lrx, lry=lry, valid=valid)


    def draw(self, renderContext):
        self.renderContext = renderContext
        painter = renderContext.painter()
        viewport = painter.viewport()

        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() if self.plugin.apiChanged27 else 0.0

        mpp = mupp  # meters per pixel
        if rotation != 0.0:
            # 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.PSEUDO_MERCATOR_MAX_COORD / self.layerDef.TILE_SIZE
        self.zoom = int(math.ceil(math.log(tile_mpp1 / mpp, 2) + 1))
        self.zoom = max(0, min(self.zoom, self.layerDef.zmax))

        #if current zoom is not in layer range then bail
        if not self.zoom_is_in_range():
            return True

        self.calculate_tile_range(extent)

        #if tile range is not good
        if not self.tile_range.valid:
            return True

        if not self.tilecount_is_in_range():
            return True

        self.tile_manager.update(self.zoom, self.tile_range)
        tiles = self.tile_manager.get_tiles()

        # apply layer style
        # save painter state
        painter.save()
        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 rotation == 0.0:
            # no need to reproject tiles
            self.render_tiles(renderContext, tiles)

        else:
            # reproject tiles
            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)
            self.render_tiles_warp_on_the_fly(renderContext, mapExtent, tiles)

        # restore layer style
        painter.setOpacity(oldOpacity)
        if self.smoothRender:
            painter.setRenderHint(QPainter.SmoothPixmapTransform, oldSmoothRenderHint)

        # restore painter state
        painter.restore()

        return True

    def render_tiles(self, renderContext, tiles, sdx=1.0, sdy=1.0):

        # create an compose_image that has the same resolution as the tiles
        image = tiles.compose_image()

        QgsImageOperation.adjustBrightnessContrast(image, self.brightness, self.contrast)
        QgsImageOperation.adjustHueSaturation(image, self.saturation)

        # 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 compose_image on the map canvas
        renderContext.painter().drawImage(rect, image)

        self.log("TilesCollection extent: " + str(extent))
        self.log("Draw into canvas rect: " + str(rect))

    def render_tiles_warp_on_the_fly(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 compose_image from the tiles
        image = tiles.compose_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 compose_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 compose_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 compose_image on the map canvas
        rect = QRectF(QPointF(0, 0), QPointF(viewport.width() * sdx, viewport.height() * sdy))
        painter.drawImage(rect, reprojected_image)

    def drawDebugInfo(self, renderContext, zoom, ulx, uly, lrx, lry):
        painter = renderContext.painter()
        scaleX, scaleY = self.getScaleToVisibleExtent(renderContext)
        painter.scale(scaleX, scaleY)

        self.drawFrames(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY)
        self.drawNumbers(renderContext, zoom, ulx, uly, lrx, lry, 1.0 / scaleX, 1.0 / scaleY)

    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()
        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 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 zoom_is_in_range(self):
        # zoom limit
        if self.zoom < self.layerDef.zmin:
            if self.plugin.navigationMessagesEnabled:  # or zoom > self.layerDef.zmax is not necessary because it will use the last zoom level when zooming in
                msg = None
                tpl = "Zoom {_direction} to see {_layer}. Zoom levels {_min}-{_max} in layer. Currently at zoom level {_cur}."
                if self.zoom < self.layerDef.zmin:
                    msg = self.tr(tpl).format(_min=self.layerDef.zmin, _max=self.layerDef.zmax, _cur=self.zoom,
                                              _layer=self.layerDef.imageid, _direction="in")
                if msg:
                    self.showMessageBar(msg, QgsMessageBar.INFO, 2)
            return False
        return True

    #TODO: check this logic... seems a little off
    def tilecount_is_in_range(self):
        # tile count limit
        tileCount = (self.tile_range.lrx - self.tile_range.ulx + 1) * (self.tile_range.lry - self.tile_range.uly + 1)
        if tileCount > self.MAX_TILE_COUNT:
            # if the zoom level is less than the minimum, do not draw
            if self.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 False
        return True

    def readXml(self, node):
        self.readCustomProperties(node)

        service_url = self.customProperty("service_url", "")
        imageid = self.customProperty("imageid", "")
        bucket_name = self.customProperty("bucket_name", "")
        zmin = int(self.customProperty("zmin", TileDefaultSettings.ZMIN))
        zmax = int(self.customProperty("zmax", TileDefaultSettings.ZMAX))
        bbox = BoundingBox.fromString(self.customProperty("bbox", ""))
        epsg = int(self.customProperty("epsg", 3857))
        max_connections = int(self.customProperty("max_connections", 16))

        self.layerDef = IdahoLayerDefinition(service_url=service_url, imageid=imageid, bucket_name=bucket_name,
                                             zmin=zmin, zmax=zmax, bbox=bbox, epsg=epsg,
                                             max_connections=max_connections)

         # set parameters
        self.layerDef.parameters.colorinterpretation = self.customProperty("colorinterpretation", "UNSPECIFIED")
        self.layerDef.parameters.bands = self.customProperty("bands", [0])
        self.layerDef.parameters.format = self.customProperty("format", None)
        self.layerDef.parameters.token = self.customProperty("token", None)
        self.layerDef.parameters.doDRA = self.customProperty("doDRA", None)
        self.layerDef.parameters.lowCutoff = self.customProperty("lowCutoff", None)
        self.layerDef.parameters.highCutoff = self.customProperty("highCutoff", None)
        self.layerDef.update()

        self.tile_manager = TileManager(self, self.layerDef)

        self.setExtent(BoundingBox.asPseudoMercator(self.layerDef.bbox, self.layerDef.epsg).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)))


        return True

    def writeXml(self, node, doc):
        self.setCustomProperty("colorinterpretation", self.layerDef.parameters.colorinterpretation)
        self.setCustomProperty("bands", self.layerDef.parameters.bands)
        self.setCustomProperty("format", self.layerDef.parameters.format)
        self.setCustomProperty("token", self.layerDef.parameters.token)
        self.setCustomProperty("doDRA", self.layerDef.parameters.doDRA)
        self.setCustomProperty("lowCutoff", self.layerDef.parameters.lowCutoff)
        self.setCustomProperty("highCutoff", self.layerDef.parameters.highCutoff)
        self.writeCustomProperties(node, doc)
        element = node.toElement();
        element.setAttribute("type", "plugin")
        element.setAttribute("name", IdahoLayer.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("Image ID"), self.layerDef.imageid))

        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 showStatusMessage(self, msg, timeout=0):
        self.statusSignal.emit(msg, timeout)

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

    def showMessageBar(self, text, level=QgsMessageBar.INFO, duration=0, imageid=None):
        if imageid is None:
            imageid = self.plugin.pluginName
        self.messageBarSignal.emit(imageid, text, level, duration)

    def showMessageBarSlot(self, imageid, text, level, duration):
        self.iface.messageBar().pushMessage(imageid, 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))