Google Maps Hacking and Bookmarklets

New: myGmaps enables you to create, save and host custom data files and display them with Google Maps.
Visit myGmaps.com for custom Google Maps.

Update: New version maps.6.js. Breaks at least onMapStateChanged handlers, now expects a function rather than an object—affects lat/long bookmarklet.

Update: There looks like a new version of Google Maps (maps.4.js—make that maps.5.js, someone's been busy--check out Part 13 for some rampant speculation :-) ) is on the scene. I haven't looked at what's changed—I've been too busy with the unauthorised Google Maps Standalone Mode.

Update:The latest version of Google Maps (maps.3.js) requires http://libgmail.sf.net/loadxml-0.0.4.js or later. The route following bookmarklets have not yet been updated to work with the latest Google Maps version.

Ah, the lure of all things Google...

I've been poking around in Google Maps and had some success I thought others might be able to build on. I've put this in a couple of places but thought I'd throw it here as well:

Part 1 : XML Handling

Okay, I've been playing around with sending requests to Gmaps via a Python proxy and manipulating the files returned on the fly.

As a result I've created a bookmarklet that loads the required XML file into the page: (Google Maps Load XML Data Example Bookmarklet)

javascript:(function() {window.parent._load('<?xml version="1.0"?><page><title>Somewhere</title><query>Somewhere</query><center lat="49.275428" lng="-123.116856"/><span lat="0.017998" lng="0.027586"/><overlay panelStyle="/mapfiles/geocodepanel.xsl"><location infoStyle="/mapfiles/geocodeinfo.xsl" id="A"><point lat="49.286428" lng="-123.116856"/><icon image="/mapfiles/marker.png" class="local"/><info><title xml:space="preserve"></title><address><line>Somewhere</line></address></info></location></overlay></page>', window.document)})();

If you save this book marklet and click on it when on any (?--certainly the home page) Gmaps page it should move you to a location in Vancouver, Canada.

By changing various values you can do some more experimenting... For example, I have successfully added another "location" tag with an id "B" and new coordinates. Also I changed the marker image to be a random png from the Google site--but discovered the image dimensions seem to be hard coded so it was distorted.

Part 2 : Direct Page Manipulation

I've located probably the most interesting object ("_m") on the page, which is demonstrated with this bookmarklet which pans the map: (Google Maps Pan Example Bookmarklet)

javascript: (function() {_m.map.pan(100,0)}) ();

(Some of) the available properties and methods are as follows (care of the handy JavaScript Shell bookmarklet):

props(_m)

Fields: map, mapContainer, panel, metaPanel, permalink, feedbackLink,
urlMaker, vpage, vpageDoc, lastSearchSpan

Methods of prototype: beforePrint, afterPrint, loadMap,
createMapControl, onMapStateChanged, resizeMapView, getWindowSize,
loadIconClasses, loadXML, loadVPage, showOverlayPanel,
showDirectionsPanel, search, clearSearchState, getPageURL, email,
print, initSafari, loadSafari

Methods of prototype of prototype: setTimeout, _setTimeoutDispatcher,
eventHandler, trace

props(_m.map)

Methods: onzoom, onmousedown, onresize

Fields: container, disablePopups, disableDragging, zoomLevel,
topLeftTile, currentPanOffset, centerBitmap, tilePaddingOffset,
tableSize, overlays, locations, panDistance, panKeys, spec, viewSize,
div, infoWindow, directionsDiv, dragObject, tileImages, onpan,
onspecificationchange, oninfowindowclose, stateListeners,
lastPageCenter, lastPageZoom, directions, centerLatLng, lastLatLng,
openLocation, panSiner, panStart, panTimeout

Methods of prototype: createMapDiv, loadTileImages, deleteTiles,
initializeMap, getSpanLatLng, getCenterLatLng, getBoundsBitmap,
getBoundsLatLng, calculateTileMeasurements, configureImage, onDrag,
onMove, rotateTiles, rotateLeft, rotateRight, rotateUp, rotateDown,
getTotalOffset, onDragStart, onDragEnd, onDoubleClick, onClick,
getRelativeClickPoint, reconfigureAllImages, pan, doPan, cancelPan,
recenterOrPanToLatLng, recenterOrPanToBitmap, centerAndZoom,
centerAtLatLng, centerAtBitmap, addStateListener, onStateChanged,
onResize, getCurrentOffset, switchSpecification, setSpecification,
zoomTo, toggleTileBorders, addOverlay, createLocalMarker,
createLocationMarker, clearOverlays, getDivCoordinate,
repositionOverlays, setMarkerPosition, loadVPage, registerKeyHandlers,
onKeyPress, onKeyUp, ignoreKeyEvent, startContinuousPan,
doContinuousPan, onWindowBlur, onIconMouseDown, clearInfoWindowArgs,
infoWindowNavigate, showInfoWindow, addMarkersToInfoWindowMask,
addMarkerToInfoWindowMask, showSizedInfoWindow, showMapBlowup,
createSpecChangeLink, onInfoCloseClick, closeInfoWindow,
panToInfoWindow, repositionInfoWindow, getVMLPathString, createRawVML,
getBitmapVectors, getVectorPath, getEncodedImageSource,
createVectorSegments, createImageSegments, drawDirections,
drawDirectionsMarkers, showDirectionsStart, showDirectionsEnd,
showDirectionsStep, getDirIndicatorAngle, getDirIndicatorPath,
hideDirectionsMarkers, directionsMarkersAreVisible, createMapControl,
createZoomControls, createPanningControls, createZoomSlider,
getRelativeZoomSliderPos, getZoomFromRelativeCoord, showCopyright,
createCopyright

Methods of prototype of prototype: setTimeout, _setTimeoutDispatcher,
eventHandler, trace

_m.map.viewSize
(684, 516)

This should allow specifying things like a pan from one place to another, and adding additional markers, via Javascript bookmarklets. This approach is probably more practical than intercepting the XML/JS retrieved from the server (which is still possible & could be interesting).

Part 3 : Automatic route following

Last thing for the moment...

The following bookmarklet will follow a plotted route from start to finish, moving the map as appropriate: (Animate Google Maps Route Bookmarklet 1)

javascript: (function () {mv = function(i) { c = _m.map.directions.polyline.getPoint(i);_m.map.recenterOrPanToLatLng(c); if (i < _m.map.directions.polyline.numPoints - 1) {window.setTimeout("mv("+(i+1) + ")",500)}}; mv(0)})();

It works best when you've zoomed in pretty closely. It has no error checking so ensure you have actually got directions currently listed. It takes no notice of the zoom level nor how many points there are in the route, so it could take a long time if the route is long. Obviously there are improvements that can be made along those lines.

I've tested this under FireFox 1.0 on Mac OS X, I'd be interested to know if it works successfully elsewhere.

Update:A comment by "matt"[1] extended the bookmarklet to move the start marker along as we follow the trail: (Animate Google Maps Route Bookmarklet 2)

javascript: (function () {mv = function(i) { c = _m.map.directions.polyline.getPoint(i);_m.map.recenterOrPanToLatLng(c); _m.map.setMarkerPosition(_m.map.directionsStart, N.get("local"), c); if (i < _m.map.directions.polyline.numPoints - 1) {window.setTimeout("mv("+(i+1) + ")",750)}}; mv(0)})();

Part 4 : Adding humanity

The following bookmarklet is like those in Part 3 above except it moves a non-Google-supplied image (of a person) along the route: (Animate Google Maps Route Bookmarklet 3)

javascript: (function () {if (!_m.map.me) {_m.map.me = _m.map.createLocationMarker("http://libgmail.sourceforge.net/man.png", N.get("local"));}; mv = function(i) { c = _m.map.directions.polyline.getPoint(i);_m.map.recenterOrPanToLatLng(c); _m.map.setMarkerPosition(_m.map.me, N.get("local"), c); if (i < _m.map.directions.polyline.numPoints - 1) {window.setTimeout("mv("+(i+1) + ")",750)} else {_m.map.me.hide()}};_m.map.me.show();mv(0)})();
This provides an example of how to create totally custom and arbitrarily placed markers--this could be used to highlight locations of interest along the route, for example, or display a UFO or armies of attacking mutants--or something. (Hmmm, the SimCity idea is starting to look possible--where's Godzilla when you need him? Hmmm, animated storys on Google Maps? :-) )

At present the existing shadow is used. Still need to investigate changing image size.

Update: Using an image with a custom size: (Arachnophobia)

javascript: (function () {if (!_m.map.it) {iti = new Object(); iti.x = 80; iti.y = 80; iti.shadowURL = ""; _m.map.it = _m.map.createLocationMarker("http://libgmail.sourceforge.net/spider.png", iti);}; mv = function(i) { c = _m.map.directions.polyline.getPoint(i);_m.map.recenterOrPanToLatLng(c); _m.map.setMarkerPosition(_m.map.it, N.get("local"), c); if (i < _m.map.directions.polyline.numPoints - 1) {window.setTimeout("mv("+(i+1) + ")",750)} else {_m.map.it.hide()}};_m.map.it.show();mv(0)})();
Still need to offset the image so it looks correct.

Update: Same as above but with the image offset so it looks better: (Arachnophobia 2)

javascript: (function () {if (!_m.map.it) {iti = new Object(); iti.x = 80; iti.y = 80; iti.shadowURL = ""; itd = new Object(); itd.width = 80; itd.height = 80; itd.pointCoord = new n(41, 34); _m.map.it = _m.map.createLocationMarker("http://libgmail.sourceforge.net/spider.png", iti);}; mv = function(i) { c = _m.map.directions.polyline.getPoint(i);_m.map.recenterOrPanToLatLng(c); _m.map.setMarkerPosition(_m.map.it, itd, c); if (i < _m.map.directions.polyline.numPoints - 1) {window.setTimeout("mv("+(i+1) + ")",750)} else {_m.map.it.hide()}};_m.map.it.show();mv(0)})();

Part 5 : Now with InjectionPower!™

The following approach uses a technique to "inject" an external script into an existing page. According to the author of the original page, it only works with Internet Explorer, but I've had success with FireFox 1.0 (when a delay is added to the function call). I would be interested to know if anyone has success or failure with this technique. (Inject External File Bookmarklet)

javascript:(function(){if (typeof(extjs)=='undefined') {extjs='';}; extjs = prompt('File to inject:',extjs); var script=document.createElement('script'); script.src=extjs; document.getElementsByTagName('head')[0].appendChild(script); setTimeout('onInject()',1000)})();

When prompted for the file to inject, enter: http://libgmail.sf.net/routefollow-0.0.1.js Note: When you do this, you are allowing your browser to run completely arbitrary code in a context where it has more control over your browser than usual--specifically, it will run as if it was loaded from the current page. Make sure you understand the implications of this before you use the above bookmarklet.

You should now see "Go" and "Stop" links in the yellow bar near the top of the Google Maps page.

I have extended this technique to allow an automatically updating clock (with the data retrieved from a non-Google URL) proof-of-concept which lays the groundwork for some interesting possibilities...

Update: Okay, here's a much nicer implementation of the injection code that doesn't require the timeout hack. It relies on using a "load" event triggered when the inserted script element has loaded--handy really: (Inject External File Bookmarklet 2)

javascript:(function(){if(typeof(extjs)=='undefined'){extjs='';};extjs=prompt('File to inject:',extjs);var script=document.createElement('script');script.src=extjs;var evl=new Object();evl.handleEvent=function (e){onInject();};script.addEventListener('load',evl,true);document.getElementsByTagName('head')[0].appendChild(script);})();

Part 6 : Big Brother is listening

The following bookmark hooks into the "state listener" functionality Gmaps helpfully provides in order to update a Lat/Long display whenever the map is moved (it does not rely on polling): (LatLong Autoupdate Bookmarklet)

javascript:(function(){scl=new Object();scl.onMapStateChanged=function (){document.getElementsByClassName('title')[0].innerHTML=_m.map.getCenterLatLng()};_m.map.addStateListener(scl);})()

Update: As of maps.6.js addStateListener expects a function, not an object: (LatLong Autoupdate Bookmarklet 2)

javascript:(function(){_m.map.addStateListener(function(map) {document.getElementsByClassName('title')[0].innerHTML=map.getCenterLatLng()});})()

Part 7 : Arbitrary XML upload via GUI

There's currently a few alternative approachs to this about (that'll teach me not to release early and often! :-) ) but here's my cross-browser UI-based implementation of loading an arbitrary Google Maps format XML file into Google Maps.

