Attention! Do you have any ideas for reorganizing and updating the Mapki? Please leave a note here. Thank you!

Tile Cutter

From Google Mapki

Jump to: navigation, search

This c# program reads in one geo-referenced source image and automatically produces a set of 256x256 tiles ready for use in a custom map layer or overlay. The code uses some functions from the Java Tile Utilities on the MAPKI Tile utility code in Java. You can download a free copy of c# from Microsoft [1] .

The input image needs to have an ESRI format World File [2] and be in WGS84 geographic coordinates, that is each pixel is a fixed size in latitude and longitude. Global Mapper [3] is a great tool for producing suitable input images from a variety of sources such as SRTM terrain data [4]. I have used it together with the code here to generate the terrain overlays that you can see at [5]

The code as shown cuts tiles at the source image's natural Google zoom level and two zooms either side (no interpolation / smoothing yet). As programmed the code only writes tiles that are fully inside the source image. The iteration finds partially intersecting margin tiles but does not output them. Changing the code to include the margin tiles would be easy, putting black or transparent in the pixels that have no equivalent in the source.

On your page's java script, use a function like this to supply tiles in your custom layer.

function customGetTileUrl(a,b) {
 var f = "http://www.mysite.com/mytiles/z" + b + "x" + a.x + "y" + a.y + ".jpg";
 return f;
}

To create a hybrid tile layer with the Google's MAP use code like this (11 and 12 are min and max zoom levels for your tiles)

function loadLayer {
 var tilelayers = new Array();
 tilelayers[0] = G_NORMAL_MAP.getTileLayers()[0];
 tilelayers[1] = new GTileLayer(copyrightCollection , 11, 12);// see MAPKI for copyright management
 tilelayers[1].getTileUrl = customGetTileUrl;
 tilelayers[1].getOpacity = function () {return 0.5;};
 var custommapmap = new GMapType(tilelayers, new GMercatorProjection(13), "MyTiles+Map", {errorMessage:"No data available"});
 map.addMapType(custommapmap);
}


Here is the c# source code


using System;
using System.Collections.Generic;
using System.Text;

namespace TileCutter
{

/**
* A set of simple routines to provide information about google tiles (API v2).
* Internally a sort of offset mercator projection is used (as per google), this places the origin (0,0)
* at the top left and goes to +1,+1 at the bottom right.
* A proper mercator would have 0,0 in the middle and range from -0.5,-0.5 bottom left to +0.5,+0.5 top right.
*/
   public class GoogleTileUtils
   {
       public static int TILE_SIZE = 256;

       /**
        * Returns the pixel offset of a latitude and longitude within a single typical google tile.
        * @param lat
        * @param lng
        * @param zoom
        * @return
        */
       public static System.Drawing.Point getPixelOffsetInTile(double lat, double lng, int zoom)
       {
           System.Drawing.Point pixelCoords = toZoomedPixelCoords(lat, lng, zoom);

           return new System.Drawing.Point(pixelCoords.X % TILE_SIZE, pixelCoords.Y % TILE_SIZE);
       }

       /**
       * returns a Rectangle2D with x = lon, y = lat, width=lonSpan, height=latSpan
       * for an x,y,zoom as used by google.
       */
       public static System.Drawing.RectangleF getTileRect(int x, int y, int zoom)
       {
           int tilesAtThisZoom = 1 << zoom;
           double lngWidth = 360.0 / tilesAtThisZoom; // width in degrees longitude
           double lng = -180 + (x * lngWidth); // left edge in degrees longitude

           double latHeightMerc = 1.0 / tilesAtThisZoom; // height in "normalized" mercator 0,0 top left
           double topLatMerc = y * latHeightMerc; // top edge in "normalized" mercator 0,0 top left
           double bottomLatMerc = topLatMerc + latHeightMerc;

           // convert top and bottom lat in mercator to degrees
           // note that in fact the coordinates go from about -85 to +85 not -90 to 90!
           double bottomLat = (180 / Math.PI) * ((2 * Math.Atan(Math.Exp(Math.PI * (1 - (2 * bottomLatMerc)))))
                              - (Math.PI / 2));

           double topLat = (180 / Math.PI) * ((2 * Math.Atan(Math.Exp(Math.PI * (1 - (2 * topLatMerc))))) - (Math.PI / 2));

           double latHeight = topLat - bottomLat;

           return new System.Drawing.RectangleF((float)lng, (float)bottomLat, (float)lngWidth, (float)latHeight);
       }

