Monthly Archives: August 2015

Make a Google Map SPEAK component in Sitecore

Map

I just love SPEAK! It has been around for a while but still feels new. I remember when SPEAK came, it felt very cool and fresh but it was so hard to get into. Back then I had
mostly been working with web forms(Sitecore), so the MVC concept was all new to me. Now when I’ve been working with MVC(Sitecore), SPEAK is so much easier to understand.

Here are some really good links to help you to understand how SPEAK works:
SPEAK – Sitecore documentation
Jakob Christensen’s great SPEAK videos
Martina Welander’s great SPEAK posts
Anders Laub’s great SPEAK posts

It’s always nice to present geographical data on a map so why not have it on the front page(Launchpad – “/sitecore/client/Applications/Launchpad”) combining it with Sitecore’s nice Timeline component and showing the latest visitors(like the visitors in “ExperienceProfile/Dashboard”).

This would be the mission for this post, I divided the work into following steps.
1. Create a custom map component
2. Create a sub page containing the custom map component and the Timeline component
3. Add the sub page to the front page (Launchpad – “/sitecore/client/Applications/Launchpad”)

The great posts from Mike Robbins – Add Experience Analytics Report To Launch Pad and Anders Laub – Add a SPEAK application to the Sitecore 8 Launchpad inspired and helped me a lot, thanks guys 🙂

1. Create a custom map component

I started by looking on how Sitecore did their Business Components in “/sitecore/client/Business Component Library” and also pages like “/sitecore/client/Applications/Launchpad” and “/sitecore/client/Applications/ExperienceProfile/Contact”. I also read Pierre Derval posts about SPEAK which helped me a lot.

I decided to do a SPEAK 1.1 component, called GoogleMapSpeak1. I also wanted to have some map properties like height, width, map type etc.
Sitecore Rocks:
Rocks

The GoogleMapSpeak1 Paremeters.template:
Parameters
The Droplist points to the MapTypes folder.

Here is the rendering, GoogleMapSpeak1, with the “properties”(parameters):
GoogleMapRendering
I really tried to change/set the sort order on the parameters(properties) but it seems to sort in alphabetical order only 😦

Project structure:
VS

The view – GoogleMapSpeak1.cshtml

@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@using Sitecore.Web.UI.Controls.Common.UserControls
@model RenderingModel
@{
  var rendering = Html.Sitecore().Controls().GetUserControl(Model.Rendering);
  
  rendering.Class = "sc-GoogleMapSpeak1";
  rendering.Requires.Script("client", "GoogleMapSpeak1.js");
  
  rendering.SetAttribute("data-sc-height", rendering.GetString("Height", "Height", string.Empty));
  rendering.SetAttribute("data-sc-width", rendering.GetString("Width", "Width", string.Empty));
  rendering.SetAttribute("data-sc-latitude", rendering.GetString("Latitude", "Latitude", string.Empty));
  rendering.SetAttribute("data-sc-longitude", rendering.GetString("Longitude", "Longitude", string.Empty));
  rendering.SetAttribute("data-sc-zoom", rendering.GetString("Zoom", "Zoom", string.Empty));
  rendering.SetAttribute("data-sc-maptype", rendering.GetString("MapType", "MapType", string.Empty));
 
  var htmlAttributes = rendering.HtmlAttributes;
}
<div @htmlAttributes>
  <div id="map-canvas"></div>
</div>

I added the div for the map.

Now to the interesting part, the js file. In order to make Google Map work with RequireJS we need the require.async from the requirejs-plugins. The syntax to call the Google Map script:

async!http://maps.google.com/maps/api/js?v=3.exp&signed_in=true&sensor=false

GoogleMapSpeak1.js:

require.config({
    paths: {
        "async": "/SpeakComponents/require.async"
    }
});


