Custom Map in Version 1
From Google Mapki
Important: This article refers specifically to creating a custom map in the old (version 1) API version. To view the method for adding a custom map in version 2, click here: Add Your Own Custom Map.
Contents |
- http://www.runwayfinder.com/
- http://potf.net/documark
- http://www.ponies.me.uk/maps/osmap.html
- http://www.ponies.me.uk/maps/dbgmap.html note: as of 12/7/05 seems to be partly broken
- http://open.atlas.free.fr/OpalCustomMap.php
- http://maps.kylemulka.com/umbuildings.php
- http://maps.kylemulka.com/satcompare.php
- http://www.simplespatial.com/gmaps/
- http://www.econym.demon.co.uk/googlemaps2/
- http://stredozem.panprstenov.com
- http://maps.webfoot.com/RaceOverlays.html (API v2)
Custom Maps in GMap V1
V1 of the API is still up and running. If V1 is better for you, you might consider implementing your Custom Map in V1. If you want to do so, follow the tutorial that follows. BUT be warned, Google wants to migrate people OFF V1, and V1 could disappear in the near future.
This Tutorial was created by Will James of onNYTurf.com who looks after the writing here, with the help of some others like you. The text is released for attribution, non-commercial, share-alike reproduction, as specified by creative commons here Especially since I learned this trick from Adrian Holovaty who did the first subway map hack for Chicago using this custom map type method.
If you want to bypass the tutorial and just want the code, skip down to the full script parts. Be sure to get the scipt for both the Map and the Tile Image Server
[NOTE]: If you implemented a custom map type PRIOR TO Oct 4th 2005 and have discovered your map breaks, please see the "Patch for Map Tile Path" in the discussion. Due to a change in the core map code, some who used this tutorial before Oct 4th 2005 will need to patch their maps with a code change.
Additional credits for helping with updates to this tutorial go to: Rummy, X15, and Benno
OVERVIEW
To really spice things up you may want to add your own map...a map that looks the way you want it to look. This off-API Hack shows you how to add your own custom map.
There are plenty of cool reasons to add your own map images. In creating the NYC Subway Google Map Hack I felt that the Google Drawing method was not elegant enough, and the lines it creates are hard to see on top of Google's orange/green default map. To make the Subway lines easier to see against the streetscape and make the lines nicer I decided to add a custom map type, one I illustrated. You can see it here.
Another great use of adding your own map is using it as a semi-transparent overlay on the other map images. Here is a very nice example of that.
Creating this transparency effect is outside the scope of this tutorial. But if you want to overlay a custom map in this way, you will still need to know the basics explained here.
5 STEPS TO ADDING A CUSTOM MAP
There are 5 simple things you will need to add to your javascript to add your own map.
1) Create A New Map Type
2) Tell the map where it can find the images that will make up your map.
3) Customize Your Map's Button Name
4) Add Your Map to Google Maps' List of Map Types
5) Optional Settings
1) Create A New Map Type
The Google Map comes with three default maps which you are probably already familiar with. They are: MAP, SATELLITE, and HYBRID. These are known as map types, and they are accessed from an array called mapTypes. The mapTypes array is a property of our map Object, and we reference it like so: map.mapTypes. To add your own map we need to create a new map type and then add it to this array.
Each map type Google provides (i.e. MAP, SATELLITE, HYBRID) has many properties. We need to make our custom map type just like these. But we really don't want to have to build a whole map type from scratch, setting all the properties anew. We can save a lot of time by duplicate an existing map type and switching just a few of the values to suit our needs.
For the NYC Subway Google Map Hack I made my map type match Google's default MAP. This default map is the first map stored in the mapTypes array. So we are just going to copy that first map type and store it in a variable temporarily for editing (here it is named yourMapType, but you can name this variable what ever you like). There is one special thing we need to be aware of here; since each mapType has several associated properties we can't just dupe it like this:
//This Won't Work
yourMapType = map.mapTypes[0];
We need to make sure we copy all the properties of the map type Object, so we have a special little function we are going to include and call to copy all the properties of the default map type:
// Copy Object Function
function copy_obj(o) {
var c = new Object(); for (var e in o) { c[e] = o[e]; } return c;
}
Note from Rummy and Ammended by Will:
This is a modification of the original Copy Object Function. It is not confirmed if it works cross-browser, it does work in firefox 1.0.7 with maps.22.js. But this is how the Copy Oject Function should be rewritten. NOT using this version is safe. The steps in the tutorial compensate for not using this option.
// Copy Object Function
function copy_obj(o) {
var c = new Object();
for (var e in o) {
if (typeof o[e] =='array' && o[e].constructor == Array) {
c[e] = o[e].copy();
}
else {
c[e] = o[e];
}
}
return c;
}
This function duplicates the (new in maps.22.js) baseUrls Array rather than taking a reference, so an assignment to baseUrls[0] doesn't replace the one in the original google map object.
End Note
Using our Copy Object Function we can now duplicate the default map type:
// Copy mapType (this works!)
yourMapType = copy_obj(map.mapTypes[0]);
If you want to create a hybrid style map where you get the underlying satellite imagery
and your own overlays on top then you should copy the hypbrid map type instead:
yourMapType = copy_obj(map.mapTypes[2]);
Now we change a few of the properties to customize our map type. There are only two we really need to change:
- The Path to Our Map Images
- Our Maps' Button Name
2) Set The Path to Your Map Images
Each Google map type is made up of thousands of image tiles. When you see the Google Map in a window you are really seeing about 12 tiles at a time, depending on your screen size. As you drag the map around in the window, your browser downloads what ever tiles it needs in order to show more of the map. Each map type has a path to its associated image tiles. This value is kept in the property baseUrls. The default maps point to Google's server. To load our own image tiles, we need this path to point at our own server. We actually don't point right at a directory of images. The map sends values to the server and asks for an image in return. So we use a PHP script to serve up the tiles based on the values the map sends (this image serving script is discussed in more detail later), so here we point to that script:
// Set Path to Our Custom Map Image Tiles
yourMapType.baseUrls = new Array();
yourMapType.baseUrls[0] = "http://www.yoursite.com/googlemap/images/index.php?";
Google Maps looks for an array of urls. So first we set the baseUrls property to be an array, then we put our PHP script path in the first slot "[0]". You'll notice we include a "?" at the end of our path. The Google Maps application looks for this so we include it. What it does specifically is not important for this tutorial.
We are almost set.
Its critical to note that the baseUrls array holds the path to any tiles for Zoom level 8 and below. These are your closest zoom levels. For tiles showing the map further zoomed out, we must specify the path in second array, the lowResBaseUrls array. The path here points to tiles at Zoom level 9 and higher. Since I like to keep all my custom map tiles in the same place, no matter the zoom, I use the same php path:
yourMapType.lowResBaseUrls = new Array();
yourMapType.lowResBaseUrls[0] = "http://www.yoursite.com/googlemap/images/index.php?";
While this may seem like a hassle, Google does us a favor here. Google assumes we are most likely to be creating custom map types at the closer zooms. So they have broken off 9 and above to make it easy for us to use their tiles for Zooms 9 and higher. You dont need to change the lowResBaseUrl. If you dont specifiy it, if you dont create the array and dont set a path, then Google will serve its zoomed out imagery for your map type. Nice.
The Hybrid Map Type uses another set of Tile URLs for the transparent tiles that overlay the Satellite imagery. Again, they are split into 2 and are stored in arrays.
yourMapType.overlayBaseUrls = new Array();
yourMapType.overlayBaseUrls[0] = "http://www.yoursite.com/googlemap/images/index.php?";
yourMapType.lowResOverlayBaseUrls = new Array();
yourMapType.lowResOverlayBaseUrls[0] = "http://www.yoursite.com/googlemap/images/index.php?";
With these configured, our images will load when our custom map type is selected.
3) Customize Your Map's Button Name and Copyright
Although the Google Map API does not automatically add the Map Types buttons that appear in the upper right corner of their map, most people add them using the API addControl method. If you call this method in your map, a button will automatically be generated for your map type too as you can see on the Subway map:
But that button did not just magically get the name 'ubway' We can customize the name of our map type's button by setting our map type's getLinkText property. The Google Map application calls it as a function, so we want to return the name we want to give it, like so:
// Change the Button Name for Our Map Type.
yourMapType.getLinkText = function() { return 'SUBWAY'; }
If you want to add copyright to the maps owner then you can change the getCopyright() function to return the relevant information:
// Change the copyright string on our map type
yourMapType.getCopyright = function() { return 'benno.id.au 2005'; }
Nice and simple eh? So that's all we have to do to make a custom map type now we just add it to the array mapTypes.
4) Add Your Map to the List of Map Types
We will add our map to the end of the mapTypes array, and then, because we love our map more than any other map in the world we set it to be the default map when our map page first loads. To add our custom map to the list we just make the next available spot in the array equal to out temporary map type object that we have been working on. We use the API's setMapType() to then make our custom map type load first.
// Add Our Map to the mapTypes Array.
map.mapTypes[map.mapTypes.length] = yourMapType;
// Make Our Map the Map Type that Loads First
map.setMapType(map.mapTypes[map.mapTypes.length-1]);
(note: In the above we use the array value length and length-1 to determine where in the mapTypes array we put our map and where we reference our map. In this way our map is always the last in the list. We could have made our map fourth in the list like so: map.mapTypes[3] = yourMapType; (remember arrays start their count at 0 not 1, so 3 is really the fourth item). But what if Google were to add a fourth map type to the array? It would replace ours. So we always make our map the last in the array just incase Google adds more map types in the future.)
5) Optional settings
So far we've created a map type that covers the whole globe, and all 18 zoom levels. In practice we might only have map images for a limited region and for a limited number of zoom levels. We might even have map images for deeper zoom levels than the standard API map types support.
There are a set of optional properties and methods that we can add to our new map type to handle these situations.
We can set the map type's zoomLevelOffset property to the deepest zoom level that our images support. This can be a negative value if we support deeper zooms than normal, or it can be a positive value if we don't have images for the deepest zoom levels. If we set this value to 5, then zoom level zero in our map type will correspond to zoom level 5 in a normal map type. The API will apply this offset when the user switches maptypes. The image serving script below will need to adjust for this offset, the API will request z values that match the zoom levels for our map type. In parallel with this setting, we should adjust the value of numZoomLevels:
// Set the deepest zoom level
yourMapType.zoomLevelOffset = -5;
yourMapType.numZoomLevels = 18 - yourMapType.zoomLevelOffset;
We can set the getMaxZoomLevel() method to specify the maximum zoom level which our images can support. When the user zooms out further than this, the display will revert to the default map type and our entry will be removed from the map type control.
// Set the shallowest zoom level
yourMapType.getMaxZoomLevel=function(){ return 4; }
We can specify a limited region where our map type will be supported if our imagery does not cover the whole globe. When the user pans so that the centre of the map is outside the specified region, the display will revert to the default map type and our entry will be removed from the map type control. The getBounds() method does all the work, but we don't want to create a new GBounds object every time the API checks our bounds, so we call new GBounds once, and have getBounds() return a reference to it:
// Set a limited region
yourMapType.bounds = new GBounds(-81,39,-77,46);
yourMapType.getBounds=function(){return this.bounds}
We can also specify the map type to be used when the user zooms or pans outside the region supported by our map type. This would normally be the default map type G_MAP_TYPE, but if we want to revert a different map type we can do this:
// Switch to Satellite mode when out of bounds
yourMapType.getAlternativeMapType=function(){ return G_SATELLITE_TYPE }
Full Script
Finally lets put it all in the context of the rest of our typical map initializing function:
NOTE: This function is called from the body tag of our HTML: <body onload='Load()'>
// Load With A Custom Map
function onLoad() {
//Create a new GMap and load it to the html object id="map"
map = new GMap(document.getElementById('map'));
// Copy Object Function
function copy_obj(o) {
var c = new Object(); for (var e in o) { c[e] = o[e]; } return c;
}
//Copy mapType
yourMapType = copy_obj(map.mapTypes[0]);
// Set Path to Our Custom Map Image Tiles
yourMapType.baseUrls = new Array();
yourMapType.baseUrls[0] = "http://www.indawgyi.goduck.net/index.php?";
yourMapType.lowResBaseUrls = new Array();
yourMapType.lowResBaseUrls[0] = "http://www.yoursite.com/googlemap/images/index.php?";
// Map Display name in the auto-generated maplink in the top right corner.
yourMapType.getLinkText = function() { return 'SUBWAY'; }
// Register the new mapType with the running google map.
map.mapTypes[map.mapTypes.length] = yourMapType;
//Set the onload view to our new map
map.setMapType(map.mapTypes[map.mapTypes.length-1]);
//Add Map Type buttons in the upper right corner
map.addControl(new GMapTypeControl());
//Add Small zoom controls
map.addControl(new GSmallZoomControl());
}
Thats all you have to add to your javascript to a custom map type to your map. You will still need to make map tiles of course, and you will need a script on the server to serve up your map tiles. So lets get right to setting up your script...
IMAGE SERVING SCRIPT
As mentioned in step two above we use a php script to pass image tile requests to the map. Every time you drag the map around in the window, the Google Map application requests whatever tiles it needs in order to show more of the map. It does this by passing 3 value to the server for each tile it needs: an X coordinate value, a Y coordinate value, and a Zoom value. For each tile it needs, the map application sends a set of these 3 values. To send back the image appropriate to these values we need a script.
Our image serving script uses the 3 values sent by the Google Map application to dynamically create paths to the appropriate image required to fill in a space in the map window. Our script is kept in a file named index.php
With a little fancy footwork we can also use our script to send generic filler tiles to the map when we don't have custom tiles for a requested area. Let me explain. In some cases you might create a custom map that covers a small area. In fact this almost always is going to be the case. Its highly unlike anyone will create a custom map that covers all zoom levels across the whole globe. Therefore you will probably need to use some filler tiles where your map does not cover an area or zoom level. We can use our image serving script to determine when we don't have an appropriate custom tile and need to send a filler tile.
To understand this script we'll start with the whole thing and then break it down.
Full Script
Here is the full script that goes in our file index.php
<?php
define("NO_DATA", "./no_data.gif");
define("ZOOM_IN", "./zoom_in.gif");
define("ZOOM_OUT", "./zoom_out.gif");
$x = $_GET["x"];
$y = $_GET["y"];
$z = $_GET["zoom"];
$filename = "./maptiles/${x}_${y}_${z}.gif";
if ( $z < 2 ) {
$content = file_get_contents( ZOOM_OUT );
}else if ( $z > 5 ){
$content = file_get_contents( ZOOM_IN );
}else if (is_numeric($x) && is_numeric($y) && is_numeric($z) && file_exists($filename)){
$content = file_get_contents( $filename );
}else{
$content = file_get_contents( NO_DATA );
}
header("Content-type: image/gif");
echo $content;
?>
Ok, there it is. Now lets break it down.
Break Down
The first three lines setting up some alternatives we will use these a little later on. These are going to be our filler tiles. We have one for each of three conditions. When the zoom level requested is too far out and the viewer needs to zoom-in, when the zoom level requested is too close and the viewer needs to zoom-out , and when there is no image for a requested area or no data is sent by the map.
<?php
define("ZOOM_IN", "./zoom_in.gif");
define("ZOOM_OUT", "./zoom_out.gif");
define("NO_DATA", "./no_data.gif");
Next we get the X, Y, and Zoom values from the Map and store them in similarly named variables.
$x = $_GET["x"];
$y = $_GET["y"];
$z = $_GET["zoom"];
Using these three values we create a path to a tile file, here a gif, and we store it in the variable $filename. You'll note that the x, y, and zoom (z) values are part of the tile name. This is the basic structure we use to name all our tiles. This is how we differentiate the hundreds or thousands of custom tiles we likely end up with when creating a custom map. Now that's a lot of custom names, but don't worry about it too much. Later we'll automate the cutting and naming of your tiles.
//Note the path...here we reference a subdirectory of our images folder named "maptiles"
//You can put your tiles anywhere you like, this is just a suggestion
$filename = "./maptiles/${x}_${y}_${z}.gif";
Now we have a path appropriate to the data sent, but that does not mean the tile actually exists. $filename is just a fourth image option' addition to no-data, zoom-in, and zoom-out. Before we send any image back to the browser we want to check if the request is within our zoom range and map area. This next chunk of the script uses several if/else statements to filter though the possibilities and determine the appropriate image. The winner will be stored in variable $content. The specific checks we make are explained by the in-line comments:
// if the zoom level requested is too close,
// we send images that say zoom out
if ( $z < 2 ) {
$content = file_get_contents( ZOOM_OUT );
// if the zoom level requested is too far out,
// we send images that say zoom in
}else if ( $z > 5 ){
$content = file_get_contents( ZOOM_IN );
// here we make sure values were sent for x, y, and zoom,
// AND that the image tile actually exists.
// If it all checks out we send the file we defined in $filename
}else if ( is_numeric($x) && is_numeric($y) && is_numeric($z) && file_exists( $filename ) ){
$content = file_get_contents( $filename );
}else{
// other wise if one of the values was not sent,
// or no matching tile exists we send a NO_DATA image
$content = file_get_contents( NO_DATA );
}
Now that we know which image we are sending back we ready to do so. The reason we didn't just send it above is that we first need to tell the browser that we are sending an image. We tell it this by sending it some header information:
header("Content-type: image/gif");
Now we can pass it the image path with a simple echo of $content
echo $content;
?>
That's all there is to it! Save it as index.php and upload it to your images folder.
There are two things I really like about this script that make it a life saver. The first is the NO_DATA condition. For the Subway map I only made tiles that covered the greater NYC area. Outside of that there are no custom tiles. Without the NO_DATA image, in these places where I don't have tiles to match Google's, we would normally see broken image links or the grey background of the map window. But thanks to the NO_DATA image I can create one blue tile that matches my border and extends infinitely any where I don't have custom tiles for a location. Alternatively for those more advanced, you could extend this script to relay the x, y, and zoom values back to Google; then you could request their tiles for places where you do not have any.
The other thing I like about this script is you can quickly adjust which zoom levels are available. You just set min and max thresholds in those if/else statements and you get alternative tiles sent to the map for anything outside your zoom range. In creating the NYC Subway Map I tweaked the map art at each level. This is a bit time consuming, but gives me better looking maps. So I incrementally add new zoom levels as I complete the art. Every time I add a new one all I have to do is go into this script and change the min or max value to make it available.
Improving Caching in the image serving script
The image serving script as it stands will always serve the requested image and doesn't give much information about the images timestamp. Adding a Last-Modified header gives web caches a chance to do their stuff reducing the bandwidth of your site. You can also respond to an If-Modified-Since request with a 304 Not Modified again reducing bandwidth. This php fragment shows how you can supply a Last-Modified header and react to the the If-Modified-Since header. Once you have your tile server working, replace the line echo $content; with this code.
// Getting headers sent by the client.
$headers = emu_getallheaders();
// Checking if the client is validating his cache and if it is current.
if (isset($headers['If-Modified-Since']) && (strtotime($headers['If-Modified-Since']) == filemtime($filename))) {
// Client's cache IS current, so we just respond '304 Not Modified'.
header('Last-Modified: '.gmdate('D, d M Y H:i:s', filemtime($filename)).' GMT', true, 304);
} else {
// Image not cached or cache outdated, we respond '200 OK' and output the image.
header('Last-Modified: '.gmdate('D, d M Y H:i:s', filemtime($filename)).' GMT', true, 200);
header('Content-Length: '.filesize($filename));
echo($content);
}
function emu_getallheaders() {
foreach($_SERVER as $h=>$v)
if(ereg('HTTP_(.+)',$h,$hp))
$headers[$hp[1]]=$v;
return $headers;
}
Next...Automate Making and Naming Map Tiles
Part two of this tutorial, the Automatic Tile Cutter shows you how to configure a Photoshop script to automatically cut up your custom map. This ones a life saver!
Custom Maps Without an Image Serving Script
It is possible to create a custom map type without using an image serving script.
If your tiles all exist as actual image files, you don't need a server script to fetch them, you can determine the name of the tile file in your .getTileUrl() method, and return the actual URL of the tile image file rather than a request to a server.
