MapQuest API: Location Plotting, Centering & Automatic Zooming using PHP and jQuery
// August 5th, 2010 // Programming, Web Development // David Stinemetze
*** Skip to Sample and Final Code ***
I recently implemented custom map/location functionality into a website for a client. Normally when I use maps on websites, I tend to use Google Maps and typically stick to the standard provided embed code. In this specific case, the map needed to be slightly more customized and for a few various reasons, I ended up selecting MapQuest instead. Essentially what was needed was a map that popped up in a floating div that could show any number of locations, based on data specified in an address field. Also the map needed to by default, pick a somewhat central location and proper zoom factor so all these locations would show up without any extra work on the user’s end. This turned out to be a little more challenging than I originally assumed. To get started, I signed up for a MapQuest application key. Upon retrieving the key, I create a file called show_map.php and place this in the code:
- <?php
- ?>
Breaking Up the Data
This map feature was an addition to a pre-existing site with custom functionality and a database larger than I really wanted to manipulate manually. The field that contained address data was just a generic text field. This single text field contained the addresses (and phone numbers) of all the different locations in different formats, much like this:- 300 Alamo Plaza, San Antonio, TX 78205 (210) 555-5555
- 17000 W Ih 10 San Antonio TX 78257 210-555-5556
- 1 Lone Star Pass – San Antonio, TX 78264
- 8210 Agora Parkway, Selma, TX 78148 (210)555-5557
- <?php
- // In this example I am hard-coding the address into the variable $address. In practice, you could
- // read it in as a $_GET argument, from a database call or from pretty much anywhere else you’d like.
- $address = "300 Alamo Plaza, San Antonio, TX 78205 (210) 555-5555
- 17000 W Ih 10 San Antonio TX 78257 210-555-5556
- 1 Lone Star Pass – San Antonio, TX 78264
- 8210 Agora Parkway, Selma, TX 78148 (210)555-5557";
- /*
- These next two lines deal with what system the data was entered on and whether carriage returns are
- treated as \r, \n or \r\n. Essentially I’m forcing any instance of \r to become \n which could potentially
- create a \n\n. I then convert any instance of \n\n to a single \n. Now I can split strings up based on \n.
- There may be easier ways of doing this. I just needed a quick solution and knew this would work. If you
- have a better solution for this let me know.
- */
- // Now I take my addresses and tokenize them into an array using \n as my delimiter.
- // Now we want to strip phone numbers from addresses and put them into a separate array in case I need it later
- for ($i=0; $i<sizeof($addresses); $i++) {
- /*
- Replace anything that matches the specified phone number types with nothing and store the phone
- number in a separate array. There’s a lot that goes into this regular expression. It worked in
- all the cases I had. May not work in every case. It’d probably take half the page to explain what
- each piece does, so instead I will just refer you to:
- http://www.webcheatsheet.com/php/regular_expressions.php – Regular Expression Cheat Sheat
- */
- if (preg_match(‘/\s*\({0,1}[0-9]{3}[)\-]{0,1}\s*[0-9]{3}\s*\-\s*[0-9]{4}\s*/’, $addresses[$i], $matches)) {
- $phones[$i] = $matches[0];
- } else {
- $phones[$i] = "";
- }
- }?>
- 300 Alamo Plaza, San Antonio, TX 78205
- 17000 W Ih 10 San Antonio TX 78257
- 1 Lone Star Pass – San Antonio, TX 78264
- 8210 Agora Parkway, Selma, TX 78148
Centering
The addresses I am dealing with tend to be located close to each other geographically, relative to the size of the earth. So instead of going through complex math formulas, I treat the locations as if they are on a flat 2-dimensional surface instead of the surface of a sphere. I imagined drawing a box around the upper and lower limits of the map like this:
Now I can just calculate the center point of this rectangle, and use that as my center point. If it’s slightly off, that’s okay as long as all the points show up. To actually do this I had to convert the above addresses into latitude and longitude, then take the minimum and maximum latitudes and longitudes, and replace them into the coordinates above:
- x1 = Minimum Latitude
- x2 = Maximum Latitude
- y1 = Minimum Longitude
- y2 = Maximum Longitude
- <?php
- // create curl resource for our connections
- $ch = curl_init();
- // set extreme values for the min/max lat/long.
- $lat_min = $lng_min = 999999;
- $lat_max = $lng_max = -999999;
- // create an array to store the coordinates for each location
- foreach ($addresses as $address) {
- // the URL for retrieving coordinates for given address
- $geocode_url = "http://www.mapquestapi.com/geocoding/v1/address?key=".MAPQUEST_API_KEY.
- // Retrieve the data as JSON.
- curl_setopt($ch, CURLOPT_URL, $geocode_url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
- $json = curl_exec($ch);
- // This example assumes retrieval worked properly. Feel free to add error detection to your code.
- // Mapquest wraps an extraneous renderOptions(); function around the JSON results that we need to get rid of.
- // decode our json data
- $return = json_decode($json);
- // focus on the results portion of the data
- $results = $return->results[0];
- // focus on the resulots->locations portion of the data.
- $locations = $results->locations[0];
- // determine if any of our coordinates meet any of the extreme conditions and reset extremes accordingly
- if ($locations->latLng->lat > $lat_max) $lat_max = $locations->latLng->lat;
- if ($locations->latLng->lat < $lat_min) $lat_min = $locations->latLng->lat;
- if ($locations->latLng->lng > $lng_max) $lng_max = $locations->latLng->lng;
- if ($locations->latLng->lng < $lng_min) $lng_min = $locations->latLng->lng;
- // add coordinate to positions array in a format that will play nicely with plotting later
- $positions[] = "lat:".$locations->latLng->lat . ", lng:" . $locations->latLng->lng;
- }
- // calculate the center latitude and longitude
- $center_lat = ($lat_min + $lat_max) / 2;
- $center_lng = ($lng_min + $lng_max) / 2;
- ?>
Calculating Zoom Factor
Next we need to calculate an appropriate zoom factor. No matter the zoom factor or size of the map, MapQuest’s kilometer scale is 77px wide. This means we can calculate the width of our entire display map in kilometers in addition to pixels. In theory this means, the zoom factor we are looking for, is the largest value that keeps the width/height of the entire map area, larger than the target rectangle.- <?php
- //specify our map dimensions and scale size in pixels
- // This array contains the number of kilometers, the standard scale on mapquest contains at each zoom factor.
- // This is an implementation of the Haversine Formula that calculates distance in Kilometers between
- // ($lat1,$lon1) and ($lat2,$lon2).
- function calculate_distance_km ($lat1, $lon1, $lat2, $lon2) {
- }
- // calculate the change in X and Y coordinates separately
- $x_delta = calculate_distance_km($lat_min, $lng_min, $lat_max, $lng_min);
- $y_delta = calculate_distance_km($lat_min, $lng_min, $lat_min, $lng_max);
- // calculate the best zoom factor on the X-axis
- // calculate the width of the total map in kilometers
- $total_width_km = MAP_WIDTH * $zoom_ranges[$bestX] / MAP_SCALE_IN_PIXELS;
- if ($total_width_km < $x_delta) break;
- }
- // calculate the best zoom factor on the Y-axis
- // calculate the height of the total map in kilometers
- $total_height_km = MAP_HEIGHT * $zoom_ranges[$bestY] / MAP_SCALE_IN_PIXELS;
- if ($total_height_km < $y_delta) break;
- }
- // we want to take the minimum value, because we don’t want to zoom in too far.
- ?>
Displaying the Map
Now that we’ve taken care of the calculations, it’s time to display the map.- <!DOCTYPE html>
- <head>
- <script src="http://mapquestapi.com/sdk/js/v6.0.0/mqa.toolkit.js?key=<?php echo MAPQUEST_API_KEY; ?>"></script>
- <script type="text/javascript">
- MQA.EventUtil.observe(window, ‘load’, function() {
- // create a new map in div#map using our calculated center and zoom factor
- window.map = new MQA.TileMap(
- document.getElementById(‘map’),
- ‘map’);
- // put a zoom control on the map
- MQA.withModule(‘zoomcontrol3′, function() {
- map.addControl(
- new MQA.LargeZoomControl3(),
- new MQA.MapCornerPlacement(MQA.MapCorner.TOP_LEFT)
- );
- });
- // set our locations
- basic.setBias({x:-5,y:-5});
- basic.setRolloverContent(‘<?php echo str_replace("’", "\\‘", $addresses[$i-1]); ?>’);
- map.addShape(basic);
- <?php } ?>
- });
- </script>
- </head>
- <body>
- <div style="width: <?php echo MAP_WIDTH; ?>px; margin: 5px auto; ">
- <!– this is our map container –>
- <div id="map" style="height:<?php echo MAP_HEIGHT; ?>px; width:<?php echo MAP_WIDTH; ?>px; border: 2px solid #000; "></div>
- <?php
- $num = 0;
- echo ‘<ol>’;
- foreach ($addresses as $fa) {
- $fa .= ‘ — ‘. $phones[$num];
- }
- $num++;
- }
- echo ‘</ol>’;
- ?>
- </div>
- </body>
- </html>
Separating the Zoom Control
Because we’re dealing with dynamic content, it’s possible the zoom control could get in the way of a location. Also in my opinion, it clutters the map. Using the jQuery UI Slider I can pull the zoom out of the map. To do so, the HTML above would change to:- <!DOCTYPE html>
- <head>
- <!– add in the necessary jQuery files –>
- <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery-ui.css" type="text/css" media="all" />
- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
- <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js" type="text/javascript"></script>
- <script src="http://mapquestapi.com/sdk/js/v6.0.0/mqa.toolkit.js?key=<?php echo MAPQUEST_API_KEY; ?>"></script>
- <script type="text/javascript">
- MQA.EventUtil.observe(window, ‘load’, function() {
- // create a new map in div#map using our calculated center and zoom factor
- window.map = new MQA.TileMap(
- document.getElementById(‘map’),
- ‘map’);
- // Remove the Old Zoom Control and Replace With this code to capture mouse wheel events
- MQA.withModule(‘zoomcontrol3′,‘mousewheel’, function() {
- map.enableMouseWheelZoom();
- });
- MQA.EventManager.addListener(map, ‘zoomend’, function(evt) {
- $("#map_zoom_slider").slider("option","value",evt.zoom);
- });
- // set our locations
- basic.setBias({x:-5,y:-5});
- basic.setRolloverContent(‘<?php echo str_replace("’", "\\‘", $addresses[$i-1]); ?>’);
- map.addShape(basic);
- <?php } ?>
- });
- // add jquery slider info
- $(function() {
- $("#map_zoom_slider").slider({
- range: "max",
- min: 1,
- max: 16,
- slide: function(event, ui) {
- map.setZoomLevel(ui.value);
- }
- });
- });
- </script>
- <body>
- <div style="width: <?php echo MAP_WIDTH; ?>px; margin: 5px auto; ">
- <!– this is our map container –>
- <div id="map" style="height:<?php echo MAP_HEIGHT; ?>px; width:<?php echo MAP_WIDTH; ?>px; border: 2px solid #000; "></div>
- <!– this is our slider container –>
- <div id="map_zoom_slider" style="margin: 5px auto 0 auto;"></div>
- <?php
- $num = 0;
- echo ‘<ol>’;
- foreach ($addresses as $fa) {
- $fa .= ‘ — ‘. $phones[$num];
- }
- $num++;
- }
- echo ‘</ol>’;
- ?>
- </div>
- </body>
- </html>
Making it Pop Up
The final piece, is making the data pop up. Just because I’m a fan of it, even though it’s deprecated, I chose to use ThickBox. After proper installation of thickbox, all I have to do is add a link to show_map.php from some other page.- <a class="thickbox" rel="nofollow"
- href="/show_map.php?KeepThis=true&TB_iframe=true&height=570&width=650">Map Link</a>
Want the source for the final show_map.php file? Download It
/* Facebook */

