MapQuest API: Location Plotting, Centering & Automatic Zooming using PHP and jQuery

// August 5th, 2010 // Programming, Web Development //

*** 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:

  1. <?php
  2. define(‘MAPQUEST_API_KEY’, ‘TheyKeyIGotFromMapQuest’);
  3. ?>

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:
  1. 300 Alamo Plaza, San Antonio, TX 78205   (210) 555-5555
  2. 17000 W Ih 10 San Antonio TX 78257   210-555-5556
  3. 1 Lone Star Pass – San Antonio, TX 78264
  4. 8210 Agora Parkway, Selma, TX 78148 (210)555-5557
In some cases commas were used. Other cases, they weren’t. Phone numbers are different formats. Fortunately for the most part it was just 1 per line. Also MapQuest is somewhat generous when it comes to address formats. So the only real problem I had with the data was stripping out the inconsistent phone formats. To do so, I added the following code to my show_map.php file.
  1. <?php
  2. // In this example I am hard-coding the address into the variable $address.   In practice, you could
  3. // read it in as a $_GET argument, from a database call or from pretty much anywhere else you’d like.
  4. $address = "300 Alamo Plaza, San Antonio, TX 78205   (210) 555-5555
  5. 17000 W Ih 10 San Antonio TX 78257   210-555-5556
  6. 1 Lone Star Pass – San Antonio, TX 78264
  7. 8210 Agora Parkway, Selma, TX 78148 (210)555-5557";
  8.  
  9. /*
  10.    These next two lines deal with what system the data was entered on and whether carriage returns are
  11.    treated as \r, \n or \r\n. Essentially I’m forcing any instance of \r to become \n which could potentially
  12.    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.
  13.    There may be easier ways of doing this.  I just needed a quick solution and knew this would work. If you
  14.    have a better solution for this let me know.
  15. */
  16.  
  17. $address = str_replace("\r", "\n", $address);
  18. $address = str_replace("\n\n", "\n", $address);
  19.  
  20. // Now I take my addresses and tokenize them into an array using \n as my delimiter.
  21.  
  22. $addresses = explode("\n", $address);
  23.  
  24. // Now we want to strip phone numbers from addresses and put them into a separate array in case I need it later
  25. $phones = array();
  26. for ($i=0; $i<sizeof($addresses); $i++) {
  27.         /*
  28.          Replace anything that matches the specified phone number types with nothing and store the phone
  29.          number in a separate array.  There’s a lot that goes into this regular expression. It worked in
  30.          all the cases I had. May not work in every case. It’d probably take half the page to explain what
  31.          each piece does, so instead I will just refer you to:
  32.                 http://www.webcheatsheet.com/php/regular_expressions.php – Regular Expression Cheat Sheat
  33.         */
  34.         if (preg_match(‘/\s*\({0,1}[0-9]{3}[)\-]{0,1}\s*[0-9]{3}\s*\-\s*[0-9]{4}\s*/’, $addresses[$i], $matches)) {
  35.                 $addresses[$i] = str_replace($matches[0], "", $addresses[$i]);
  36.                 $phones[$i] = $matches[0];
  37.         } else {
  38.                 $phones[$i] = "";
  39.         }
  40. }?>
This now results in an array that looks like this:
  1. 300 Alamo Plaza, San Antonio, TX 78205
  2. 17000 W Ih 10 San Antonio TX 78257
  3. 1 Lone Star Pass – San Antonio, TX 78264
  4. 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:
  1. x1 = Minimum Latitude
  2. x2 = Maximum Latitude
  3. y1 = Minimum Longitude
  4. y2 = Maximum Longitude
MapQuest can retrieve the latitude and longitude for an address in either JSON or XML format. I chose to use JSON. From that data I calculated my “center point”.
  1. <?php
  2. // create curl resource for our connections
  3. $ch = curl_init();
  4.  
  5. // set extreme values for the min/max lat/long.  
  6. $lat_min = $lng_min = 999999;
  7. $lat_max = $lng_max = -999999;
  8.  
  9. // create an array to store the coordinates for each location
  10. $positions = array();
  11.  
  12. foreach ($addresses as $address) {
  13.         // the URL for retrieving coordinates for given address
  14.         $geocode_url = "http://www.mapquestapi.com/geocoding/v1/address?key=".MAPQUEST_API_KEY.
  15.             "&callback=renderOptions&inFormat=kvp&outFormat=json&location=".urlencode($address);
  16.  
  17.         // Retrieve the data as JSON.  
  18.         curl_setopt($ch, CURLOPT_URL, $geocode_url);
  19.         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  20.         $json = curl_exec($ch);
  21.  
  22.         // This example assumes retrieval worked properly. Feel free to add error detection to your code.
  23.         // Mapquest wraps an extraneous renderOptions(); function around the JSON results that we need to get rid of.
  24.         $json = str_replace(array("renderOptions(",");"), "", $json);
  25.  
  26.         // decode our json data
  27.         $return = json_decode($json);
  28.         // focus on the results portion of the data
  29.         $results = $return->results[0];
  30.         // focus on the resulots->locations portion of the data.
  31.         $locations = $results->locations[0];
  32.  
  33.         // determine if any of our coordinates meet any of the extreme conditions and reset extremes accordingly
  34.         if ($locations->latLng->lat > $lat_max) $lat_max = $locations->latLng->lat;
  35.         if ($locations->latLng->lat < $lat_min) $lat_min = $locations->latLng->lat;
  36.         if ($locations->latLng->lng > $lng_max) $lng_max = $locations->latLng->lng;
  37.         if ($locations->latLng->lng < $lng_min) $lng_min = $locations->latLng->lng;
  38.  
  39.         // add coordinate to positions array in a format that will play nicely with plotting later
  40.         $positions[] = "lat:".$locations->latLng->lat . ", lng:" . $locations->latLng->lng;
  41. }
  42.  
  43. // calculate the center latitude and longitude
  44. $center_lat = ($lat_min + $lat_max) / 2;
  45. $center_lng = ($lng_min + $lng_max) / 2;
  46. ?>
