|
This material discusses the use of the Google Map API, in conjunction
with HTML, PHP, and mySQL, to generate a page displaying
the locations of members belonging to Programming Forums.
I anticipate that the final product will employ "hacks" that connect it to the member database of forum/BB software and use the information normally found on a user's profile. Currently, the information is contained in an independent database. Get the files. |
A Google Map Project
The Google Maps API
The Google Maps API lets you embed a Google map in your application. It is a collection of script objects possessing various properties and methods for their manipulation. The code is included in your page in the same way that you would include any external .js file.
You must have a key as part of the link to the code. You will need a different key for each URI you plan to use. It is effective for all subdirectories of the URI to which it applies. I mention this because the FTP address for my site differs from my actual domain name. My programming tool constructs the www address from the ftp address when it invokes the browser. Rather than dink with the URI each time, I obtained two keys -- one for daweidesigns.netfirms.com and another for www.daweidesigns.com.
At first glance, the documentation looks like very good descriptive material,
but less than a comprehensive
API specification.
"Application Programming Interface". An API is an interface that enables one program to use facilities provided by another.
I based some of my early usage on inferences I drew from the information.
That was truly unnecessary. The things that you can do are covered,
but you may have to look in a couple of different places.
The material does not cover the actual design of the object, of course.
A curious person such as I would
obviously like to see that, but the objects, their properties, and the
methods necessary for their use are exposed quite adequately. If you're
really curious, you may call a Google function with invalid parameters,
trigger an error, and use your debugger or Javascript Console to pull
up the code and have a look.
This project has two very distinct pieces — the map, and an information area. I chose to place them horizontally, side by side. Let's put a map on the page.
| Copyright © 2005 David Mills | Contact Me |
Placing a Map
A map requires but two things: a map object and a container for it. I define my container as follows:
<div id="map" style=" width: 600px; height: 400px;
border: 1px solid; padding: 10px;">
</div>
Note that the container is empty. It contains some preliminary dimensional information, but that information is subject to dynamic adjustment according to presentational considerations. One instantiates a map object and places it in a single operation, as follows:
var map = new GMap (document.getElementById ("map"));
This yields a bare-bones map inside the specified container. One may use additional operations to center and zoom it as desired, and one may place control objects and overlays on the map. I use global variables for the default latitude and longitude and the default zoom level. I also place a zoom control and a mode selection control (map, satellite, or hybrid).
My "placeMap" function looks like this:
function placeMap (map, lat, lng, zoom)
{
map.setMapType (G_HYBRID_TYPE);
map.addControl (new GLargeMapControl());
map.addControl (new GMapTypeControl());
map.centerAndZoom (new GPoint (lng, lat), zoom);
}
It is invoked, using the global defaults, by
placeMap (map, latDefault, lngDefault, zoomDefault);
In addition to the map and the controls, Google has a built-in response to a double-click which pans the map to the point of the click. At this point one has a fully functional mapping program for the Earth, subject only to the constraints inherent in the images Google has available for any given region and zoom level.
You will notice that, at the wider zoom levels, the map presents more than 360 degrees of the Earth’s surface. This is also true on the actual Google Maps site. In these situations, Google simply tiles the map pieces. They do not scale the coordinates or resize the images.
If one places a marker at a replica point, using Google's returned coordinates, Google places it there. I have added code to place the marker within the correct 360 degree range (-180 to +180) regardless of the point of the actual click. I also constrain the size of the map to 360 degrees at the default zoom level. This results in a smaller-than-desired container at higher resolutions, but seems to be a superior approach to presentation.
I use the API for some additional functions. They concern the interaction of the map with the information being displayed and will be covered later.
The Google Maps API
The Information Area
| Copyright © 2005 David Mills | Contact Me |
The Information Area
I must admit that I acted contrary to my own advice in writing this little application. I did not design it first. In my enthusiasm to get a map up with some pins on it, I just began coding. Many of you will recall seeing the information area grow as time went by. The results support my usual advice, not my actions. Functions expanded willy-nilly as I added more capability. In some instances, I reorganized and rewrote. Doing things over is, of course, a waste of effort. Furthermore, the outcome isn't as elegant as it might be had I thought it out in advance.
The information area, as it stands at the time of this writing, contains the user's name and user-name, some biographical material, a link to the user's avatar, a flag indicating a second (alternate) entry, and the user's location in longitudinal and latitudinal coordinates. There are controls for centering on a location or user, for generating or changing a user's information, for setting a zoom level during centering, and for resetting the map to its original state. The current location of the map center is also shown. I added much of this in piecemeal fashion.
Originally, I stored the member information in a XML
XML (Extensible Markup Language) is a W3C initiative that allows information to be encoded with a meaningful structure and semantics. It is easily read and comprehended by humans and easily parsed by computers.
file and accessed it
using separate http requests
When you type a URL into a web browser, it sends a request for the item named by that URL to the server. The browser (or any client) may send multiple requests, involving multiple connections. There is no requirement that a browser render all its responses as documents. One common use of the technique is in the loading of images. One could, for instance, make requests for database information and use the results to modify the contents of an existing page dynamically, on the client side.
. It occurred to me that an application such as this
might be better implemented if it had access to an existing data set, such
as a forum member's profile. I reconsidered my approach and implemented
a mySql database for the member information.
All of the information to be stored in the database is contained in a form. Clicking the "Map Me" button submits the form. The form is contained in a div whose primary purpose is to force the information area to float to the right of the map container. The div also contains the zoom selector, the member selector, and the coordinates of the current center point. These latter are not part of the form, and are not submitted when the "Map Me" button is clicked.
Placing a Map
Firing the Sucker Up
| Copyright © 2005 David Mills | Contact Me |
Firing the Sucker Up
The application is a three-tier thangy – the client side, the middle
tier (the server side) and the database tier (mySql). I drive the map from
the middle tier with a PHP script. HTTP is, of course, a stateless protocol.
Since I only need to be aware of two states, initial contact and forms submission,
I do not resort to the use of sessions
Session support in PHP consists of a way to preserve certain data across subsequent accesses. This enables you to build more customized applications and increase the appeal of your web site. — PHP manual
. The presence or absence of form content
is sufficient to define the request type.
When an initial request occurs, I query the database and build a JS statement that will declare a two-dimensional member’s table on the client side, complete with content. I echo this content into the page, between script tags, and voilà, I have material to query and display without resorting to trips to the server to fetch new member information.
Many purists and high priests will denigrate this approach and proclaim that client-side script is evil. I’m sorry, Bubba, but The Google provides the Map API in client-side script. If you want to disable that, you’d better hike ‘em down to the Texaco station and buy one of them thar paper thangys. Since it is a requisite, I will take advantage of it to reduce bandwidth requirements and server and infrastructure load.
In addition to building a client-side table, I build a client-side message variable. This allows me to pass error messages and advisories to the client, who may then present them with message boxes or alerts. They aren’t just dumped unilaterally into the page at some ugly and inappropriate spot.
The member-selection widget adapts to the contents of the member database. Since it is fixed for any given request, however, I construct it on the server side and echo it into the page at the appropriate spot, rather than generate it on the client side with DOM manipulations. All interactions with it (member selection, centering, and zooming) are handled on the client side.
If the request is a forms submittal, I query the database to see if the member exists. The username is the key. If the member exists, I update the record. If the member doesn't exist, I insert the new material as a new record. Subsequent actions are the same as for an initial request.
I build the page sections (map and information area) as described previously,
and place markers for the members represented in the table. At that point,
I’m done. I go outside for a smoke and listen for someone to yell my
name. Everything else is event driven
"Event driven" is not just a programming paradigm; if one applies it properly, one may successfully manage projects or companies using the technique. In this instance, though, it means that, unlike traditional programs, which follow control flow patterns, changing course at branch points, the control flow of event-driven programs is largely driven by external events. The use of the term, "largely," is germane.
.
The Information Area
Placing Markers
| Copyright © 2005 David Mills | Contact Me |
Placing Markers
A marker, or, more precisely, a GMarker, is an object in the API's repetoire. It is one of a small collection of objects called "overlays." The marker constructor takes as arguments a point and an icon. It exports a few events, one of which is a click. Note that a click on a marker generates two events: a marker click and a map click. Because the map click provides arguments which can be examined to determine if the click is on a marker, or just on a map point, I only use the map click event and make the appropriate behavior determination there.
The icon argument is optional; if it is not supplied, the default Google icon
is used. An icon can be a rather complex object, with transparency definitions,
clickable sections, and much more. I replaced the standard icon with one
bearing the PFO logo
, but I took the simplest way out: it consists of the
marker (with transparent surroundings) and a shadow. More complicated things,
such as polygons defining clickable areas, I leave as an exercise for the
reader. I also made a smaller marker
for alternate locations for the same
member. Any pixel in the image may be specified as the anchor point (the
pixel placed at the marker position). For my markers, this is the bottom
center pixel, which is the "point" of
the marker. The icons are set up as follows:
function setIcon (mainI, altI)
{
mainI.image = 'http://www.daweidesigns.com/images/marker.png';
mainI.iconSize = new GSize(20, 34);
mainI.shadow = 'http://www.daweidesigns.com/images/shadow.png';
mainI.shadowSize = new GSize(37, 34);
mainI.iconAnchor = new GPoint(10, 34);
altI.image = 'http://www.daweidesigns.com/images/altIcon.png';
altI.iconSize = new GSize(12, 20);
altI.shadow = 'http://www.daweidesigns.com/images/altShadow.png';
altI.shadowSize = new GSize(22, 20);
altI.iconAnchor = new GPoint(6, 20);
}
The markers are placed as part of the "initMap ()" function. A simple for loop iterates through the members array and places a marker at each set of coordinates.
for (i = 0; i < nRows; i++)
{
// The user's coordinates
var point = new GPoint (parseFloat (userData [i][4]),
parseFloat (userData [i][3]));
// User's primary or alternate location
if (userData [i][7] == 1) marker = new GMarker (point, altIcon);
else marker = new GMarker (point, icon);
// Place the marker
map.addOverlay (marker);
// Add it to the markers array (serves no purpose at present)
memberMarkers [memberMarkers.length] = marker;
}
One may tie another type of overlay, an "infowindow," to the location of an existing marker. That's how I display the welcome message and the user avatars.
Firing the Sucker Up
Events & Listeners
| Copyright © 2005 David Mills | Contact Me |
Events & Listeners
I pay attention to only three of the events that the API exposes: zoom, moveend, and click. My only use of the zoom event is to set the value of the zoom selector in the information area. I always synchronize that value with the Google zoom control attached to the map.
I use the moveend event to set the Center coordinates located at the bottom of the information area.
I use the click in various ways. If a click occurs on a marker, I check to see if it’s a member marker or a temporary marker. If it is a temporary marker, I remove it. Done. If it’s a member marker, I call the “displayUserByCoordinate” function, which locates a member in the table by coordinates and moves the member’s information into the information area. There is also a “displayUserByUsername” function which locates and displays member information according to the username field. Internally, these functions determine a member's position in the database as a numerical index and call another function, “displayUserByIndex”. If it is not a marker, I remove any temporary marker that may exist and place a temporary marker at the point of the click.
I do not perform these functions directly with the event listener function. Google has its own response to a double-click, which is to pan the map such that the point of the click is centered. When that happens, I don’t want to place a temporary marker; I just want to leave Google alone to do their thang. Since they do not expose a double-click event for me to work with, I wrote code to detect multiple clicks and ignore them. Obviously, that involves an interval definition to distinguish between a double-click and two rapid single-clicks. Most users get to set their own definition when they set up their mouse. No value I choose can be perfect. I use 200 milliseconds; it works most of the time. You can fool it if you want to; I don’t care. So I place a marker, big deal; I also probably immediately remove it.
To get put on the mailing list for a map event one simply calls the “addListener” method of the GEvent object, passing as arguments the map object in question, the name of the event, and the name of a function to process the event. It looks like this:
GEvent.addListener (mapObject, eventName, functionName);
One may write a function and tack the name in there, or one may write the function directly into the argument location. It's a JS thangy that is sometimes neat. Since I don't want to expose the function to being called by my own code, but only by the Google object, I drop it right in there.
Here is the addListeners code:
function addListeners ()
{
GEvent.addListener (map, "moveend", function()
{
var center = map.getCenterLatLng ();
var latLngStr = '(' + center.x.toFixed (6) +
', ' + center.y.toFixed (6) + ')';
document.getElementById ("currentPos").innerHTML = latLngStr;
});
GEvent.addListener (map, "zoom", function(oldZoom, newZoom)
{
document.getElementById ("zoomer").value = newZoom;
});
GEvent.addListener (map, 'click', function (overlay, point)
{
if (nClicks == 0)
{
gOverlay = overlay;
gPoint = point;
setTimeout (handleClick, 200);
}
nClicks++;
});
}
Notice that I put the overlay and point aruguments into a couple of global variables where they can be picked up by the handleClick function and processed. That's tacky. You shouldn't do it that way. You can pass the arguments directly to the handleClick function by adding them after the delay argument in the call to setTimeout. When I did the MyBad, I immediately jumped into my nun's habit and slapped my own wrist with the ruler. I expect you to do the same under such circumstances.
This is the handleClick function:
function handleClick ()
{
if (nClicks == 1)
{
var pLng = document.getElementById ('lng');
var pLat = document.getElementById ('lat');
var pUser = document.getElementById ('user');
var aLng = parseFloat (pLng.value);
if (aLng > 180) aLng -= 360;
else if (aLng < 180) aLng += 360;
if (!isNaN (aLng)) pLng.value = aLng;
if (gOverlay)
{
// Look to see if it's a member marker
for (i = 0; i < memberMarkers.length; i++)
if (memberMarkers [i] == gOverlay) break;
if (i >= memberMarkers.length)
{
// Not a member
map.removeOverlay (gOverlay);
pLng.value = "";
pLat.value = "";
status ['lng'] = false;
status ['lat'] = false;
}
else
{
// Member marker
gPoint = new GPoint (gOverlay.point.x, gOverlay.point.y);
displayUserByCoordinate (gPoint);
}
}
else if (gPoint)
{
var nLng = parseFloat (gPoint.x);
if (nLng > 180) nLng -= 360;
else if (nLng < -180) nLng += 360;
gPoint.x = nLng;
if (locateMarker) map.removeOverlay (locateMarker);
var mk = new GMarker (gPoint, icon);
pLng.value = gPoint.x.toFixed (7);
pLat.value = gPoint.y.toFixed (7);
status ['lng'] = true;
status ['lat'] = true;
map.addOverlay (mk);
locateMarker = mk;
}
}
nClicks = 0;
}
Placing Markers
Information Windows
| Copyright © 2005 David Mills | Contact Me |
Information Windows
I mentioned previously that there is a type of overlay called an infoWindow. These may be placed anywhere on the map (sometimes Google will pan the map to keep them from clipping) and contain any kind of information you can express in HTML. I use them for the "welcome" greeting and to place user avatars when a user is selected and centered. The technique used is to generate a GPoint object describing where you want the window, generate a DOM element (I use a div) to contain the information, place the information in the DOM element, then call the map's "openInfoWindow" method, passing the point and the element objects as arguments. This is how I place the user's avatar:
function placeAvatar (theAv)
{
map.closeInfoWindow ();
welcomeOverlay = false;
var av = new GPoint (document.getElementById ('lng').value,
document.getElementById ('lat').value);
var t = document.createElement ('div');
if (theAv != "")
{
t.innerHTML = '<img src="' + theAv + '" border="5"/>';
}else{
t.innerHTML = "<p><b>No Avatar on File</b></p>";
}
map.openInfoWindow (av, t);
}
Events & Listeners
| Copyright © 2005 David Mills | Credits | Contact Me |