Nice piece of coding! I never used MapQuest but it’s certainly worth looking at once I need to use a map again.
I’ve used thickbox a couple of times and it’s a good script. Recently I moved over to Floatbox because it has a lot more options and is highly configurable. Use it to embed movies as well. Take a look at my blog to see how it works. The link to Floatbox is on the bottom of my pages. Good thing is it’s free for personal use.
Thank’s for the comments.
I’m eventually gonna stop using thickbox, given that it’s deprecated and all. I’ve just gotten quite comfortable with it, since it’s quick and easy. I’ve used it so many times that it’s practically second nature. Aesthetically speaking, I’m ready to find a new solution, but I just haven’t had the time to do the necessary research. Hopefully I’ll be able to do so soon.
You can also use the MQA.TileMap.bestFit() function instead of figuring out the zoom and center yourself. You can also use the MapInit object to set both before creating the map. It’s not documented yet, but here is a sample using the MapInit method.
http://mqdemo.com/brian/mapinit.html
Love the jQuery slider zoom control. Great idea.
Interesting. I’ll have to give it a shot. When I originally wrote this, I didn’t remember seeing the bestFit function anywhere as it probably would have saved me a bit of time. That’s okay though. It gave me some extra experience dealing with the API.
The jQuery zoom made so much more sense to me than the zoom that just manages to get in the way of the map. Plus you can style it however you want.
Thanks for the feedback!
Is there a way to have mapquest SORT the data it returns? “Nearest locations” first.
Nearest location, relative to what? In my example, I am doing a make shift central point calculation. If you want the nearest locations to the central point, which is something I calculate after the fact, any sorting would have to be done in my code.
If you’re looking for the closest location based on the user’s current position, that can be a bit tricky. If on a mobile device you can probably tie it into the GPS in the phone if it has one, otherwise you’re dependent on the IP address. IP address location isn’t an exact science. I’ve seen some instances of IP addesses that are physically located in San Antonio, TX be attributed to service providers in Austin, TX and Plano, TX. So the nearest to the user’s IP address is probably not the nearest to their actual location.
Now if you’re trying to sort the nearest from some fixed point, then there maybe something in the MapQuest API that allows you to do that. I honestly haven’t looked that far into it, so I’m not 100% sure.