ThinkGeo.com    |     Blog    |     Wiki    |     Support

Deleting tiles from FileBitmapCache

When our application starts up it receives layer updates from the server. What we want to have happen is to delete the tiles associated with the layer updates, before anything is drawn to the screen. When we go to delete the tiles form the cache nothing actually gets deleted. If I delay the delete tile operation until after the overlay is refreshed then the tiles do get deleted.

So this appears to be a case where deleting tiles from the cache will not work until after the overlay is drawn. Is there any way to get this to delete the tiles without first drawing the overlay?

By the way clearing the cache always works regardless of whether the overlay is drawn or not, but is not a solution for us.

Hi Richard,

Thanks for your question, could you please correct me if I have any misunderstanding here? There is an application with map loaded, the map consumes a remote service to get tile images as background map, after the application is launched, sometimes, it receives a signal from service that the data on server side is updated, now client side application re-request the tile image from service to update the map, but seems like the old tile cache on server side is not deleted until the new tile is drawn and sent to client side application. If i’m incorrect, could you please help me clarify? If yes, could you please help on the followings?

a. What Overlay or Layer is using in the application? Could you please provide any codes or a demo here? So that I can dig into further.
b. The only difference between method “DeleteTile(Tile tile)” and “ClearCache(*)” is the previous only calculates one tile based on the “Center Point” of “input tile boundingbox” to delete the specific tile. Here is the pseudo-code of method “DelteTile”, please check if it can give some hints:

    TileMatrixCell cell = this.TileMatrix.GetCell(tile.BoundingBox.GetCenterPoint());
    string tileImageFileName = GetTileImageFileName(cell.Row, cell.Column, tile.Scale);
    if (File.Exists(tileImageFileName))
    {
        File.Delete(tileImageFileName);
    }

While “ClearCache” is getting all the cached files under “Cache Directory”, and then call “File.Delete” one by one. According to this, I was thinking that maybe 2 reasons why the expected tile is not deleted:

  1. The tileImageFileName is not the one we want to delete, maybe something wrong with the input parameter “Tile”, scale, row or column is possible, especially for scale.
  2. the deleting tile cache image doesn’t exist when calling “DeleteTile”.

Could you please check the above 2 possible reasons?

Regards,
Johnny

SQLite data source on the client. The slite datasource gets an update from a server – client knows what gets updated and generates range blocks for the updated features. The client has an overlay that has an associated FileBitmapCache.

Tile image gets generated when the map is drawn on the local client. The client uses the deleteTile that takes a rectangleShape to remove tiles.

pseudo code:

internal static List modifiedAreas = GenerateListofUpdatedFeatures();

                        cachedFeatureOverlay = new LayerOverlay();
                        mapOverlays.Add(cachedFeatureOverlay);
                        var bitmapTileCache = new FileBitmapTileCache();
                        string cachePath = System.IO.Path.Combine(ConfigPath, CachedTiles");
                        if (!Directory.Exists(cachePath))
                        {
                            Directory.CreateDirectory(cachePath);
                        }
                        bitmapTileCache.CacheDirectory = cachePath;
                        bitmapTileCache.CacheId = "LinearCachedTiles";
                        bitmapTileCache.ImageFormat = TileImageFormat.Png;
                        cachedFeatureOverlay.TileCache = bitmapTileCache;

cachedFeatureOverlay.TileCache.DeleteTiles(modifedAreas);

Result is the local tile cache does not get updated. I do have a work around that I am not particularly comfortable with, but works. Its based on above pseudocode. Only difference is that I call refresh and once complete I then delete the tiles. So this is why I believe refresh is required to initialize something for the deleteTiles(List) function.

internal static List modifiedAreas = GenerateListofUpdatedFeatures();

                        cachedFeatureOverlay = new LayerOverlay();
                        mapOverlays.Add(cachedFeatureOverlay);
                        var bitmapTileCache = new FileBitmapTileCache();
                        string cachePath = System.IO.Path.Combine(ConfigPath, "CachedTiles");
                        if (!Directory.Exists(cachePath))
                        {
                            Directory.CreateDirectory(cachePath);
                        }
                        bitmapTileCache.CacheDirectory = cachePath;
                        bitmapTileCache.CacheId = "LinearCachedTiles";
                        bitmapTileCache.ImageFormat = TileImageFormat.Png;
                        cachedFeatureOverlay.TileCache = bitmapTileCache;
            cachedFeatureOverlay.Drawn += CachedFeatureOverlay_Drawn;
            cachedFeatureOverlay.Refresh();

    private static void CachedFeatureOverlay_Drawn(object sender, DrawnOverlayEventArgs e)
    {
            cachedFeatureOverlay.Drawn -= CachedFeatureOverlay_Drawn;
            foreach( var item in modifedAreas )
            {
                cachedFeatureOverlay.TileCache.DeleteTiles(item);                
           }
    }

Just checked our android version and it looks like it has the same issue. Unfortunately the work around for WPF can’t be done on Android.

Scratch the workaround – it sort of works, unfortunately main screen update can occur prior to clearing out the unwanted tiles. Either way I still need to get something working for both Android and WPF ( probably iOS as well, but haven’t messed with that platform in a while.)

Hi Richard,

The workaround is the right way that should go, just a minor change. The LayerOverlay cannot update itself when the FeatureSource of the layers hosted in LayerOverlay is updated, until redraw the tiles of the overlay by calling Refresh or panning/zooming in/out the map. Here is the pseudo code in “Overlay.Refresh()” method:

    protected virtual void RefreshCore()
    {
        Draw(map.CurrentExtent, OverlayRefreshType.Redraw);
    }

    public void Draw(RectangleShape targetExtent, OverlayRefreshType refreshType)
    {
        if (!IsVisible && refreshType != OverlayRefreshType.Pan) { return; }

        DrawingOverlayEventArgs drawingArgs = new DrawingOverlayEventArgs(targetExtent, false);
        OnDrawing(drawingArgs);
        if (drawingArgs.Cancel)
        {
            OnDrawn(new DrawnOverlayEventArgs(targetExtent));
            return;
        }

        DrawCore(targetExtent, refreshType);
    }

From the implementation of Refresh method, we can see only a small change of your workaround should work as you expected. Please use “Drawing” event, instead of “Drawn”, before “Drawing” even will be fired before the tile id redrawn, in other words, may is redrawn. In “Drawing” even, even we can “Cancel” the drawing, but only “DELETE” the cache.

Hope it helped.

Regards,
Johnny

I tried using the drawing event – cache files are not removed. Something apparently occurs during tile drawing that causes the cache to be initialized properly and the drawing event apparently occurs prior to that.

Even if the drawing event did work this is not an option for Android as those platforms don’t have event handlers on the overlays other than the exception related handlers. Android also has the issue with cache tiles not getting deleted before the overlay with the tilecache is drawn.

Hi Richard,

Besides using “Drawing” event, still have to call “Refresh” method to fire the “Drawing” event, if don’t want to redraw the tile, “Cancel” property can be assigned as “true”. Another possible event we can try is “DrawingTile”, but still have to call “Refresh” method.

In “Android Edition”, we may have to overwrite the following method to simulate “Drawing” event:

public class CustomLayerOverlay : LayerOverlay
{
    protected override void DrawTileCore(GeoCanvas geoCanvas, TileView tile)
    {
        // TODO: Here we can do something before drawing the tile, similar to the "Drawing" event.
        
        base.DrawTileCore(geoCanvas, tile);

        // TODO: Here  we can do somethign after darwing the tile, similar to "Drawn" event
    }
}

Please take a try if it works for you.

Thanks,
Johnny

I ended up going the override route for WPF using DrawTileCore. I was not able to delete tiles until after a call to base.DrawTileCore was made. For WPF I’ve got something that works well enough.

Android is a lot more problematic - Less functions available to override with regards to drawing. I tried overriding DrawCore – tiles did not get deleted when called from that location.

DrawTileCore --This doesn’t get called if their is already a cached file for the tile in question. I also tried hooking into getTileCore – still no luck there.

There must be some kind of “requirement” that has to be met before the DeleteTiles(RectangleShape) Knowing what that requirement is might help resolving this issue.

Hi Richard,

Thanks for your update, I get many information from your communication.

The reason why DeleteTiles don’t works is because its default scale point to zoom level 01, so it always try to delete the cached image from folder 590591790 by default. After map refresh, the scale get changed, it also change the value of overlay.TileCache.TileMatrix.Scale, so you can delete target images correct.

Based on this, you can see the solution to delete cache images is:

        double currentScale = ExtentHelper.GetSnappedScale(rect, (float)map.ActualWidth, map.MapUnit);
        cache.TileMatrix.Scale = currentScale;
        cache.DeleteTiles(rect);

You can put it in the map_Loaded event, it will delete target tiles in initial zoom level.

And you can delete other levels in other functions for example:

    map.CurrentScaleChanged += Map_CurrentScaleChanged;

    private void Map_CurrentScaleChanged(object sender, CurrentScaleChangedWpfMapEventArgs e)
    {
        if (thisLevelDeletedFlag)// You can set a flag here to avoid delete many times which will reduce performance 
        {
            LayerOverlay overlay = map.Overlays[0] as LayerOverlay;
            overlay.TileCache.TileMatrix.Scale = e.CurrentScale;
            overlay.TileCache.DeleteTiles(rect);
        }
    } 

You can also build a function to delete all levels or specified levels once:

    public void DeleteOldCache(FileBitmapTileCache cache, RectangleShape rect, ZoomLevelSet zoomLevelSet)
    {
        foreach (ZoomLevel level in zoomLevelSet.GetZoomLevels())
        {
            cache.TileMatrix.Scale = level.Scale;
            cache.DeleteTiles(rect);
        }
    }

But I don’t suggest you use it, because in certain scenario it’s very slow for deep levels even if they don’t have cached images.

For avoid that, you can use the other solution, loop the folder names (They are scale value) under your cache folder (For example “LinearCachedTiles”), then loop the names, set it to TileMatrix.Scale and delete the tiles in specified area.

And please notice, if the specified area is very big, clear cache is a better choice, clear cache will delete the cache folder without calculation.

I hope that’s able to help you.

Regards,

Ethan

At startup I still can’t get it to delete the tile cache even when settings the scale to the tilematrix. Basically using DeleteOldCache for the most part as is – I did change it to use the scale from the directories as you recommended. If the map is already displayed when the data is brought from the server then it works just fine, but at startup prior to any refresh operation being called it will not remove the files from the cache. I don’t see a map_loaded event in WPF or Android, and I tried to clean up the cache during the 1st call CurrentScaleChanged which did not work either.

This was all tested in WPF – I did not try Android at this point for this change.

Hi Richard,

In fact CurrentScaleChanged will be fire when you set current extent the first time, so you should want subscribe it before that. You can subscribe to the CurrentScaleChanged event in constructor or in XAML page.

Here is a sample about it, you can modify the bool value in line 37 to make sure whether it works.

9720.zip (101.9 KB)

image

image

For android, you can put the logic in OnCreate function, still put it before set map1.CurrentExtent.

Any question please feel free to let us know.

Regards,

Ethan

I really should have paid more attention to the tileMatrix. My map was set to meters and the tilematrix is set to decimalDegrees by default. Everything works fine once I fixed that issue.

Hi Richard,

Thanks for your update.

I am glad to hear that works, any question please feel free to let us know.

Regards,

Ethan