We now have an approximate center point for our map.

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.
  1. <?php
  2.         //specify our map dimensions and scale size in pixels
  3.         define(‘MAP_WIDTH’, ’500′);
  4.         define(‘MAP_HEIGHT’, ’400′);
  5.         define(‘MAP_SCALE_IN_PIXELS’, ’77′);
  6.  
  7.         // This array contains the number of kilometers, the standard scale on mapquest contains at each zoom factor.
  8.         $zoom_ranges = array(1840,630,210,71,32,14,7,3.2,1.6,0.8,0.4,0.2,0.1,0.05,0.03,0.02);
  9.  
  10.         // This is an implementation of the Haversine Formula that calculates distance in Kilometers between
  11.         //    ($lat1,$lon1) and ($lat2,$lon2).
  12.         function calculate_distance_km ($lat1, $lon1, $lat2, $lon2) {
  13.                 return ( 3958*3.14159265*sqrt( ($lat2-$lat1) * ($lat2-$lat1) +
  14.                         cos($lat2/57.29578)*cos($lat1/57.29578)*($lon2-$lon1)*($lon2-$lon1))/180);
  15.         }
  16.  
  17.         // calculate the change in X and Y coordinates separately
  18.         $x_delta = calculate_distance_km($lat_min, $lng_min, $lat_max, $lng_min);
  19.         $y_delta = calculate_distance_km($lat_min, $lng_min, $lat_min, $lng_max);
  20.  
  21.         // calculate the best zoom factor on the X-axis
  22.         for ($bestX = 0; $bestX < sizeof($zoom_ranges) ; $bestX++) {
  23.                 // calculate the width of the total map in kilometers
  24.                 $total_width_km = MAP_WIDTH * $zoom_ranges[$bestX] / MAP_SCALE_IN_PIXELS;
  25.                 if ($total_width_km < $x_delta) break;
  26.         }
  27.  
  28.         // calculate the best zoom factor on the Y-axis
  29.         for ($bestY = 0; $bestY < sizeof($zoom_ranges); $bestY++) {
  30.                 // calculate the height of the total map in kilometers
  31.                 $total_height_km = MAP_HEIGHT * $zoom_ranges[$bestY] / MAP_SCALE_IN_PIXELS;
  32.                 if ($total_height_km < $y_delta) break;
  33.         }
  34.  
  35.         // we want to take the minimum value, because we don’t want to zoom in too far.
  36.         $zoom_best = min($bestX, $bestY);
  37. ?>

Displaying the Map

