cash loans
25 September 2009

Working with AMFPHP and the Google Maps Flash API

We’ve been working a lot with the Google Maps Flash API lately; recent projects Ride Oregon and Oregon Bounty make regular use of Google Maps to plot various types of data. Each project raised unique challenges, the solutions to which were rarely found in Google’s documentation.

A note about setup: for data storage, retrieval, and manipulation, we use AMFPHP; for our local dev environment, MAMP and VirtualHostX, managed with a version control system. That said, here are are some tips for working with the Google Maps Flash SDK:

You need the Adobe Flash player to view this.

Creating Custom Markers

To easily create custom markers, extend the Marker class and pass a custom icon display into MarkerOptions from the constructor. This keeps everything contained in one object, and makes references to the custom icon a snap.

// extends Marker;
public var icon : CustomIcon;
public function CustomMarker ( latLng : LatLng )
{
  icon = new CustomIcon();
  icon.latLng = latLng;

  super( latLng, new MarkerOptions( { icon : icon } ) );
}

Tracking Markers Outside the Viewport

Tracking markers when they go offstage can be a pain, fortunately there are some useful libraries written to help with that kind of thing. I’ve also included our custom implementation (GhostTracker.as) as a part of the source code for this post.

Panning Markers Back Into the Viewport

When a marker is offstage and is selected by the user, the map can be centered or panned to move the marker detail into view. Centering the map with setCenter can be a jarring user experience, since opening any marker will pan the map view; an alternate approach is to use the panBy function to accomplish the same thing with only the minimal necessary movement:

var pt : Point = map.fromLatLngToViewport( latLng );
var np : Point = new Point();
var edgePadding : Number = 10;
var center : Number = _detail.width * 0.5 + edgePadding * 2;
var displayHeight : Number = _detail.height + edgePadding;
var mapRect : Rectangle = new Rectangle( 0, 0, map.width, map.height );

// if x is less than left boundary ( data sort window right edge );
if ( pt.x - center < mapRect.x ) np.x = ( pt.x - center ) - mapRect.x; // if x is greater than right edge; if ( pt.x + center > mapRect.right - edgePadding ) np.x = ( pt.x + center ) - ( mapRect.right - edgePadding );

// if y is greater than map height - padding;
if ( pt.y > mapRect.bottom - edgePadding ) np.y = pt.y - ( mapRect.bottom - edgePadding );

// if y - height is less than top edge;
if ( pt.y - displayHeight < edgePadding * 2 ) np.y = ( pt.y - displayHeight ) - edgePadding * 2;

// position map so that full display area is visible;
map.panBy( np );

Centering on a Group of Markers

When working with an array of markers that need to be displayed together within the viewport, a LatLngBounds object can be extended to include all LatLng points. From there, it’s just a matter of setting the center of the map to the center of the bounds area.

var bounds : LatLngBounds = new LatLngBounds();
var marker : Marker;
var latLng : LatLng;
var maxZoom : Number = 13;
var t : int = markers.length;
for ( var i : int = 0; i < t; i++ )
{
  marker = markers[ i ] as Marker;

  latLng = marker.getLatLng();

  if ( latLng.lat() != 0 && latLng.lng() != 0 ) bounds.extend( latLng );
}

if ( !bounds.isEmpty() ) map.setCenter( bounds.getCenter(), Math.min( maxZoom, map.getBoundsZoomLevel( bounds ) ) );

Getting Elevation Data

Elevation data for routes and markers can be retrieved and stored efficiently via PHP, or at runtime in AS3, using a couple of different sources.

// PHP with vars $lat & $lng (returns value in meters);
$primary = "http://ws.geonames.org/srtm3?lat=$lat&lng=$lng";
$secondary = "http://gisdata.usgs.gov/xmlwebservices2/elevation_service.asmx/getElevation?X_Value=$lng&Y_Value=$lat&Elevation_Units=METERS&Source_Layer=-1&Elevation_Only=TRUE";

