/// <summary>
        /// Renders the layer
        /// </summary>
        /// <param name="g">Graphics object reference</param>
        /// <param name="map">Map which is rendered</param>
        public override void OnRender(System.Drawing.Graphics g, IMap map)
        {
            MapTransform mapTransform = new MapTransform(new PointF((float)Map.Center.X, (float)Map.Center.Y), (float)Map.PixelSize, Map.Image.Width, Map.Image.Height);

            string level = BruTile.Utilities.GetNearestLevel(Schema.Resolutions, Map.PixelSize);

            IEnumerable <TileInfo> tileInfos = Schema.GetTileInfos(mapTransform.Extent, level);

            var graphics = Graphics.FromImage(Image);

            //log.DebugFormat("Downloading tiles:");
            foreach (var tileInfo in tileInfos)
            {
                var bytes = cache.Find(tileInfo.Index);

                if (bytes == default(byte[]))
                {
                    try
                    {
                        //log.DebugFormat("row: {0}, column: {1}, level: {2}", tileInfo.Index.Row, tileInfo.Index.Col, tileInfo.Index.Level);
                        bytes = TileSource.GetTile(tileInfo);
                        cache.Add(tileInfo.Index, bytes);
                    }
                    catch (WebException e)
                    {
                        //log once per 45 seconds max
                        if ((DateTime.Now - lastErrorLogged) > TimeSpan.FromSeconds(45))
                        {
                            log.Error("Can't fetch tiles from the server", e);
                            lastErrorLogged = DateTime.Now;
                        }
                    }
                }
                else
                {
                    //log.DebugFormat("row: {0}, column: {1}, level: {2} (cached)", tileInfo.Index.Row, tileInfo.Index.Col, tileInfo.Index.Level);
                }

                if (bytes == null)
                {
                    continue;
                }
                using (var bitmap = new Bitmap(new MemoryStream(bytes)))
                {
                    var rectangle = mapTransform.WorldToMap(tileInfo.Extent.MinX, tileInfo.Extent.MinY,
                                                            tileInfo.Extent.MaxX, tileInfo.Extent.MaxY);
                    DrawTile(Schema, level, graphics, bitmap, rectangle);
                }
            }
        }
        private IList <IFeature> GetFeaturesInTile(TileInfo tileInfo)
        {
            if (TileSource == null)
            {
                return(null);
            }

            var features = _cache?.Find(tileInfo.Index);

            // Is tile in cache?
            if (features == null)
            {
                // No, tile not in cache, so get tile data new

                // Calc tile offset relative to upper left corner
                double factor = (tileInfo.Extent.MaxX - tileInfo.Extent.MinX) / 4096.0;

                var tileData = TileSource.GetTile(tileInfo);

                if (tileData == null)
                {
                    // Tile isn't in this source, so construct one from lower zoom level
                    var maxZoom = int.Parse(BruTile.Utilities.GetNearestLevel(TileSource.Schema.Resolutions, TileSource.Schema.Resolutions[(TileSource.Schema.Resolutions.Count - 1).ToString()].UnitsPerPixel));
                    // Get bounds of this tile
                    var bounds = new BoundingBox(new Point(tileInfo.Extent.MinX, tileInfo.Extent.MinY), new Point(tileInfo.Extent.MaxX, tileInfo.Extent.MaxY));
                    // Calc new TileInfo
                    var zoom = int.Parse(tileInfo.Index.Level);

                    // If zoom is ok, than there are no tile informations for this tile
                    if (zoom <= maxZoom)
                    {
                        return(null);
                    }

                    var tileX = tileInfo.Index.Col;
                    var tileY = tileInfo.Index.Row;

                    while (zoom > maxZoom)
                    {
                        zoom--;
                        tileX = tileX >> 1;
                        tileY = tileY >> 1;
                    }
                    var newTileInfo = new TileInfo {
                        Index = new TileIndex(tileX, tileY, zoom.ToString())
                    };
                    // Now get features for this overview tile
                    var newFeatures = GetFeaturesInTile(newTileInfo);
                    // Extract all features, which belong to small tile
                    features = new List <IFeature>();
                    foreach (var feature in newFeatures)
                    {
                        if (feature is VectorTileFeature vft)
                        {
                            if (vft.Bounds.Intersects(bounds))
                            {
                                features.Add(vft);
                            }
                        }
                    }

                    // Save for later use
                    if (_cache != null)
                    {
                        _cache.Add(tileInfo.Index, features);
                    }
                }

                if (features == null && tileData != null)
                {
                    // Parse tile and convert it to a feature list
                    Stream stream = new MemoryStream(tileData);

                    if (IsGZipped(stream))
                    {
                        stream = new GZipStream(stream, CompressionMode.Decompress);
                    }

                    features = VectorTileParser.Parse(tileInfo, stream);

                    // Save for later use
                    if (_cache != null && features.Count > 0)
                    {
                        _cache.Add(tileInfo.Index, features);
                    }

                    stream = null;
                }
            }

            System.Diagnostics.Debug.WriteLine($"Cached Tile Level={tileInfo.Index.Level}, Col={tileInfo.Index.Col}, Row={tileInfo.Index.Row}");

            return(features);
        }