Now that we’ve taken care of the calculations, it’s time to display the map.
  1. <!DOCTYPE html>
  2. <head>
  3. <script src="http://mapquestapi.com/sdk/js/v6.0.0/mqa.toolkit.js?key=<?php echo MAPQUEST_API_KEY; ?>"></script>
  4. <script type="text/javascript">
  5.   MQA.EventUtil.observe(window, ‘load’, function() {
  6.  
  7.   // create a new map in div#map using our calculated center and zoom factor
  8.   window.map = new MQA.TileMap(
  9.     document.getElementById(‘map’),
  10.     <?php echo $zoom_best; ?>,
  11.     {lat:<?php echo $center_lat; ?>, lng:<?php echo $center_lng; ?>},
  12.     ‘map’);
  13.  
  14.     // put a zoom control on the map
  15.     MQA.withModule(‘zoomcontrol3′, function() {
  16.       map.addControl(
  17.         new MQA.LargeZoomControl3(),
  18.         new MQA.MapCornerPlacement(MQA.MapCorner.TOP_LEFT)
  19.       );
  20.     });
  21.  
  22.     // set our locations
  23.     <?php for ($i=1; $i<=sizeof($positions); $i++) { ?>
  24.         var basic=new MQA.Poi( {<?php echo $positions[$i-1]; ?>} );
  25.         basic.setBias({x:-5,y:-5});
  26.         basic.setRolloverContent(‘<?php echo str_replace("’", "\\‘", $addresses[$i-1]); ?>’);
  27.         map.addShape(basic);
  28.     <?php } ?>
  29.   });
  30. </script>
  31. </head>
  32. <body>
  33. <div style="width: <?php echo MAP_WIDTH; ?>px; margin: 5px auto; ">
  34.  
  35. <!– this is our map container –>
  36. <div id="map" style="height:<?php echo MAP_HEIGHT; ?>px; width:<?php echo MAP_WIDTH; ?>px; border: 2px solid #000; "></div>
  37. <?php
  38.         $num = 0;
  39.         echo ‘<ol>’;
  40.         foreach ($addresses as $fa) {
  41.                 if (isset($phones[$num]) && strlen($phones[$num]) > 0) {
  42.                         $fa .= ‘ &mdash; ‘. $phones[$num];
  43.                 }
  44.                 $num++;
  45.                 echo ‘<li>’. $fa . ‘</li>’;
  46.         }
  47.         echo ‘</ol>’;
  48. ?>
  49. </div>
  50.  
  51. </body>
  52. </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:
  1. <!DOCTYPE html>
  2. <head>
  3. <!– add in the necessary jQuery files –>
  4. <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery-ui.css" type="text/css" media="all" />
  5. <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
  6. <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js" type="text/javascript"></script>
  7. <script src="http://mapquestapi.com/sdk/js/v6.0.0/mqa.toolkit.js?key=<?php echo MAPQUEST_API_KEY; ?>"></script>
  8.  
  9. <script type="text/javascript">
  10.   MQA.EventUtil.observe(window, ‘load’, function() {
  11.  
  12.   // create a new map in div#map using our calculated center and zoom factor
  13.   window.map = new MQA.TileMap(
  14.     document.getElementById(‘map’),
  15.     <?php echo $zoom_best; ?>,
  16.     {lat:<?php echo $center_lat; ?>, lng:<?php echo $center_lng; ?>},
  17.     ‘map’);
  18.  
  19.     // Remove the Old Zoom Control and Replace With this code to capture mouse wheel events
  20.     MQA.withModule(‘zoomcontrol3′,‘mousewheel’, function() {
  21.         map.enableMouseWheelZoom();
  22.     });
  23.  
  24.     MQA.EventManager.addListener(map, ‘zoomend’, function(evt) {
  25.         $("#map_zoom_slider").slider("option","value",evt.zoom);
  26.     });
  27.  
  28.     // set our locations
  29.     <?php for ($i=1; $i<=sizeof($positions); $i++) { ?>
  30.         var basic=new MQA.Poi( {<?php echo $positions[$i-1]; ?>} );
  31.         basic.setBias({x:-5,y:-5});
  32.         basic.setRolloverContent(‘<?php echo str_replace("’", "\\‘", $addresses[$i-1]); ?>’);
  33.         map.addShape(basic);
  34.     <?php } ?>
  35.   });
  36. // add jquery slider info
  37. $(function() {
  38.         $("#map_zoom_slider").slider({
  39.                 range: "max",
  40.                 min: 1,
  41.                 max: 16,
  42.                 value: <?php echo $zoom_best; ?>,
  43.                 slide: function(event, ui) {
  44.                         map.setZoomLevel(ui.value);
  45.                 }
  46.         });
  47. });
  48. </script>
  49. <body>
  50.  
  51. <div style="width: <?php echo MAP_WIDTH; ?>px; margin: 5px auto; ">
  52. <!– this is our map container –>
  53. <div id="map" style="height:<?php echo MAP_HEIGHT; ?>px; width:<?php echo MAP_WIDTH; ?>px; border: 2px solid #000; "></div>
  54. <!– this is our slider container –>
  55. <div id="map_zoom_slider" style="margin: 5px auto 0 auto;"></div>
  56.  
  57. <?php
  58.         $num = 0;
  59.         echo ‘<ol>’;
  60.         foreach ($addresses as $fa) {
  61.                 if (isset($phones[$num]) && strlen($phones[$num]) > 0) {
  62.                         $fa .= ‘ &mdash; ‘. $phones[$num];
  63.                 }
  64.                 $num++;
  65.                 echo ‘<li>’. $fa . ‘</li>’;
  66.         }
  67.         echo ‘</ol>’;
  68. ?>
  69. </div>
  70.  
  71. </body>
  72. </html>
Now we can style the slider however we want and keep it off the map.

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.
  1. <a class="thickbox" rel="nofollow"
  2.   href="/show_map.php?KeepThis=true&amp;TB_iframe=true&amp;height=570&amp;width=650">Map Link</a>
Want to see how it works? Try it out: Sample Map Link
Want the source for the final show_map.php file? Download It
David Stinemetze is the Lead Developer and Director of Social Media for San Antonio Web Design, SEO and Hosting firm, Internet Direct.
Website | Facebook | Twitter

/* Facebook */

to “MapQuest API: Location Plotting, Centering & Automatic Zooming using PHP and jQuery”

  1. Aiko says:

    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.

  2. Brian says:

    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!

  3. Susan says:

    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.

Leave a Reply