Plotting Long Routes

When plotting routes on custom maps in Ride Oregon, we often had to exceed the 25 point max in the loadFromWaypoints function of the Directions class. The solution was simply to queue multiple requests, with the last point from the previous request as the first point in the new one.

// assumes class variable _waypoints is an array containing stored points to get directions for;
var max : Number = 25;
var tot : Number = _waypoints.length;
var req : Number = Math.ceil( _waypoints.length / max )
var per : Number = Math.min( max, tot );
var first : Marker;

// split directions into multiple requests;
for ( var j : int = 0; j < req; j++ )
{
  // store latLng points;
  var waypoints : Array = [];

  // store first recorded point as first point in new query;
  if ( first ) waypoints.push( first.getLatLng() );

  per = Math.min( max, tot );

  // create series of directions with max or fewer waypoints;
  for ( var k : int = 0; k < per; k++ )
  {
    var w : Marker = _waypoints[ k + j * max ];

    waypoints.push( w.getLatLng() );
  }

  // reduce total;
  tot -= max;

  // store first point;
  first = w;

  // break if invalid request;
  if ( waypoints.length <= 1 ) break;

  // set up new directions instance;
  var directions : Directions = new Directions( new DirectionsOptions( { avoidHighways : true } ) );
  directions.addEventListener( DirectionsEvent.DIRECTIONS_SUCCESS, onDirectionsSuccess );
  directions.addEventListener( DirectionsEvent.DIRECTIONS_FAILURE, onDirectionsFailure );

  // get new set of directions;
  directions.loadFromWaypoints( waypoints );
}

Listen for a response DirectionsEvent to be dispatched:

function onDirectionsSuccess ( e : DirectionsEvent ) : void
{
  // push e.directions to array of direction requests;
  // add to e.directions.distance to total route distance;
  // add e.directions.createPolyline to map;
}

function onDirectionsFailure ( e : DirectionsEvent ) : void
{
  // failed;
}

Saving an Image of the Map

In order to create no-flash versions of all the maps, we combined the display layer of the map, with Google’s Static Map API. Initially, we went this route because there was no other option (Google had banned access to their images via BitmapData.draw, and it would throw a SecurityError), but they’ve since removed the restriction and added a getPrintableBitmap function to the API. Still, if you load content into your icons from another domain, or just don’t want to deal with crossdomain policy files, layering the Static API is a decent solution.

// resolves Error #2121: Security sandbox violation: BitmapData.draw
function saveMapImage () : void
{
  var rect : Rectangle = new Rectangle( 0, 0, Math.min( map.width, 640 ), Math.min( map.height, 640 ) );
  var bmd : BitmapData = new BitmapData( rect.width, rect.height, true, 0x00000000 );
  var mapDisplay : Sprite = Sprite( Sprite( Sprite( map.getDisplayObject() ).getChildAt( 1 ) ).getChildAt( 2 ) );

  // draw custom overlay;
  bmd.draw( mapDisplay, null, null, null, rect, true );

  // encode ByteArray and send to server...
}

Custom AMFPHP Services

We’ve put together an abstract set of AMFService and AMFServiceEvent classes to work with AMFPHP. This way you can write a service in PHP, and call its methods directly on an instance of the AMFService class in ActionScript. There are plenty of features that could be added to this implementation (service method and parameter checking, for instance), but for most cases its a clean and flexible approach. If you have a method called getCustomData defined in CustomService.php, setting up an interface is simple:

var customService : AMFService = new AMFService( 'CustomService', '/amfphp/gateway.php' );
customService.addEventListener( AMFServiceEvent.RETURN, onServiceData );
customService.getCustomData();

function onServiceData ( e : AMFServiceEvent ) : void
{
  trace( e.data );
}

Hopefully a few of these techniques save some headaches when working with Google Maps in Flash. Feel free to post a comment with your own solutions.

posted by Shaun Tinney

thinking about… AS3, Flash, PHP

Have a comment?