In part 1, we used a custom page type, page attributes and a custom block template to turn the Page List block into a Google Map that displays pins that link through to pages with addresses. Now we adjust things to use latitude and longitude values.

The issue we encountered in part one was that the Google Maps API isn't really designed to use address details to create map markers - markers are instead supposed to be created with Latitude and Longitude co-ordinates, otherwise address lookups on every page render slow things down enormously.

In this second part, we'll walk through how we can change the setup to use coordinate attributes instead and how we can build a simple package that automatically fetches and stores these values behind the scenes.

Like part 1, we'll develop this for version 5.6 of concrete5. (When we use this approach on a 5.7 site, I'll update these instructions accordingly)

Adjusting the custom page template

In part 1, we fetched and used a property's address attributes, passing them to the jQuery script gmap3 to process and place the map marker. While we creating address attributes we also conveniently created a lat and long set of attributes.

Let's adjust the script to now use these values instead:

<?php
defined('C5_EXECUTE') or die("Access Denied.");
$th = Loader::helper('text');
?>
<?php
$addresses = array();

foreach ($pages as $page):
    $title = $th->entities($page->getCollectionName());
    $url = $nh->getLinkToCollection($page);
    
    $lat = $page->getAttribute('lat');
    $long = $page->getAttribute('long');
    
    if ($lat && $long) {
        $position = 'latLng: ['.$lat.', '.$long .']';
        $addresses[] = '{'.$position .', data:'.json_encode($url). '}';
    }
endforeach; ?>

<?php

$currentPage = Page::getCurrentPage();

if (!$currentPage->isEditMode()) { ?>
    <div id="map<?php echo $bID; ?>" style="min-height: 400px"></div>
<?php } else { ?>
    <p><em>Map disabled in edit mode</em></p>
<?php } ?>

<script type="text/javascript"  src="https://maps.google.com/maps/api/js?sensor=false&amp;language=en"></script>

<script>
    $(document).ready(function(){
        $("#map<?php echo $bID; ?>").gmap3({
            map:{  },
            marker: {
                values: [<?php echo implode(',', $addresses); ?>],
                events:{
                    click: function(marker, event, context){
                        window.location = context.data;
                    }
                }
            }
        },"autofit");
    });
</script>

It's a pretty simple change, we just fetched the lat and long attribute values and used them instead of the address.

After refreshing your map, all your map markers will disappear. That's because your lat and long attributes will be empty. If you edit one of your properties and manually put in some values, the markers should appear again.

So now we need a way of automatically looking up and populating these values.

Lat/Long Updater Package

What we'd like to be able to do is effectively forget about these two attribute values and have them populate (and update if the address changes) automatically. To do this, we'll take advantage of concrete5's event hooks, specifically we want to perform an update when a page is updated. Specifically we'll take advantage of the on_page_version_approve event, as this is fired when a page is first created or a new version is approved.

For this package, we simply need a folder in our packages directory, let's call this lat_long_updater, and in this we'll create a package controller, controller.php

As it really just performs one function, we'll put the update functions in the package controller as well, it means we only need one script. Here is is:

<?php 
defined('C5_EXECUTE') or die(_("Access Denied."));

class LatLongUpdaterPackage extends Package {

	protected $pkgHandle = 'lat_long_updater';
	protected $appVersionRequired = '5.6.2.1';
	protected $pkgVersion = '0.9.1';
	
	public function getPackageDescription() {
		return t("Automatically retrieves Latitude and Longitude values for records that have addresses");
	}
	
	public function getPackageName() {
		return t("Lat/Long Updater");
	}
	
	public function getPackageHandle() {
		return $this->pkgHandle;
	}
     
    public function on_start() {
        Events::extend('on_page_version_approve', 'LatLongUpdaterPackage', 'doUpdate',  __FILE__);
    }

