New Mappable module, and some unsolicited programming pedagogy

14 Jun

New Mappable module, and some unsolicited programming pedagogy

In these next two posts, we’ll take a break from the SilverSmith stuff and talk about what I’ve been doing in the time I haven’t been working on that major project. For those of you who are anxious, don’t worry — next week’s post will be a status update on SilverSmith, including some slick new UI designs that I’ll be sharing with you.

Mappable module: Put anything on a Google Map

I was working on a project at Bluehouse Group that required repeatedly mapping a number of different objects on a Google Map. This happened on several different templates and contexts, and although I was reusing some fairly flexible code from my work onĀ Xetic.org, I wanted to take it a step further and create something that really throttled the Sapphire framework and wasn’t so disparate. My goal was to be able to plot objects on a Google map from within the template, with no controller functions needed. That is:

$MyDataObjectSet.GoogleMap

$MyObject.GoogleMap

$MyOtherDataObjectSet.GoogleMap(640,480)

etc…

The answer came in the form of some clever decorators, a solid Google Maps API, and one of my favorite programming concepts — an interface.

PHP Interfaces: What they are, and why you should care

Frequent readers of this blog may know that I’m fairly allergic to PHP’s lax attitude on common programming conventions. It lets you get away with a lot, and that can lead to some sloppy coding by amateurs and experts alike. Interfaces are one of those great elements of programming that are almost purely conceptual in nature and really serve no functional purpose other than to provide good structure and force you to think things through in an elegant, efficient way.

An interface is used to describe a set of properties and methods that a type of class is guaranteed to need. For instance, a shape will always be able to tell you its area, and an an animal will always be able to tell you its scientific name. You might be wondering how an interface is any different from a parent (perhaps abstract) class. An interface is much more lightweight than a parent class, and the most important thing to know about an interface is that you can inherit from (or implement) as many as you like, unlike standard inheritance in PHP, which only allows you to have one parent.

A word about abstract classes

Let’s continue using animals for our example. We know that there is no such entity as “Animal” in our world — it’s purely a concept. You can’t go to the pet store and ask for an “animal” and expect the clerk to know what you mean — the same way you can’t ask your friend to draw you a “shape.” Because we’re only defining structure and building a concept, we’ll create an abstract class called “Animal,” from which all animals inherit. We know that all animals are going to share certain properties and methods, so let’s define those.

abstract class Animal {

   public $numberOfLegs;

   public $canSwim;

   public $canFly;

   abstract public function speak();

   abstract public function eat($food);

}

Make sense? We know that all animals can do these things, or have these properties, so we define them in an abstract (conceptual) class. Now we can extend that a bit further and create a specific animal.

class Lion extends Animal {

   public $numberOfLegs = 4;

   public $canSwim = false; // ??? any zoology majors want to chime in?

   public $canFly = false;

   public function speak() {
      return $this->isAngry ? "ROAR!" : "Meow!";
   }

   public function eat($food) {
      return $food instanceof Meat ? "Yum!" : "No thanks. I'm a carnivore, dude.";
   }

}

Ideally, we would have one or several abstract classes between Animal and Lion, such as “Mammal,” for instance, but I think for the purposes of this exercise, the point is made pretty clear.

Enter Interfaces

What happens when we have an animal that is a mammal, but also has a set of other properties that are shared across special types of its kind? Let’s imagine “marsupial” as an example. Marsupials, for those of you slept through high school biology, are special mammals that carry their young in a pouch. One approach would be to stuff the Mammal class with a $hasPouch boolean, but is that really the cleanest way to accommodate this edge case? An interface makes much more sense.

interface Marsupial {

    public function getPouchSize();

    public function emptyPouch();

    public function fillPouch($joeys);

}

Now let’s define a Kangaroo.

class Kangaroo extends Mammal implements Marsupial {

    protected $pouchContents = array ();

    protected $waitingForBabies = false;

    protected $pouchSize;

    public function getPouchSize() {
        return $this->pouchSize;
    }

    public function emptyPouch() {
        $this->pouchContents = null;
        $this->waitingForJoeys = true;
    }

    public function fillPouch($joeys) {
        foreach((array) $joeys as $joe) {
            $this->pouchContents[] = $joe;
        }
    }
}

That’s it! Interfaces are great for creating special classes of objects. The important thing is that they dictate that every class that implements the interface must define its properties an methods, at the very least. We can’t implement the Marsupial interface if we haven’t told the application how to empty and fill its pouch. Think of it as a template for building a class, and all of the fields are required.

I build SilverStripe sites. I don’t build weird Aussie animals.

OK, point taken. So how is this used in the real world? Well imagine an iOS app, perhaps. (Warning: I’m not an iOS developer so the following is purely speculative and likely inaccurate). Every iOS application whether for iPhone or iPad, at the very least, has to do some basic things.

interface iPhoneApp {

    public function getIcon();

    public function needsUpdate();

    public function getNumberOfNotifications();

}