       /**
        * returns the lat/lng as an "Offset Normalized Mercator" pixel coordinate,
        * this is a coordinate that runs from 0..1 in latitude and longitude with 0,0 being
        * top left. Normalizing means that this routine can be used at any zoom level and
        * then multiplied by a power of two to get actual pixel coordinates.
        * @param lat in degrees
        * @param lng in degrees
        * @return
        */
       public static System.Drawing.PointF toNormalisedPixelCoords(double lat, double lng)
       {
           // first convert to Mercator projection
           // first convert the lat lon to mercator coordintes.
           if (lng > 180)
           {
               lng -= 360;
           }

           lng /= 360;
           lng += 0.5;

           lat = 0.5 - ((Math.Log(Math.Tan((Math.PI / 4) + ((0.5 * Math.PI * lat) / 180))) / Math.PI) / 2.0);

           return new System.Drawing.PointF((float)lng, (float)lat);
       }

       /**
        * returns a point that is a google tile reference for the tile containing the lat/lng and at the zoom level.
        * @param lat
        * @param lng
        * @param zoom
        * @return
        */
       public static System.Drawing.Point toTileXY(double lat, double lng, int zoom)
       {
           System.Drawing.PointF normalised = toNormalisedPixelCoords(lat, lng);
           int scale = 1 << zoom;

           // can just truncate to integer, this looses the fractional "pixel offset"
           return new System.Drawing.Point((int)(normalised.X  * scale), (int)(normalised.Y  * scale));
       }

       /**
        * returns a point that is a google pixel reference for the particular lat/lng and zoom
        * @param lat
        * @param lng
        * @param zoom
        * @return
        */
       public static System.Drawing.Point toZoomedPixelCoords(double lat, double lng, int zoom)
       {
           System.Drawing.PointF normalised = toNormalisedPixelCoords(lat, lng);
           double scale = (1 << zoom) * TILE_SIZE;

           return new System.Drawing.Point((int)(normalised.X * scale), (int)(normalised.Y * scale));
       }

   }

   /**
   * routines to read in an ESRI world file for a geographic projection
   */
   public class WorldFile
   {
       //returns a PointF with x = left , y = top , width = units per pixel x, height = units per pixel y
       // for a Geo file this is lon , lat, degrees per pixel lon, degrees per pixel lat
       public static System.Drawing.RectangleF readWorldFile(string file)
       {
           try
           {
               System.IO.StreamReader sr = new System.IO.StreamReader(file);
               string[] lines = new string[6];
               for (int i = 0; i < 6; i++)
                   lines[i] = sr.ReadLine();
               sr.Close();

               return new System.Drawing.RectangleF(float.Parse(lines[4]),
                                                    float.Parse(lines[5]),
                                                    float.Parse(lines[0]),
                                                    float.Parse(lines[3]));   

           }
           catch (Exception e)
           {
               Console.WriteLine("The world file could not be read: ");
               Console.WriteLine(e.Message);
           }
           return System.Drawing.RectangleF.Empty;
       }
   }