    public function doUpdate($page) {
        if ($page->getAttribute('manual_lat_long')) {
            return true;
        }

        $address = $page->getAttribute('street_address') . ' ' . $page->getAttribute('city') .  ' '. $page->getAttribute('state') . ' '. $page->getAttribute('zip_code');

        if (!$address) {
            return true;
        }

        $url = "http://maps.google.com/maps/api/geocode/json?sensor=false&address=";

        $request = $url . urlencode($address);
        $response = LatLongUpdaterPackage::remoteCall($request);
        $response = json_decode($response, true);

        if($response['status'] == 'OK'){
            $lat = $response["results"][0]["geometry"]["location"]["lat"];
            $long = $response["results"][0]["geometry"]["location"]["lng"];

            $page->setAttribute('lat', $lat);
            $page->setAttribute('long', $long);
        }else{
            return false;
        }
	}

	public function remoteCall($url) {
		if (!$url) {
			return false;	
		}  
		  
		$curl = curl_init();
		$opts = array();
		$opts[CURLOPT_URL] = $url;
		$opts[CURLOPT_RETURNTRANSFER] = true;
		$opts[CURLOPT_CONNECTTIMEOUT] = 10;
		$opts[CURLOPT_TIMEOUT] = 20;
		$opts[CURLOPT_RETURNTRANSFER] = true;
		
		curl_setopt_array($curl, $opts);
		$rbody = curl_exec($curl);

		curl_close($curl);	  
 	 	return $rbody;
	}

	public function install() {
		$pkg = parent::install();
	}
	 
	public function uninstall() {
		parent::uninstall();
	}
}

I won't explain every line, but in summary:

  • The on_start function uses Events::extend, to trigger a call to the function doUpdate whenever the on_page_version_approve event is fired.
  • The doUpdate function looks at the updated page and sees if it has an address in it's address attributes.
  • If an address is found, the function remoteCall is called, which fires off a request to Google to 'geocode' the address.
  • The response back from Google is read and if it finds a set of latitude and longitude values, it update those attributes on the page being updated.

So with this package installed, anytime a page with a set of address fields is updated, it will automatically go and retrieve the lat and long values, storing them on the page. New and updated properties will now appear on our Google Map, but now they should display almost instantly as it doesn't have to look up each address first.

composer_attributes.png

The nice thing about this approach too is that it's not limited to one particular page type - you could associate your address page attributes with as many different pages types as you need. For example, you might also have a 'neighbourhood' page type to act as a category for properties - you could have a map to show neighbourhoods and a map for each neighbourhood to show properties.

Final tweak

If you've studied the above code you may have noticed the lines:

if ($page->getAttribute('manual_lat_long')) {

    return true;
}

If for whatever reason Google isn't returning an accurate location for your marker and you want to manually enter the latitude and longitude, we need a way to disable the auto-updating of the lat and long attributes, otherwise our adjustments will be overriden. To handle this, we can create a checkbox attribute with the handle manual_lat_long - when this is checked the update script won't attempt to update the lat and long values. We can also use this to prevent a property from showing on the map, by checking it and clearing any lat and long values that might have been populated.

So with part 1 of this handling the output of addresses on a Google Map, and this second part handling the lookup of latitude and longitude values automatically with a simple package, we've got a way to use the composer to created pages with associated addresses and have them automatically display on a Google Map - no manually adding markers, etc.

From here, we could customise our page types (perhaps by also putting on another Google map since we now have the latitude and longitude values handy), we could add further attributes and expand the setup quite easily for a whole range of custom purposes.

icon.pngThe package above has been uploader to github and can be hacked/customised to suite your needs quite easily. Download the release, make sure the folder is labeled lat_long_updater and place it in your packages directory to install via the Dashboard.

Again, the code above the package on Github is for version 5.6 of concrete5, not 5.7.

If you're familiar with how to customise Google Maps, you might also want to customise the colours it outputs - see our previous post on that for some tips on how to do this.

-Ryan