Using the "Inject External File Bookmarklet" from above (and after reading the associated warning) inject the following:

http://libgmail.sourceforge.net/loadxml-0.0.1.js
This will add a text input box and "Upload" button. Supply any URL pointing to an XML file in the Google Maps format and it will use it. (There's a ten second delay before display at present to allow for retrieval time, which is rough but hopefully does the job until I improve it.)

Sample files include:

The last example is interesting because it doesn't work properly. :-) It supplys a non-Google .xsl file to format the info which XmlHttpRequest currently chokes on but I'm working on that...

The loadxml-0.0.1.js script uses a (extremely rough) Python CGI script (thanks TravisBSD.org!) to retrieve the file, bypassing the lovely JavaScript same-domain security protection. I've tried to minimise its exploitability but if you've got any thoughts on it feel free to let me know (by email, rather than exploit!).

#!/usr/bin/env python
#
#   getfile.cgi
#
import urllib2

import cgi


form = cgi.FieldStorage()

url = form.getfirst("url","")

data = ""

# TODO: Make more robust
if url.startswith("http://"):
    data = urllib2.urlopen(url).read()

    if not data.startswith('<?xml version="1.0"?>'):
        data = ""

print 'Content-Type: text/javascript'
print
print """\
function getData () {
    return '%s';
}
""" % (data.replace("'", "\'").replace("\n","")) # TODO: Ensure robust.

Update: I have modified the loadxml script to use the new event-based loading method which results in better performance and reliability. Inject the following script as above:

http://libgmail.sourceforge.net/loadxml-0.0.2.js

Part 8 : Modifying Google Maps appearance with custom XSL files

The following will make custom XSL files available to Google Maps:

   
Of course, I don't really know squat about XSL so I'm sure more impressive things are possible...

How's it done? Well, I essentially implemented an extended XmlHttpRequest object that allows off-domain file retrieval and got Gmaps to use that instead of the normal one. (I just hand off same-domain calls to the usual object and use script tag injection to handle the off-domain calls.)

The code's a mess but it works for me under Firefox. There's some XML parsing code I found somewhere which probably won't work under IE at the moment.

Part 9 : Real time information display

This is the beginning of an investigation into display of (near) real time information on Google Maps with the addition of custom content and appearance.

The information used for this research is from the Seattle Fire Department's Real Time 911 information page. A script parse911.cgi retrieves the page on request then currently performs a Google Maps search on the first ten (most recent) locations listed and aggregates the results. It also trys to locate the viewing area so all markers can be seen at once--this isn't perfect at the moment, however. It seems the data retrieved for any particular location is not always accurate, so bear that in mind if you think you see your dog's house burning down.

There's additional information available at the site such as the incident type, and the number and type of vehicles present--which suggests some interesting possibilities... :-)

Update: Upload http://stuff.rancidbacon.com/parse911-0.2.0.cgi to see both custom XML and XSL files in action (still requires a manual refresh at present). Also, see the end of Part 11 for a tip for helping with custom XSL development.

Part 10 : Google Maps Classes and Functions Reference

First rough draft of Google Maps Classes and Functions Reference. [Updated to maps.3.js]

Part 11 : Beta be more robust

The recent change to maps.3.js broke a few pieces of code on this page so I've been thinking of how to make things a bit more robust. For reasons of compression and/or obfuscation Gmaps mostly uses one or two letter identifiers for functions, classes and variables in its JavaScript code. In order to make these hacks more robust against code changes I've developed a utility function to determine the current identifier for a particular function or class.

By adapting the technique used to update the generation of the Classes and Functions Reference in Part 10 we seek to identify a unique signature for a desired function or class and use that rather than the actual identifier used in any specific code version. This is all enabled by JavaScript's introspection capabilities.

Consider the XmlHttpRequestFactory whose create method needs to be replaced to enable off-domain file retrieval. In maps.2.js it was called Yd but it is known more colloquially as cg in maps.3.js:

Extract from maps.2.jsExtract from maps.3.js
function Yd() {}

Yd.create=function() {
  if(m.type==1) {
     var Vd=m.version==5?"Microsoft.XMLHTTP":"Msxml2.XMLHTTP";
     return new ActiveXObject(Vd)
  } else {
    return new XMLHttpRequest()
  }
}
function cg() {}

cg.create=function() {
  if(typeof ActiveXObject!="undefined") {
     return new ActiveXObject("Microsoft.XMLHTTP")
  } else if(typeof XMLHttpRequest!="undefined") {
     return new XMLHttpRequest()
  } else { 
    ;return null
  }
}

By comparing the two versions and other definitions in the scripts we can determine that if we have a function declaration with a create method that in turn contains the text XMLHttpRequest we can be reasonably certain it refers to XmlHttpRequestFactory. All we need is a helper to find it for us automatically. Hey, here's one now (this example is from http://libgmail.sf.net/loadxml-0.0.5.js):

_XmlHttpRequestFactory = deObf("function", "create", "XMLHttpRequest");

function deObf(targetObjType,targetObjMethod,targetObjMethodSourceSnippet){
  for(objName in this){
    obj=this[objName];
    if(typeof(obj)==targetObjType){
      if(targetObjMethod!=""){
        if(typeof(obj[targetObjMethod])=="function"){
          objMethod=obj[targetObjMethod]
        }
        else
          if((typeof(obj["prototype"])!="undefined")&&(typeof(obj["prototype"][targetObjMethod])=="function")){
            objMethod=obj["prototype"][targetObjMethod]
          }
        else{
          continue;
        }
      }
      else{
        objMethod=obj;
      }
      if(objMethod.toString().indexOf(targetObjMethodSourceSnippet)>0){
        return obj;
      }
    }
  }
}

In general the first argument will be "function" or "object", the second argument will be a method name or blank if the target is only a function and the third argument will be a code snippet. This approach is probably too verbose for bookmarks--even when compressed--but it seems viable for longer scripts.

Here are two more potentially useful examples:

_parseXmlFromString = deObf("function","", "parseFromString")

_Xslt = deObf("function", "asynchronousTransform", "transformToHTML")
Tip: The object referred to by _Xslt includes a cache for the XSL style-sheets used to format the pop-up and side panels. The caching functionality can be unwanted when developing or debugging style-sheets so use the following to clear the cache and force a reload from the URL:
_Xslt.cache_ = new Object()

Introspection references

For details on how functions are part of the global JavaScript namespace and this can be accessed as an array to retrieve the definitions consult these JavaScript introspection references:

Oh yeah, I just have to say again: the JavaScript Shell Bookmarklet is fantastic.

Part 12 : Whoops, how did that happen..?

Okay, this is just a screenshot at the moment, but here it is:
The above is a screenshot of a web page with an iframe that contains a page from another domain which is showing a totally interactive Gmaps view with custom data and stylesheets. This is a proof-of-concept for a "service" that could be supplied the URL of a data file and style-sheet and display the interactive map.

There are no bookmarklets or other browser hacks involved and the original Gmaps maps.3.js Javascript file is used unmodified.

Update: Check out the unauthorised Google Maps Standalone Mode. I have no idea how long it'll be around for... :-)

Part 13 : An unofficial official Gmaps API?

I've just noticed something kinda interesting in the last two updates (4 & 5) to the Google Maps Javascript files (maps.X.js) and thought I'd indulge in some idle speculation. (Incidentally, it appears the "Local" search option uses a different Javascript file http://www.google.com/mapfiles/maps.local.1.js.)

At the bottom of the Javascript we see the following:

var _Point=q;
var _Map=c;
var _IconClass=L;
var _XMLHttp=we;
var _MapsApplication=ga;
Now, tell me, can you see where in Google's code or HTML page those names are referenced?

Future

Now all I have to do now is come up with some totally gratuitous way to combine Gmail & Gmaps via libgmail and a bookmarklet and I'll be all set... :-)

Tools/References

Other resources

Feedback/Discussion

Feel free to post comments in this thread discussing Google Maps hacking.

Consider adding content to the Google Maps hacking wiki.

Alternatively, I can be contacted at the address follower at myrealbox.com if you prefer.

Hi Lynda! :-)