define(["sitecore", "jquery", "async!http://maps.google.com/maps/api/js?v=3.exp&signed_in=true&sensor=false"], function (Sitecore, jQuery) {

    var model = Sitecore.Definitions.Models.ControlModel.extend({
        initialize: function (options) {
            this._super();

            this.set("width", null);
            this.set("height", null);
            this.set("latitude", null);
            this.set("longitude", null);
            this.set("zoom", null);
            this.set("mapType", null);
           
            this.set("markers", []);
            this.set("speakMap", null);


        },
        setupSpeakMap: function () {

            var mapDiv = jQuery("#map-canvas");

            mapDiv.css({ width: this.attributes["width"], height: this.attributes["height"] });

            var map = new google.maps.Map(document.getElementById('map-canvas'));

            this.set("speakMap", map);

        },
        showSpeakMap: function () {

            if (this.attributes["speakMap"] == null) {
                this.setupSpeakMap();
            }

            var map = this.attributes["speakMap"];

            if (this.attributes["latitude"] != null && this.attributes["longitude"] != null) {
                map.setCenter(new google.maps.LatLng(this.attributes["latitude"], this.attributes["longitude"]));
            }

            if (this.attributes["zoom"]) {
                map.setZoom(this.attributes["zoom"]);
            }

            if (this.attributes["mapType"]) {
                map.setMapTypeId(eval("google.maps.MapTypeId." + this.attributes["mapType"]));
            }

            this.set("speakMap", map);

        },
        addMarkerToMap: function (markerId, lat, lng, iconPath) {

            //Marker is already added to the map
            if (this.attributes["markers"][markerId] != undefined) {
                this.attributes["markers"][markerId].setMap(this.attributes["speakMap"]);
                return this.attributes["markers"][markerId];
            }
           
            var marker = new google.maps.Marker({
                position: new google.maps.LatLng(lat, lng),
                map: this.attributes["speakMap"],
                animation: google.maps.Animation.DROP
            });

            if (iconPath) {
                marker.setIcon(iconPath);
            }

            this.attributes["markers"][markerId] = marker;

            return marker;
        },

        removeMarkerFromMap: function (markerId) {

            var marker = this.attributes["markers"][markerId];

            //Marker is not on the map
            if (marker == null)
                return;

            marker.setMap(null);

            delete this.attributes["markers"][markerId];

        },
        showAllMarkersOnMap: function () {
            for (key in this.attributes["markers"]) {
                if (this.attributes["markers"].hasOwnProperty(key)) {
                    this.attributes["markers"][key].setMap(this.attributes["speakMap"]);
                }
            }
       
        },
        clearMarkersOnMap: function () {
            for (key in this.attributes["markers"]) {
                if (this.attributes["markers"].hasOwnProperty(key)) {
                    this.attributes["markers"][key].setMap(null);
                }
            }
        },
        deleteMarkers: function() {
            this.clearMarkersOnMap();
            this.attributes["markers"] = [];
        }
        
    


    });



    var view = Sitecore.Definitions.Views.ControlView.extend({
        initialize: function (options) {
            this._super();

            this.model.set("width", this.$el.data("sc-width"));
            this.model.set("height", this.$el.data("sc-height"));
            this.model.set("latitude", this.$el.data("sc-latitude"));
            this.model.set("longitude", this.$el.data("sc-longitude"));
            this.model.set("zoom", this.$el.data("sc-zoom"));
            this.model.set("mapType", this.$el.data("sc-maptype"));
           
            this.model.showSpeakMap();

        }
    });

    Sitecore.Factories.createComponent("GoogleMapSpeak1", model, view, ".sc-GoogleMapSpeak1");
});

I choose to show the map when the view initializes, by calling method showSpeakMap().

If you guys noticed I put the map object in the “speakMap” property. That means when you use/call the component from a page code, it will be something like this:

this.GoogleMapSpeak1.get("speakMap")

I also added some “marker methods” but they are not really necessary, the important one is the speakMap object.

The component is now done 🙂

2. Create a sub page containing the custom map component

Lets test the map component. I wanted to place the map component on the frontapage(Launchpad) together with Sitecore’s nice Timeline component. Instead of adding them directly on the LaunchPad lets put them in a SubAppRenderer.

It was Mike Robbins post who gave me the idea.

First I need to create a SPEAK page(VisitorsLayout):
VisitorsLayout

One important thing. If you are going to render your speak page through the SubAppRenderer, you should not use placeholders and PageCode at all.

When you add SPEAK components to the layout of the common page, do not specify placeholders and do not add PageCode or structure components to the common page

Here is the VisitorsLayout in “design layout”:
DesignLayout
I’ve added a SubPageCode(the js file), the map component(GoogleMapSpeak1), timeline component(VisitorsTimeline) and a GenericDataProvider(VisitorsDataProvider) which is needed in order to get the visitor data.

The js file, VisitorsPageCode.js, is placed in the SubPageCode component. If you notice there is a providerHelper, it will be used to fetch/collect visitors data for the Timeline component. I could have created a controller for this, do an ajax call and so on but why should I do that when Sitecore has something that already is working.

