Using the Google Maps API to zoom photographs

Please note that this is a fairly advanced exercise that requires at least some understanding of Javascript and the Google Maps API.

I work for a well known retail/online flooring store, and was recently asked to replace an antiquated (and defunct) image zoom application with something cheaper and more reliable than its more modern replacement (which would cost us thousands of dollars per month to license). iFloor is known for having extremely high resolution photographs of their products, and we needed new a way to display them that was easy for customers to use, as well as light on processing power and bandwidth, not to mention easy for developers and system administrators to maintain.

I owe a great deal of thanks to iFloor for allowing me to publish this tutorial, so if you find this helpful, please consider them the next time that you are in the market for new floors or an area rug.

Being that I'm somewhat of an old-school web designer and don't like to rely on Flash/SWF in web pages (although I'll happily admit that image zoom would be a pretty good application for it), I immediately turned my attention to the tile-based zoom techologies like Google Maps and Open Layers. Having been given a relatively short deadline, I wasn't about to write my own tile display engine from scratch, and as much as I am a fan of Free Open Source software, I decided to stick with the commercially-backed and well-documented Google Maps API because it would be easier for both myself and iFloor to maintain.

View Image:


The first problem to tackle is to create the tiles from your images. Thankfully, this process is fairly well documented and there are at least half a dozen or so scripts/websites scattered around the internet that will generate them for you. If you don't want to look for them, you can use the one that I wrote, which happens to output a directory structure that matches the myTileURL() function described in this tutorial. It's pretty straighforward -- you just pass it an output directory and list of images to slice up, and it does the rest of the work. See the --help option for more information.

For example, this is what I used to generate some of the image tiles used in this demo:

./ -v --path tiles/ paris.jpg ravenna.jpg venus.jpg

This created tiles/paris/, tiles/ravenna/, and tiles/venus/ directories, with a tiles directory tree beneath them that matches the format expected by the myTileURL() function that will be described below.

Keep in mind that the Maps API requires that your images be square, and fit within 256 times some multiple of 2. Before creating the tiles themselves, my script will resize your image up to the next largest zoom level, and pad the edges with black pixels to give you a square image. This can occasionally make the deepest zoom level a little fuzzy, but in my opinion, it's better than preventing a full zoom level if your image is 2000x2000 instead of 2048x2048. If you don't like this behavior, the code is well-documented and you should only need basic knowledge of Perl in order to comment it out.

Then you just have to define a function to tell the Maps API where to find your image tiles, and override the default getTileURL value in your GTileLayer object:

myTileURL=function(p,z){ return "tiles/"+img_base+"/"+z+"/"+p.x+"-"+p.y+".jpg"; } // ... var tilelayers = [new GTileLayer(copyright,1,max_zoom]; tilelayers[0].getTileUrl = myTileURL;

The Map Problem:

Google's instructions for using custom map tiles are fairly straightforward, with one major caveat: Google Maps was designed to display maps, not photographs. More specifically, it was designed to display 2 dimensional representations of a globe, which wrap when you scroll around (also referred to as panning).

When I first started researching the idea of using the Maps API to display zoomed versions of photographs, I needed to find a way to turn off this wrap feature -- after all, photographs are. This turned out to be significantly more difficult than I initially expected, and is the primary reason that I have taken the time to write this article. There are two pieces to this puzzle.

Boundary Detection:

I can't take credit for the idea of creating a bounds-checking script. There have been several versions published, and all rely on a function that is triggered whenever a "move" GEvent is detected. My code looks pretty much like this:

GEvent.addListener( map, "move", function() { checkBounds(); } ); function checkBounds() { // Read on for more about bounds checking... }

The idea is that the checkBounds() function will be called whenever the user drags the map around. You can then use checkBounds() to determine if the user has dragged the "map" image outside of its boundaries, and if so, reset things to put the viewport back within the expected minimum/maximum coordinates.


The problem with all of the bounds checking routines that I could find is that they were heavily dependent on map coordinate systems, which are in turn heavily dependent on both the zoom level, and Google's coordinate system, which assumes that you're using Google's map images. You can't just use pixel coordinates to tell when you've reached the edge of your image because the Maps API only wants to deal with map coordinates. On top of this, when using custom tiles, the coordinate system has a rather annoying default feature that wraps lattitude and longitude coordinates at 90 and 180 degrees, respectively, whenever the map is panned around past the parts of the image initially visible in the viewspace. This makes it incredibly difficult to determine exactly where you are within your photograph, since the coordinates 50,50 appear many times throughout the image at a high zoom level.

The few working examples I could find were either restricted to real-world map coordinates (e.g. restricting viewers to a specific city for a mashup that only had local data), or only worked for one or two zoom levels, which had custom boundary detection routines for each zoom level. Neither of these solutions was acceptable to me.

After several frustrating re-reads of the documentation, I noticed the fromPixelToLatLng() and fromLatLngToPixel() methods on the GMercatorProjection class, which weren't really documented as well as other methods, but I figured from the names that they might be useful to me. Thankfully, they do exactly what I hoped: convert image pixel coordinates to lattitude and longitude coordinates.

Since we don't care about lattitude and longitude in a photograph, I overloaded these methods with two of my own creation, and inserted them into my proj GProjection object. In order to keep things simple (I like to think in terms of percentages), I just assume that the coordinate boundaries are 100x100, no matter how far in or out the user has zoomed. In order to keep things compatible with Google's behavior, I then shift things 50 degrees up and left so that 0,0 represents the center of the image, and the boundaries are defined at -50 and 50 on both the X and Y axes.

proj.fromPixelToLatLng = function(pixel, zoom, unbounded) { var max = Math.pow(2,zoom)*256; var lng = -(pixel.x / max) * 100 + 50; var lat = (pixel.y / max) * 100 - 50; return new GLatLng(lat, lng, unbounded); } proj.fromLatLngToPixel = function(latlng, zoom) { var max = Math.pow(2,zoom)*256; var x = -max * ((latlng.lng() - 50) / 100); var y = max * (( + 50) / 100); return new GPoint(x, y); }

These routines do expect that your photos are square, and still let your viewers drag the zoom images into the black padding areas created by the create_tiles script. It would be fairly simple to could be extend them further to restrict the dragging to within the actual image boundaries, but that would require keeping track of the ratio of the original image dimensions, and somehow passing it into the javascript routine. It would require keeping track of the original image dimensions (among other things), which adds more complexity than I wish to explain in this tutorial (or even set up on the iFloor website), so I have left off the feature.

Putting It Together:

Because the new coordinate system sets a hard boundary of plus or minus 50 degrees, no matter what the zoom level, there is no longer a need to create a custom boundary checking routine for each zoom level, and it will always be accurate for our photos, no matter what zoom level the user is viewing at.

function checkBounds() { // Get some info about the current state of the map var C = map.getCenter(); var lng = C.lng(); var lat =; var B = map.getBounds(); var sw = B.getSouthWest(); var ne = B.getNorthEast(); // Figure out if the image is outside of the artificial boundaries // created by our custom projection object. var new_lat = lat; var new_lng = lng; if ( < -50) { new_lat = lat - ( + 50); } else if ( > 50) { new_lat = lat - ( - 50); } if (sw.lng() < -50) { new_lng = lng - (sw.lng() + 50); } else if (ne.lng() > 50) { new_lng = lng - (ne.lng() - 50); } // If necessary, move the map if (new_lat != lat || new_lng != lng) { map.setCenter(new GLatLng(new_lat,new_lng)); } }

At this point, I won't bore you by pasting in more code that you can just see by viewing the source for this page (or downloading the full php source). The rest of it is just the fairly straightforward routine of creating and activating a GMap object, and then attaching various site-specific customizations (e.g. a copyright notice), as well as the custom tile and boundary handlers. You will find the well-documented javascript defined at the top.

If you don't like my demo photos at the top of the page, feel free to head over to iFloor and take a look at any of the products that let you "click to zoom". Chances are that even the small thumbnails for alternate views will bring up a full scale zoom image.

Article Text is ©2008 to Chris Petersen, and may not be copied or reproduced without permission.

Code samples are ©2008 to Chris Petersen and, except where Google API licensing might require other attribution. You are, however, more than welcome to use the code samples and techniques described in this article within your own photo zoom application, provided that you provide proper attribution, and your application does not violate Google's own Terms of Use.