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:
The GoogleMapSpeak1 Paremeters.template:
The Droplist points to the MapTypes folder.
Here is the rendering, GoogleMapSpeak1, with the “properties”(parameters):
I really tried to change/set the sort order on the parameters(properties) but it seems to sort in alphabetical order only 😦
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):
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”:
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&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 < arguments.length - 1; i++) { var reg = new RegExp("\\{" + i + "\\}", "gm"); 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() + "-" + s4() + "-4" + s4().substr(0, 3) + "-" + s4() + "-" + 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:
Lets take a look at it in the Design Layout.
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:
That’s all for now folks 🙂
This is awesome, well done Göran 🙂
LikeLiked by 1 person
Thank you 🙂
LikeLike
Good job
LikeLiked by 1 person
Thank you 🙂
LikeLike
ok. This is so cool. Good job!
You should make this into a package and move it to marketplace.sitecore.net.
LikeLiked by 1 person
Thank you very much 🙂 Yes good idea, I will do a package of it and put it in the market place. I’ll let you know when it’s done.
LikeLike
I just want you to know that The Google Map SPEAK component is now released in Sitecore Marketplace,
GoogleMap SPEAK component released in Sitecore Marketplace
LikeLike