Overlaying markers using Google Maps API on an arbitrary image
December 5, 2008
So, my previous post from Inventoriana land looked at how you could use Google Maps as a basic navigator for the image of your choosing using what is called a Tile Layer Base. All that is nice and good, but for Inventoriana to really work, it has to be able to munge an XML feed and then, using (x,y) coordinates that refer to the original image, correctly place them on the google map. It sounds easier than it is, but the solution is not terribly difficult either. Here’s what you need to do.
Step 1: Get an XML Feed
For this example, we’ll use this one:

Code
<comment>
<id>181</id>
<folio_id>1962</folio_id>
<title>a</title>
<notes>b</notes>
<x>147</x>
<y>122</y>
<uniform_order>0</uniform_order>
<created>2008-01-02 20:05:59</created>
<updated>2008-08-21 14:31:11</updated>
<status>5</status>
<user>massey</user>
</comment>
<comment>
<id>250</id>
<folio_id>1962</folio_id>
<title>where did I go wrong?</title>
<notes>asdf</notes>
<x>311</x>
<y>95</y>
<uniform_order>0</uniform_order>
<created>2008-08-21 13:30:56</created>
<updated>2008-08-21 13:31:22</updated>
<status>5</status>
<user>massey</user>
</comment>
And we want to put it on this image:
Inventoriana Test Image
Easy enough, right?
Step 2: Define a map projection.
This is probably the most conceptually difficult part. Since Google Maps assumes that you are mapping part of the earth, there are two sets of coordinates being tracked: your current pixels, and also the latitude and longitude. Typically, people use mercator projections with Google maps, and, if you aren’t trying to bring in an overlay from a 2-D data source, you’ll be fine with that. However, translating (x,y) into (lat,lng) is harder than it sounds, not least because ActionScript 3.0 has not-mathematica-level trig functionality.
So the way to really go is to define your own map projection, which is linear in every direction. Here is the code:

Code
1
package inventoriana
{
2
3
import com.google.maps.*;
4
import flash.geom.Point;
5
// Our goal here is to define functions which will move between the (x,y)
6
// coordinates of the existing inventoriana persistent layer
7
// and the LatLng paradigm of google maps.
8
9
// Basically we are cheating and creating a totally linear projection,
10
// which is of course nonsense for the real earth but handy if one
11
// is using Google Maps to peruse 2d items…
12
13
public class InventorianaProjectionBase extends ProjectionBase
{
14
15
public var originalDimensions:Point; // Dimensions of Original Image.
16
17
// Right now originalDimensions is not implemented.
18
public function InventorianaProjectionBase(dimensions:Point)
{
19
originalDimensions = dimensions;
20
super();
21
}
22
23
public override function fromLatLngToPixel(latLng:LatLng,zoom:Number):Point
{
24
// Basically reverse the progress from below.
25
26
var lng_slide = latLng.lng() / 90;
27
var x_converted = lng_slide * Math.pow(2,zoom-1) * 256;
28
var lat_slide = latLng.lat() / 90;
29
var y_converted = lat_slide * Math.pow(2,zoom-1) * 256;
30
return new Point(x_converted,y_converted);
31
}
32
public override function fromPixelToLatLng(pixel:Point,zoom:Number,opt_nowrap:Boolean = true):LatLng
{
33
34
var x_slide:Number = pixel.x / (Math.pow(2,zoom-1) * 256); // returns a value between 0 and 1, in proportion to where we are.
35
var lng = 90 * x_slide ; // * -180;
36
var y_slide:Number = pixel.y / (Math.pow(2,zoom-1) * 256); // Ditto for y.
37
var lat = 90 * y_slide ; // * 85;
38
return new LatLng(lat,lng);
39
}
40
41
public override function getWrapWidth(zoom:Number):Number
{
42
// This is reasonable although not necessary.
43
// Too high a zoom and users might get lost;
44
// Too low and they will get smashed together;.
45
return Math.pow(2,zoom-1) * 256 * 4;
46
}
47
48
public override function tileCheckRange(tile:Point,zoom:Number,tileSize:Number):Boolean
{
49
// Obviously want something a little more finely honed in the end.
50
// What we really want is to make sure that there is a tile
51
return true;
52
}
53
}
54
55
}
The main meat of the class are the two functions fromLatLngToPixel() and fromPixelToLatLng(). They give google maps the tools they need to correctly place everything on the map. You’ll notice that we are essentially only using latitude and longitude values between 0 and 90; since images are cut in to squares by GMap Cutter (see previous post) this is still ok.
So now you have a projection that is entirely linear, which would be terribly irritating for the earth but handy for using images.
Step 3: Make your map use the projection.
Now that you have your map projection you can update your map subclass as follows:
First, make a ProjectionBase property (the numbers are the dimensions of the original image):
public var inventorianaProjectionBase:InventorianaProjectionBase = new InventorianaProjectionBase(new Point(670,670));
Then, after the map object has dispatched the MAP_READY event, set the Map Type to use the projection:
var inventorianaMapType:MapType = new MapType(tileLayers,inventorianaProjectionBase,”Inventoriana”);
Finally, once your XML data source has dispatched a “ready” event, you can add the overlay:

Code
private function _addMarkers(event:Event):void {
var imgWidth = 670;
var imgHeight = 670;
var zoom:Number = getZoom();
var multiplier:Number = Math.pow(2,zoom-1) * 256 / Math.max(imgWidth,imgHeight);
for each (var comment:XML in folioInfo.xmlList.comment) {
var newCoords:LatLng = inventorianaProjectionBase.fromPixelToLatLng(new Point(comment.x * multiplier,comment.y * multiplier),2);
var marker:InventorianaMarker = new InventorianaMarker(newCoords,comment);
addOverlay(marker);
}
}
Your final result should look something like this (it is just a screenshot because I am too lazy to upload my tile base to a publicly accessible location):