   class Program
   {
       // pass in 
       // 1) full path of jpg file 
       // 2) full path to jgw world file with geographic coords 
       // 3) path for output files, output will be natural zoom of input and two levels either side
       static void Main(string[] args)
       {

           if (args.Length != 3)
           {
               Console.WriteLine("Usage is <image file> <world file> <out path>");
               return;
           }

           string inImageFile = args[0];
           string inImageWorldFile = args[1];
           string outPath = args[2];


           System.Drawing.RectangleF inGeoRect = WorldFile.readWorldFile(inImageWorldFile);
           System.Drawing.Bitmap inImage = new System.Drawing.Bitmap(inImageFile);
           System.Drawing.Graphics gr = System.Drawing.Graphics.FromImage(inImage);
           
           //GeoRect width and height to degrees
           inGeoRect.Width *= inImage.Width;
           inGeoRect.Height *= inImage.Height; 

           //find natural google zoom level for the input image according to its width
           //to do this we work out the number of image pixels for 360 degrees and then divide by two until we get to one tile.

           int pixIn360 = (int)((360.0F / inGeoRect.Width) * inImage.Width);
           int zoom = 0;
           while (pixIn360 > GoogleTileUtils.TILE_SIZE)
           {
               pixIn360 /= 2;
               zoom += 1;
           }

           // iterate around the natural zoom
           for (int z = (zoom - 2); z < (zoom + 2); z++)
           {
               if ((z >= 0) && (z <= 17))
               {

                   // find the intersecting google tiles
                   //zoom 0 is 1 tile,
                   //zoom 1 is 4 tiles etc

                   System.Drawing.Point tbl = GoogleTileUtils.toTileXY(inGeoRect.Bottom, inGeoRect.Left, z);
                   System.Drawing.Point ttr = GoogleTileUtils.toTileXY(inGeoRect.Top, inGeoRect.Right, z);
       

                   // for each intersecting tile
                   int tilesAtThisZoom = 1 << z;
                   for (int x = tbl.X ; x <= ttr.X ; x++)
                   {
                       for (int y = ttr.Y; y <= tbl.Y ; y++)
                       {
                           System.Drawing.Bitmap bm = new System.Drawing.Bitmap(GoogleTileUtils.TILE_SIZE, GoogleTileUtils.TILE_SIZE, gr);
                           bool bWrite = true;                    
       
                           // get pixels from the input image to go in the output tile
                           for (int py = 0; (py < bm.Height) && (bWrite); py++)
                           {
                               //get lat of this pixel
                               float my = (y + (((float)py) / ((float)GoogleTileUtils.TILE_SIZE))) / tilesAtThisZoom;//normalised mercator height
                               float pla = (float)((180 / Math.PI) * ((2 * Math.Atan(Math.Exp(Math.PI * (1 - (2 * my))))) - (Math.PI / 2)));
                               //y coord of pixel in input image
                               int iy = (int)Math.Round(inImage.Height * ((pla - inGeoRect.Bottom) / (-inGeoRect.Height)));
                               iy = inImage.Height - iy;

                               for (int px = 0; (px < bm.Width) && (bWrite); px++)
                               {
                                  //get lng of this pixel
                                   float mx = (x + (((float)px) / ((float)GoogleTileUtils.TILE_SIZE))) / tilesAtThisZoom;//normalised mercator width
                                   float plo = -180 + (360 * mx);

                                   //x coord of pixel in the input image
                                   int ix = (int)Math.Round(inImage.Width * ((plo - inGeoRect.Left) / inGeoRect.Width));

                                   if ((ix >= 0) && (ix < inImage.Width) && (iy >= 0) && (iy < inImage.Height))
                                       bm.SetPixel(px, py, inImage.GetPixel(ix, iy));
                                   else
                                       bWrite = false;//only want complete tiles
                               }
                           }

                           // make up the output tile's filename
                           // save the output tile
                           if (bWrite)
                           {
                               string fn = "z" + z.ToString() + "x" + x.ToString() + "y" + y.ToString() + ".jpg";

                               EncoderParameters eps = new EncoderParameters(1);
                               eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)90);
                               ImageCodecInfo ici = getCodec("image/jpeg");                                
                               bm.Save(outPath + "/" + fn + ".jpg", ici ,eps);

                               //bm.Save(outPath + "/" + fn + ".png",ImageFormat.Png);
                               //bm.Save(outPath + "/" + fn + ".gif", ImageFormat.Gif);

                               //ici = getCodec("image/tiff");
                               //System.Drawing.Imaging.Encoder enc = System.Drawing.Imaging.Encoder.Compression;
                               //eps.Param[0] = new EncoderParameter(enc,(long)EncoderValue.CompressionLZW);
                               //bm.Save(outPath + "/" + fn + ".tif", ici, eps);


                           }
                           
                       }
                   }
               }

           }
           
       }
   

       private static ImageCodecInfo getCodec(string mimeType)
       {
           ImageCodecInfo[] encoders = ImageCodecInfo.GetImageEncoders();
           for(int j=0;j<encoders.Length;j++)
               {
                   if(encoders[j].MimeType == mimeType)
                   return encoders[j];
               }
           return null;
       }

   }
}

Personal tools
Advertisement