define(["sitecore", "jquery", "/-/speak/v1/experienceprofile/DataProviderHelper.js"], function (sitecore, jQuery, providerHelper) {

    var infowindow = new google.maps.InfoWindow();

    var visitorsPageCode = sitecore.Definitions.App.extend({

        initialized: function () {

            this.GoogleMapSpeak1.addMarkerToMap(1,
                     this.GoogleMapSpeak1.get("latitude"),
                     this.GoogleMapSpeak1.get("longitude"),
                     "/sitecore/shell/Themes/Standard/Images/Sitecorelogo.png");

            this.setupTimeline();

        },
        setupTimeline: function () {

            var aggregatesPath = "/aggregates";
            var latestVisitorsTable = "latest-visitors";

            var url = this.stringFormat("/sitecore/api/ao/v1/{0}/{1}", aggregatesPath, latestVisitorsTable);

            providerHelper.initProvider(this.VisitorsDataProvider, latestVisitorsTable, url, null);

            providerHelper.setDefaultSorting(this.VisitorsDataProvider, "LatestVisitStartDateTime", true);
           
            var timelineData = {
                "dataSet": {
                    "journey": []
                }
            };

            providerHelper.getData(
                this.VisitorsDataProvider,
                jQuery.proxy(function (jsonData) {


                    jQuery.each(jsonData.data.dataSet, function (keyFirst, valueFirst) {
                        jQuery.each(valueFirst, function (key, value) {

                            timelineData.dataSet.journey.push({
                                "ContactId": value.ContactId,
                                "TimelineEventId": value.ContactId,
                                "EventType": "Quantifiable",
                                "ImageUrl": "contact.png",
                                "DateTime": value.LatestVisitStartDateTime,
                                "Duration": value.VisitCount,
                                "LatestVisitCityDisplayName": value.LatestVisitCountryDisplayName,
                                "LatestVisitCountryDisplayName": value.LatestVisitCountryDisplayName
                            });


                            //console.log(value);
                        });
                    });

                    this.VisitorsTimeline.set("data", timelineData);

                }, this)


              );

            this.VisitorsTimeline.on("change:selectedSegment", this.selectTimelineSegment, this);

        },
        selectTimelineSegment: function () {

            var self = this;

            var contactIconPath = "/sitecore/api/ao/v1/contacts/{0}/image?w=40&amp;h=40";

            this.getCoordinates(this.VisitorsTimeline.get("selectedSegment").LatestVisitCityDisplayName, function (coordinates) {

                self.GoogleMapSpeak1.clearMarkersOnMap();

                self.GoogleMapSpeak1.get("speakMap").setCenter(coordinates);

                var marker = self.GoogleMapSpeak1.addMarkerToMap(self.VisitorsTimeline.get("selectedSegment").ContactId,
                       coordinates.G,
                        coordinates.K,
                        self.stringFormat(contactIconPath, self.VisitorsTimeline.get("selectedSegment").ContactId));


                infowindow.setContent(self.renderHtmlContactInfo(self.VisitorsTimeline.get("selectedSegment")));
            
                marker.TimelineEventId = self.VisitorsTimeline.get("selectedSegment").TimelineEventId;

                infowindow.open(self.GoogleMapSpeak1.get("speakMap"), marker);
        
            });



        },
        renderHtmlContactInfo: function (timelineSegment) {
            return this.stringFormat("<div><h4>{0}</h4>", timelineSegment.ContactId) +
                this.stringFormat("<div>Number of visits: {0}", timelineSegment.Duration) +
                this.stringFormat("<p><a href='/sitecore/client/Applications/ExperienceProfile/contact?cid={0}'>Get detailed info</a></p>", timelineSegment.ContactId) +
                "</div></div>";

        },
        getCoordinates: function (city, callback) {
            var geocoder = new google.maps.Geocoder();

            var latlng;

            //Geocoding using google maps api.
            geocoder.geocode({ 'address': city }, function (results, status) {
                if (status === google.maps.GeocoderStatus.OK) {
                    latlng = results[0].geometry.location;
                }
                callback(latlng);

            });
        },
        stringFormat: function () {
            var s = arguments[0];
            for (var i = 0; i &lt; arguments.length - 1; i++) {
                var reg = new RegExp(&quot;\\{&quot; + i + &quot;\\}&quot;, &quot;gm&quot;);
                s = s.replace(reg, arguments[i + 1]);
            }
            return s;
        },
        generateGuid: function () {

            function s4() {
                return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
            }

            return (s4 + s4() + &quot;-&quot; + s4() + &quot;-4&quot; + s4().substr(0, 3) + &quot;-&quot; + s4() + &quot;-&quot; + s4() + s4() + s4()).toLowerCase();
        }
    });
    return visitorsPageCode;
});

When a Timeline segment is selected it will add a marker(with an infowindow) on the map. To get the coordinates I used Google’s geocoding api.

3. Add the sub page to the front page(Launchpad)

Finally we can now add VisitorsLayout, to the LaunchPad. First we need to locate the Launchpad:
LaunchPad

Lets take a look at it in the Design Layout.
LaunchpadVisual
I’ve added some Border components(VisitorHeaderWrap and VisitorBodyWrap), Text component(VisitorHeader) for the title and the SubAppRenderer component for including the SPEAK page – VisitorsLayout.

Here you can see how the datasource in the SubAppRenderer points to the SPEAK page – VisitorsLayout:
DatasourceSubApprenderer

Here is the final result:
imageedit_7_4142219133

That’s all for now folks 🙂

Advertisements