Using Google map API with Umbraco

Geocoding property editor for Umbraco using Google Maps API

In the previous article of this series I showed how its possible to create content in Umbraco that can be displayed as a marker on a Google map. A problem with the approach I've used so far is that to get the latitude and longitude of a location you essentially have to use another website like http://www.latlong.net/ to find them and then cut and paste the into Umbraco. This is a pretty tedious process, a better way of doing this would be to build the functionality to geocode a location into Umbraco which removes the need to cut and paste any data. We can achieve this using a custom property editor.

The Umbraco API allows developers to create their own property editors which can be used in the same way as the ones that come pre-installed with Umbraco. This is a really useful feature as occasionally the pre-installed ones might not be what are required when defining a document type. To add a custom property editor you need to create a plugin.

Setting up a plugin

For a new property editor we need to create a plugin. The plugin is comprised of three components; a manifest file that tells Umbraco details of the other components and their properties, a view which is the html that is rendered when the property editor is used, and a controller which controls the behaviour of the property by using an AngularJS controller. For the purpose of this article I assume you have access to the underlying folder structure of an Umbraco site.

Firstly, create a sub folder in the /App_Plugins folder called GoogleMapGeocoder, this will contain all our plugin files. It’s called GoogleMapGeocoder because it uses the Google maps API for the geocoding.

Create the manifest

The manifest file is a text file containing some JSON that defines the property editors name, alias and its view and controller. Create a file in the /App_plugin/GoogleMapGeocoder folder called package.manifest.

The JSON we’ll use is as follows:

{
    javascript: [
        "~/App_Plugins/GoogleMapGeocoder/googlemapgeocoder.controller.js"
    ],
    propertyEditors: [
        {
            alias: "Geocoder",
            name: "Geocoder",
            editor:
            {
                view: "~/App_Plugins/GoogleMapGeocoder/googlemapgeocoder.html",
                valueType: "JSON"
            }
        }
    ]   
}

This is essentially two arrays; the first array 'javascript' contains the path to a JavaScript file that contains the Angular controller.

The Second called 'propertyEditors' contains the values for the alias and the name of the property, these are used by Umbraco to identify the property. There is also an editor object that has properties for the view which specifies the path to the plugin view and the valueType which specifies how data is stored.

Because Umbraco can only store the value for the property in a single field in the database we use the valueType property to specify that it is stored in JSON format. This is so we are able to store more than one piece of data (in this case latitude and longitude) in a structured format e.g {"latitude":51.5073509,"longitude":-0.1277582}. This means that when we retrieve it from the database it can be deserialized into an object in our code.

Note: in the previous article in the series I stored the latitude and longitude values in separate fields. From this point onward in the series we will use this property editor so the latitude and longitude will be stored in a single field in JSON format mentioned above.

Create the view

Create a new html file in the /App_plugin/GoogleMapGeocoder folder called googlemapgeocoder.html. Cut and paste the following html into the file.

<div ng-controller="googlemapgeocoder.controller">
    <input ng-model="address" type="textbox">
    <input type="button" value="Geocode" ng-click="codeAddress()">
    <div style="height: 400px;" id="map_canvas"></div>
</div>

This html is what is used when the property is added to a document type. The containing div defines the scope of the html that is bound to the controller by using the ng-controller directive attribute. The textbox input field is bound to the models address property using the ng-bind attribute, and the button input field uses the ng-click attribute to call the codeAddress() function when the button is clicked. There is also a div that is used to display a Google map with a marker showing the place of the location (latitude and longitude) returned by the Google API.

Create the controller

Create a new js file in the /App_plugin/GoogleMapGeocoder folder called googlemapgeocoder.controller.js.

To start we create the controller that is defined in the view and add it to the umbraco module used by the API, we then pass into the controller the assetService as an argument. The code that does this uses standard Angular syntax:

angular.module("umbraco").controller("googlemapgeocoder.controller", function ($scope, assetsService) { 
    //controller implementation goes here.
});
Add and initialise some variables we use in our implementation
// initialise variables
var map; 
var marker;
var geocoder; 
var latitude = 0;
var longitude = 0;
Check the model for stored values
// check scope model and set values
if (!$scope.model.value) {
    $scope.model.value = {};
} else {
    latitude = $scope.model.value.latitude;
    longitude = $scope.model.value.longitude;
}

The $scope.model.value contains the JSON that is stored for the property; we can therefore access the latitude and longitude values as properties of the value. If there are values we assign them to the latitude and longitude variables.

Use the assets service to load Google map API
// use assests service to load Google maps api
assetsService.loadJs("http://www.google.com/jsapi")
   .then(function () {         google.load("maps", "3", {             callback: initialize,             other_params: "key=AIzaSyC-sOLv-YADP4SK8kJZVFr0j8r2osSs8-k"         });     });

The assetsService we passed into the controller is used to load client side dependencies which we can then use in our code.

We use the assetsService.loadJs method to get the Google loader javascript, when it has loaded the .then() method calls a function that tells the loader object to load the Google maps API. This then calls the intitialize function defined in the callback property which is where we create the Google map. The other_params property contains the Google API key required to authenticate the request to the Google API as mentioned in the update above. You can try using the one in the snippet but its possible you might have to get your own.