To bring it even closer to home, SilverStripe uses interfaces in several places in the Sapphire framework. One of my favorite interfaces is PermissionProvider, which tells the CMS that this type of object should have its own settings in SecurityAdmin.

class Article extends DataObject implements PermissionProvider {
 	static $api_access = true;

 	public function canView($member = false) {
 		return Permission::check('ARTICLE_VIEW');
 	}
 	public function canEdit($member = false) {
 		return Permission::check('ARTICLE_EDIT');
 	}
 	public function canDelete() {
 		return Permission::check('ARTICLE_DELETE');
 	}
 	public function canCreate() {
 		return Permission::check('ARTICLE_CREATE');
 	}
        // This method MUST be defined when implementing the PermissionProvider interface!
 	public function providePermissions() {
 		return array(
 			'ARTICLE_VIEW' => 'Read an article object',
 			'ARTICLE_EDIT' => 'Edit an article object',
 			'ARTICLE_DELETE' => 'Delete an article object',
 			'ARTICLE_CREATE' => 'Create an article object',
 		);
 	}
 }

We’re still talking about the Mappable module, right?

Yes. Sorry that got a little carried away, but you can see now that mappable objects share common traits — they must, at the very least, be able to tell us their Lat/Long coordinates, for instance. Further, in the Google Maps world, they’re going to have to tell us how they are plotted on the map. Therefore, enter the Mappable interface. Let’s look at how to implement it on a given DataObject subclass.

class MemberProfile extends DataObject implements Mappable {

    static $db = array (
        'Lat' => 'Varchar',
        'Lon' => 'Varchar'
        'Type' => "Enum('Farm,Restaurant')"
    );

/* Mappable interface requirements */

    public function getLatitude() {
        return $this->Lat;
    }

    public function getLongitude() {
        return $this->Lon;
    }

    public function getMapContent() {
        return GoogleMapUtil::sanitize($this->renderWith('MapBubbleMember'));
    }

    public function getMapCategory() {
        return $this->Type;
    }

    public function getMapPin() {
        return $this->Type."_pin.png";
    }

/* end Mappable interface */

}

MemberProfile objects can appear on a map, but we don’t want to dictate what the Lat/Long fields are in a subclass. We use an interface that basically sets the requirement that you need to define how those attributes are acquired.

Getting set up with GoogleMapUtil

There are some basic requirements to getting started with the Mappable module. The most important thing is to set your Google Maps API key. If you don’t have one, you can generate one here.

_config.php

GoogleMapUtil::set_api_key($key);

Since API keys are tied to a domain, it’s wise to set up some logic, such as an associative array, to look up your API Key based on the environment. That way, all of your sites can share a _config.php file seamlessly.

Other things you might want to try:

GoogleMapUtil::set_map_size($width, $height); // The size of a map, by default
GoogleMapUtil::$automatic_center = false; // This is on by default.

Generating a Google Map

Plotting all the objects in your DataObjectSet that implement Mappable is as simple as:

$MyDataObjectSet.GoogleMap

And for custom sizing:

$MyDataObjectSet.GoogleMap(400,400)

Similarly, a single object can be put on a map.

$MyObject.GoogleMap

Customizing the Map

The GoogleMap() function returns a GoogleMapAPI object, which has a forTemplate() method to write all the gnarly JavaScript on the template to build the map. But if you want to intercept the map before it is rendered, simply create a custom function in your controller and manipulate the GoogleMapAPI object as needed.

public function DeclaredPartnersIncludingMe() {
    $p = $this->DeclaredPartners();
    if(!$p->exists()) return false;
    $set = new DataObjectSet($this);
    foreach($p as $declared) {
        if($partner = $declared->Partner()) {
            $set->push($partner);
        }
    }
    return $set;
}

This is called on the template with:

$DeclaredPartnersIncludingMe.GoogleMap

We can even do fancy stuff like add individual Mappable objects to the map, and draw some lines to connect two objects. This function plots the current member and all of his partners, with a line connecting the current member to each one.

public function PartnersMap() {
    if(!$set = $this->DeclaredPartnersIncludingMe()) return false;
    $map = $set->GoogleMap();
    $map->addMarkerAsObject($this);
    foreach($set as $obj) {
        $map->connectPoints($this, $obj, "#848484");
    }
    return $map;
}

In this case, because the function returns a GoogleMapAPI object, and not a DataObjectSet, we can just call it on the template with $PartnersMap.

Getting the lat/long values with LatLongField

In the past I’ve used the onBeforeWrite() hook to automatically assign lat/long values on saving of an object, but there are two problems I have with this:

  1. If the request fails, you end up with null values and mysterious things happen unbeknownst to the content editor that he/she cannot repair.
  2. Many clients enjoy the satisfaction of retrieving the values and validating them rather than relying on a behind-the-scenes process.

