Posts tagged ‘Tile layers’

July 21, 2011

Deleting Blank Tiles. In a Dangerous Way.

Creating raster tilesets almost invariably leads to the creation of some blank tiles – covering those areas of space where no features were present in the underlying dataset. Depending on the image format you use for your tiles, and the method you used to create them, those “blank” tiles may be pure white, or some other solid colour, or they may have an alpha channel set to be fully transparent.

Here’s an example of a directory of tiles I just created. In this particular z/x directory, more than half the tiles are blank. Windows explorer shows them as black but that’s because it doesn’t process the alpha channel correctly. They are actually all 256px x 256px PNG images, filled with ARGB (0, 0, 0, 0):

image

What to do with these tiles? Well, there’s two schools of thought:

  • The first is that they should be retained. They are, after all, valid image files that can be retrieved and overlaid on the map. Although they aren’t visually perceptible, the very presence of the file demonstrates that the dataset was tested at this location, and confirms that no features exist there. This provides important metadata about the dataset in itself, and confirms the tile generation process was complete. The blank images themselves are generally small, and so storage is not generally an issue.
  • The opposing school of thought is that they should be deleted. It makes no sense to keep multiple copies of exactly the same, blank tile. If a request is received for a tile that is not present in the dataset, the assumption can be made that it contains no data, and a single, generic blank tile can be returned in all such instances – there is no benefit of returning the specific blank tile associated with that tile request. This not only reduces disk space on the tile server itself, but the client needs only cache a single blank tile that can be re-used in all cases where no data is present.

I can see arguments in favour of both sides. But, for my current project, disk and cache space is at a premium, so I decided I wanted to delete any blank tiles from my dataset. To determine which files were blank, I initially thought of testing the filesize of the image. However, even though I knew that every tile was of a fixed dimension (256px x 256px), an empty tile can still vary in filesize according to the compression algorithm used. Then I thought I could loop through each pixel in the image and use GetPixel() to retrieve the data to see whether the entire image was the same colour, but it turns out that GetPixel() is slooooowwwww….

The best solution I’ve found is to use an unsafe method, BitMap.LockBits to provide direct access to the pixel byte data of the image, and then read and compare the byte values directly. In my case, my image tiles are 32bit PNG files, which use 4 bytes per pixel (BGRA), and my “blank” tiles are completely transparent (Alpha channel = 0). Therefore, in my case I used the following function, which returns true if all the pixels in the image are completely transparent, or false otherwise:

public static Boolean IsEmpty(string imageFileName)
{
  using (Bitmap b = ReadFileAsImage(imageFileName))
  {
    System.Drawing.Imaging.BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, b.PixelFormat);
    unsafe
    {
      int PixelSize = 4; // Assume 4Bytes per pixel ARGB
      for (int y = 0; y < b.Height; y++)
      {
        byte* p = (byte*)bmData.Scan0 + (y * bmData.Stride);
        for (int x = 0; x < b.Width; x++)
        {
          byte blue = p[x * PixelSize]; // Blue value. Just in case needed later
          byte green = p[x * PixelSize + 1]; // Green. Ditto
          byte red = p[x * PixelSize + 2]; // Red. Ditto
          byte alpha = p[x * PixelSize + 3];
          if (alpha > 0) return false;
        }
      }
    }
    b.UnlockBits(bmData);
  }

  return true;
}

It needs to be compiled with the /unsafe option (well, it did say in the title that this post was dangerous!). Then, I just walked through the directory structure of my tile images, passing each file into this function and deleting those where IsEmpty() returned true. And voila! – no more blank tiles.

January 7, 2011

Accessing a WMS Tile Server from Bing Maps Silverlight

In my previous post, I described how to add a tile layer from a WMS server using the Bing Maps v7 AJAX control. You can use a similar logic to add a WMS server as a tile source for the Silverlight control, with a few differences:

When using the Bing Maps AJAX control, the UriConstructor of the Microsoft.Maps.TileSource object uses a single {quadkey} argument to determine the tile image to be retrieved. (see http://msdn.microsoft.com/en-us/library/gg427599.aspx).

However, the equivalent GetUri() method of the Silverlight Microsoft.Maps.MapControl.TileSource object instead uses x, y, and zoomLevel parameters (http://msdn.microsoft.com/en-us/library/microsoft.maps.mapcontrol.tilesource.geturi.aspx).

Therefore, to convert from a Silverlight tile request to a WMS request, you can reuse much of the same logic as for the AJAX control, except omitting the intermediate step of calculating x, y, and zoom from the quadkey. The necessary conversion can be included directly in a tile source as follows:

public class WMSTileSource : Microsoft.Maps.MapControl.TileSource
 {
 public WMSTileSource()
 : base("http://wms1.ccgis.de/cgi-bin/mapserv?map=/data/umn/germany/germany.map&&VERSION=1.1.1&REQUEST=GetMap&SERVICE=WMS&SRS=EPSG%3A4326&BBOX={0}&WIDTH=256&HEIGHT=256&LAYERS=Bundeslaender")
 {}

 public override Uri GetUri(int x, int y, int zoomLevel)
 {
 return new Uri(String.Format(this.UriFormat, XYZoomToBBox(x, y, zoomLevel)));
 }

 public string XYZoomToBBox(int x, int y, int zoom)
 {
 int TILE_HEIGHT = 256, TILE_WIDTH = 256;
 // From the grid position and zoom, work out the min and max Latitude / Longitude values of this tile
 double W = (float)(x * TILE_WIDTH) * 360 / (float)(TILE_WIDTH * Math.Pow(2, zoom)) - 180;
 double N = (float)Math.Asin((Math.Exp((0.5 - (y * TILE_HEIGHT) / (TILE_HEIGHT) / Math.Pow(2, zoom)) * 4 * Math.PI) - 1) / (Math.Exp((0.5 - (y * TILE_HEIGHT) / 256 / Math.Pow(2, zoom)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;
 double E = (float)((x + 1) * TILE_WIDTH) * 360 / (float)(TILE_WIDTH * Math.Pow(2, zoom)) - 180;
 double S = (float)Math.Asin((Math.Exp((0.5 - ((y + 1) * TILE_HEIGHT) / (TILE_HEIGHT) / Math.Pow(2, zoom)) * 4 * Math.PI) - 1) / (Math.Exp((0.5 - ((y + 1) * TILE_HEIGHT) / 256 / Math.Pow(2, zoom)) * 4 * Math.PI) + 1)) * 180 / (float)Math.PI;
 string[] bounds = new string[] { W.ToString(), S.ToString(), E.ToString(), N.ToString() };
 // Return a comma-separated string of the bounding coordinates
 return string.Join(",", bounds);
 }
 }
  • the base of the class defines the url template for the WMS Server. I’m using a layer  that displays federal states of Germany. The {0} placeholder will be replaced with the bounding box coordinates calculated from the GetUri() method.
  • The XYZoomToBBox() method converts from Bing Maps’ native x, y, and zoom tile numbering to the W, S, E, N coordinates of a bounding box to be passed to the WMS server.
  • Since I’m doing this all within the .cs file itself, no intermediate handler is needed.

The tilelayer can be added to the map in the XAML declaration, as follows:

<m:Map x:Name="Map1" Grid.Row="1" Center="50,8" ZoomLevel="7" Mode="Aerial" CredentialsProvider="{StaticResource MyCredentials}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
 <m:Map.Children>
 <m:MapTileLayer>
 <m:MapTileLayer.TileSources>
 <local:WMSTileSource></local:WMSTileSource>
 </m:MapTileLayer.TileSources>
 </m:MapTileLayer>
 </m:Map.Children>
 </m:Map>

And here’s the resulting map:

image

Follow

Get every new post delivered to your Inbox.

Join 53 other followers