Add initialize function
// called when Google maps api has loaded
function initialize() {
    // create geocoder
    geocoder = new google.maps.Geocoder();

    // create latLng
    var position = new google.maps.LatLng(latitude, longitude);

    // create map
    map = new google.maps.Map(document.getElementById('map_canvas'), {
        zoom: 8,
        center: position
    });

    // create draggable marker
    marker = new google.maps.Marker({
        position: position,
        map: map,
        draggable: true
    });
    
    // listen for marker position changed event and set model values
    google.maps.event.addListener(marker, "position_changed", function (e) {
        $scope.model.value.latitude = marker.getPosition().lat();
        $scope.model.value.longitude = marker.getPosition().lng();
    });
}

The intitialize function is called when the Google map API is loaded. It first creates the geocoder object that we will use to find the location from our input. It then creates the position, map and marker objects used to generate the map with a draggable marker. This so we can drag it to a position on the map that we want to use as our location.

For the draggable functionality to work we then add a listener event that executes when the position of the marker is changed (dragged or set). When it executes it sets the latitude and longitude values of the scope using the current position of the marker on the map.

Add the codeAddress function
// runs when geocode button in view is clicked
$scope.codeAddress = function () {
    // use Google api to geocode location
    geocoder.geocode({ 'address': $scope.address }, function (results, status) {
        // set location if geocode successful
        if (status == google.maps.GeocoderStatus.OK) {
            map.setCenter(results[0].geometry.location);
            marker.setPosition(results[0].geometry.location);
        } else {
            alert('Geocode was not successful' + status);
        }
    });
}

To finish off the controller we need to add the code that executes when the geocode button is clicked in the view.

The function that runs when codeAddress() is called starts by using the gecoder.geocode method passing the $scope.address property as a parameter and returns an array of results and a status.

A statement then checks if the status is ok, if so, it sets where the map is centred and the position of the marker to the location of the first item in the results array which is essentially Googles best guess at the address that it has tried to geocode. If the status is not ok then an alert is displayed specifying the status. When the marker.setPosition() method is called it triggers the position_changed event that we added to the initialize function so that the it sets the latitude and longitude values of the scope using the current position of the marker on the map.

The full controller code
angular.module("umbraco").controller("googlemapgeocoder.controller",
     function ($scope, assetsService) {
         // initialise variables
         var map;
         var marker;
         var geocoder;
         var startLat = 0;
         var startLong = 0;
         
         // check scope model and set values
         if (!$scope.model.value) {
             $scope.model.value = {};
         } else {
             startLat = $scope.model.value.latitude;
             startLong = $scope.model.value.longitude;
         }
         
         // use assests service to load Google maps api
         assetsService.loadJs("http://www.google.com/jsapi")
            .then(function () {
                google.load("maps", "3", {
                    callback: initialize,
                    other_params: "key=AIzaSyC-sOLv-YADP4SK8kJZVFr0j8r2osSs8-k"
                });
            });
         
         // called when Google maps api has loaded
         function initialize() {
             // create geocoder
             geocoder = new google.maps.Geocoder();
             
             // create latLng
             var position = new google.maps.LatLng(startLat, startLong);

             // create map
             map = new google.maps.Map(document.getElementById('map_canvas'), {
                 zoom: 8,
                 center: position
             });

             // create draggable marker
             marker = new google.maps.Marker({
                 position: position,
                 map: map,
                 draggable: true
             });

             // listen for marker position changed event and set model values
             google.maps.event.addListener(marker, "position_changed", function (e) {
                 $scope.model.value.latitude = marker.getPosition().lat();
                 $scope.model.value.longitude = marker.getPosition().lng();
             });
         }

         // runs when geocode button in view is clicked
         $scope.codeAddress = function () {
             var address = $scope.address;
             // use Google api to geocode location
             geocoder.geocode({ 'address': address }, function (results, status) {
                 // set location if geocode successful
                 if (status == google.maps.GeocoderStatus.OK) {
                     map.setCenter(results[0].geometry.location);
                     marker.setPosition(results[0].geometry.location);
                 } else {
                     alert('Geocode was not successful' + status);
                 }
             });
         }
     }); 

Create the data type

Once you've created the three files all that left to do is to create a new data type in the Umbraco back office and add it to a document type.

Note: you will probably need to restart your Umbraco instance for the plugin to be registered.

Login to the Umbraco back-office and go to the Developer section. Click on the three dots next to the Data types menu and choose the ‘New Data type’ option. Enter the name you want to give the data type and in the property editor dropdown you should have the option ‘Geocoder’ which is the name that you gave the property editor in the manifest file.

Now you have created the data type you can add it to a document type as a property in the same way as you would add any of the built-in data types.

Try it out

To try it out open the Location document type that was created for the first article in the series and add it as a property called Geocoder and choose the type you created. Whilst in the document type you can delete the Latitude and Longitude properties as we won't be using them for the remainder of the series.

Once you saved it go to the Content section and create a new content item using the document type. The content editor screen should display the property editor that we have created. Give the content the name of a place e.g. London then in the property editor type London and hit the Geocode button. The map should reload with a marker appearing over London. Now when you save the content it should save the location that you have geocoded.

To test it with the Location template we created in the first article of the series you will need to change the following line:

var position = new google.maps.LatLng(@CurrentPage.Latitude, @CurrentPage.Longitude)

to

var position = new google.maps.LatLng(@CurrentPage.Geocoder.latitude, @CurrentPage.Geocoder.longitude)

So far in this series we've created content for a location that can be displayed on a map and a property editor to geocode the location. Next we look a how we can create multiple content locations and display them all on the same map.

Next article: Multiple marker display using Umbraco list view

Comments

To be able to comment you need to login using a Google or Facebook account.