Ideally we use a combination of both methods, but for now, let’s look at LatLongField as a solution.

 $fields->addFieldToTab("Root.Location", new LatLongField(array(
    new TextField('Lat','Latitude'),
    new TextField('Lon','Longitude')
),
array('PhysicalAddress1','PhysicalAddress2','PhysicalCity','PhysicalState','PhysicalZip')
));

LatLongField is a subclass of FieldGroup, which allows us to pass a set of form fields to be rendered inline on the edit form. We pass those two fields as an array in the first argument. We need one for the Lat field and one for Lon. The second argument is an array of all the fields whose values we will concatenate and pass to Google’s geocoding service to get the lat/long pair. Note that all of these fields must be present in your $db array, and they must be on the edit form somewhere — even if they’re hidden.

The third, optional argument, is the text for the “look up” button (defaults to “look up”, translated).

Wrap-up

There are probably as many ways to do Google Maps as there are Google Maps themselves. I’m not by any means casting some dogma upon SilverStripe developers that says they have to do mapping a certain way. There are many approaches in here that may not work or make sense to you, and there are several things that could use improvement or expansion. My hope is that this module will serve as a foundation for one approach to mapping in the SilverStripe framework, and I welcome the input of other developers to help make this a more solid and powerful set of tools.

Getting the Code

http://github.com/unclecheese/Mappable

13 Responses to “New Mappable module, and some unsolicited programming pedagogy”

  1. Richard Ward 15. Jun, 2011 at 6:07 am #

    Just had a play on a test site and it is brilliantly simple (well – it was after I read the bit about having to add the address fields!).

    Thank you, Uncle Cheese!

  2. mary 15. Jun, 2011 at 9:45 am #

    Thanks for this and especially for the interface explanation

  3. vancouverwill 15. Jun, 2011 at 3:42 pm #

    great work once again UncleCheese! a lot of great potential with this piece. always nice to see you writing your code so neat and following design patterns and principals too!

  4. Marcus Dalgren 16. Jun, 2011 at 3:53 pm #

    Hey Uncle Cheese!

    I did something similar awhile ago but went a very different route. I made my own DB field that implements CompositeDBField which essentially means that you use one field name to represent several fields in the database, in this case Lat, Lang, the address entered and Googles official name for the address.

    The cool part about this is that you just name the one field in your db array and silverstripe creates the required fields for you. I also made a GoogleMapField based somewhat on Sams (I think) earlier work where you simply enter an address and press search and the field values are populated behind the scenes.

    I’ll see if I have the time to do a writeup (will probably end up on SSBits) about it and do some kind of comparison so that people can see how you can do things in different ways with the CMS/framework.

  5. Jono 23. Jul, 2011 at 1:04 am #

    Wondered what ‘implements’ did! Now I know. Thanks for the understandable tutorial and also a great looking module, will try it out next time I need to add some maps.

  6. Bart 27. Jul, 2011 at 11:13 am #

    Thanks a lot for this tutorial and the code!

  7. Mick 02. Aug, 2011 at 5:39 am #

    Hi there, cheers for the tutorial but alas I think my understanding has failed me. I have put a post in the SS forum under other modules regarding my in ability to make this mappable module work. If anyone could give some pointers that would be helpful. I didn’t want to repost my post here as that would just be duplication.

  8. Jono 13. Oct, 2011 at 5:57 am #

    Hi UC,

    I’ve been playing with this module today but I noticed that it seems to use the V2 javascript API which was deprecated by Google in May 2010. V3 doesn’t require an API key so just wondering if you made a conscious decision to use V2 and why that is or if you have any plans to update it to use the latest API?

    Cheers

    • unclecheese 13. Oct, 2011 at 10:07 am #

      Hi, Jono,

      I had no idea that it was using a deprecated library. I actually know very little about Google Maps, and the API wrapper that comes with this module is from a third party. So if you know what needs to be done to upgrade it, by all means, send a pull request through GitHub!

      • Jono 20. Oct, 2011 at 2:03 am #

        No worries – I’ll see how I go on this project. The V3 API is geared towards mobile users so I think it may actually have less features out of the box than V2 in order to be lightweight (meaning potentially a development headache but better for users). Don’t know if I have the skills to upgrade but I’ll look in to it! Would be a great learning exercise. Cheers

        • unclecheese 20. Oct, 2011 at 2:31 pm #

          Yeah, I’m just looking into it now, actually. Looks like a complete overhaul. I don’t see how we could salvage any of the existing code.

  9. Flo 08. Nov, 2011 at 11:22 pm #

    Hi UC
    I just saw that you branched a v3 version of the module. is that already up for use?
    I am currently working on a project which should include search for items within a certain distance. Any idea how I could do that?
    cheers

  10. Mick 03. Apr, 2012 at 4:15 pm #

    Hi ya,
    just looking for a pointer I’m trying to use mappable on the member table that has been extended using a dataobjectdecorator something like
    class MemberTeamExtension extends DataObjectDecorator implements Mappable {
    but it crashes out so I’m guessing I’m not using the right syntax, any pointers?

    Regards

    Mick

Leave a Reply