Skip to Content

SAP Lumira Extension: Google Maps

This blog post is the first part of my entry for the 2014 Data Geek Challenge.

Edit Nov 18th, 2014: addedRobert Russell ‘s application with crime data at the end

Edit Nov 24th, 2014: integrated coding improvements suggested by Matt Lloyd and David Dalley

During my research for the Data Geek Challenge, I found out that the map options available within SAP Lumira are focused on country / region / city. However, for the analysis of Delays for Buses in Chicago, I wanted to go down to the street level. Therefore, I took this challenge as an opportunity to extend SAP Lumira with the Google Maps API.

Before I go any further, I need to thank Matt Lloyd and Manfred Schwarz for their very detailed blog posts on SAP Lumira Extensions, as well as the documentation, especially the debug part. I will assume you have read these articles prior to reading this exercise. Also, David Dalley made valuable contributions to the code through GitHub. Lastly, I only tested this script with SAP Lumira 1.20.

Fast Track


If you are interested in testing the Google Maps API Extension without the development phase, you can simply download the coding from GitHub. Click on “Download ZIP”. Extract the files and merge them in “C:\Program Files\SAP\Lumira\Desktop\extensions”. Don’t forget to restart your Lumira.

CostingGeek/LumiraGoogleMaps · GitHub


Here’s the expected result:

SAP_Lumira_Extension_Google_Maps_Result.png

Prerequisites:

You probably need to know a little about Google Maps API. There are actually 2 types: static or dynamic. Since we would like the end user to be able to move around in the map and zoom in / out, we’ll need to focus on the dynamic one. Here’s a very small example (created as a .html file):

<!DOCTYPE html>

<html>

<head>

  <title>Google Map Test</title>

  <style>html, body, #map-canvas { height: 100%; margin: 0px; padding: 0px; }</style>

  <script src=”https://maps.googleapis.com/maps/api/js?v=3.exp&language=en“></script>

  <script>

  function initialize() {

    var chicago = new google.maps.LatLng(41.850033,-87.6500523);

    var mapOptions = { zoom: 8, center: chicago };

    var map = new google.maps.Map(document.getElementById(‘map-canvas’), mapOptions);

  }

  </script>

</head>

<body onload = “javascript:initialize();”>

  <div id=”map-canvas”></div>

</body>

</html>

This example can be found here: http://www.costinggeek.com/data_geek_3/map_test.html

Picture_01.png

Locating the Chicago Cloud Gate using Google Maps API


Note the following:

  1. Line 6: <script src=”https://maps.googleapis.com/maps/api/js?v=3.exp&language=en“></script>
    This loads the script and should happen in the <head> section. This is not possible in Lumira because of the RequireJS infrastructure
  2. Line 18: <div id=”map-canvas”></div>
    By default, Lumira uses SVG elements instead of DIV, so keep this in mind.

More documentation on Google Maps APIs here:

Solving the Asynchronous Call

In the above Lumira extension examples, one can add a script in the render.js. However, RequireJS and Google Maps API are not compatible because of the order in which the scripts are called. The solution is to perform an asynchronous call to the Google Maps API. Thanks to Miller Medeiros, a Brazilian designer and developer, such a script is supported under the MIT license and can be downloaded here: https://github.com/millermedeiros/requirejs-plugins

Simply download the “src/async.js” script from GitHub and indicate in your coding where the file is located. This is done in the requires.config call. A typical coding example looks like this:

requirejs.config({

    baseUrl : ‘<your_base_url>’,

    paths: {

        ‘async’: ‘<folder_to_async_file>’

    }

});

require([‘async!https://maps.googleapis.com/maps/api/js?v=3.exp&language=en&sensor=false‘], function ( ) {

    // Some coding here

});

For the moment, place it in: C:\Program Files\SAP\Lumira\Desktop\utilities\VizPacker\libs\js

Preparing VizPacker

We now have most of the pieces of the puzzle. Start your VizPacker like in the articles above. This time, don’t forget to check the “Use DIV Container” flag. As detailed in the javascript warning, all code changes will be lost, so do this first!

Picture_02.png

Notice a DIV named “chart” inserted on the HTML tab:

<!– <<container –>

    <div id=”chart” style=”position: absolute; left:0px; right: 0px; top:0px; bottom:0px; background-color: #ffffff”></div>

<!– container>> –>

Be careful when setting the properties of your chart. For every “.” you use in the Chart Id, a sub-hierarchy will be created, that needs to be reproduced. In my example, the chart id is “com.costinggeek.googlemaps”, which is a 3-level hierarchy.

Picture_03.png

Sample Data

VizPacker has its own way of testing data. Prepare a CSV file with the following data:

“Latitude”,”Longitude”,”Description”,”Quantity”

“49.293523”,”8.641915″,”Building WDF 01″,1000

“49.294583”,”8.642838″,”Building WDF 02″,500

“49.292876”,”8.644190″,”Building WDF 03″,750

“49.294149”,”8.644115″,”Building WDF 04″,350

“49.292246”,”8.639973″,”Building WDF 05″,50

Then, on the Data Model tab, upload your data from that CSV file and start the mapping:

  • Assign dimension “Latitude” to Set 1,
  • Assign dimension “Longitude” to Set 1,
  • Assign dimension “Description” to Set 1,
  • Assign measure “Quantity” to Set 1,
  • Rename dimension Set 1 to “Latitude / Longitude / Desc”,
  • Rename measure Set 1 to “Quantity”,
  • Click on Apply and confirm the warning.

Configuring RequireJS

In the render.js, remove all sample coding between

// START: sample render code for a column chart

and

// END: sample render code

Remove this section since we won’t need any SVG component:

var vis = container.append(‘svg’).attr(‘width’, width).attr(‘height’, height)

.append(‘g’).attr(‘class’, ‘vis’).attr(‘width’, width).attr(‘height’, height);

Then, insert the following coding:

require.config({

    paths: {

        ‘com_costinggeek_googlemaps-async’: ‘sap/bi/bundles/com/costinggeek/googlemaps/com_costinggeek_googlemaps-src/js/async’

    }

});

// add DIV but make sure it’s done only once

var mapsContainer = container.select(‘div’);

if (!mapsContainer.node()) {

    mapsContainer = container.append(‘div’).attr(‘width’, ‘100%’).attr(‘height’, ‘100%’).attr(‘class’, ‘com_costinggeek_googlemaps-cg_map’);

}

// create asynchronous call to google maps api

require([‘com_costinggeek_googlemaps-async!https://maps.googleapis.com/maps/api/js?v=3.exp&language=en&sensor=false‘], function ( ) {

    // call google maps API after everything is loaded

    load_gmap();

});

// MDL: Get the name of the dimension columns from dimension group: Latitude / Longitude / Desc

var dimArr_latLongDesc = data.meta.dimensions(‘Latitude / Longitude / Desc’);

var dim_lattitude   = dimArr_latLongDesc[0];

var dim_longitude   = dimArr_latLongDesc[1];

var dim_description = dimArr_latLongDesc[2];

// MDL: end

// MDL: Get the name of the measure column from the measure group: Quantity

var msrArr_Qty = data.meta.measures(‘Quantity’);

var msr_Quantity = msrArr_Qty[0];

// MDL: end

// set global variable accessible by all sub-functions

var my_map;

var my_LatLng;

var my_LatLngBounds;

// function to show popup when markers are clicked

function attach_details( my_marker, my_description, my_quantity ) {

    var infowindow = new google.maps.InfoWindow(

    {

content: ‘<div class=”com_costinggeek_googlemaps-infoWindow”><strong>’ + my_description + ‘: </strong> ‘ + my_quantity + ‘</div>’

    });

    google.maps.event.addListener( my_marker, ‘click’, function() {

infowindow.open( my_map, my_marker );

    });

}

// function to place a marker on the map

function add_marker_lat_long( my_description, my_lat, my_long, my_quantity ) {

    my_LatLng = new google.maps.LatLng( my_lat, my_long );

    my_LatLngBounds.extend( my_LatLng );

    var my_marker = new google.maps.Marker({

map:  my_map,

position:     my_LatLng,

//icon:       icon_shop,

title:my_description,

status:     ‘active’

    });

    attach_details( my_marker, my_description, my_quantity );

}

// initialize the google map

function load_gmap( ) {

    my_LatLngBounds = new google.maps.LatLngBounds();

    var my_center = new google.maps.LatLng( 49.2933, 8.6419 );

    var mapOptions = {

        mapTypeId: google.maps.MapTypeId.ROADMAP,

        center:    my_center,

        zoom:10

    };

    my_map = new google.maps.Map(mapsContainer.node(),

        mapOptions);

    // convert all data to markers

    var j = 0;

    for ( var i = 0; i < data.length; i++ )

    {

        // MDL: Updated to use column names from data set.

        if( data[i][dim_lattitude] != undefined && data[i][dim_longitude] != undefined )

        // MDL: end

        {

            // MDL: Updated to use column names from data set.

            add_marker_lat_long( data[i][dim_description], data[i][dim_lattitude],  data[i][dim_longitude], data[i][msr_Quantity] );

            // MDL: end

            j++;

        }

    }

    // Auto center the map based on given markers

    if( j > 0 )

    {

        my_map.fitBounds(    my_LatLngBounds );

        my_map.panToBounds( my_LatLngBounds );

    }

}

Styling

On the style.css tab, replace the whole content with your preferred options. For instance:

.com_costinggeek_googlemaps-cg_map{

    border: solid 1px black;

    width:  100%;

    height: 100%;

}

.com_costinggeek_googlemaps-infoWindow {

    width:   250px;

    padding:  10px;

}

Testing in VizPacker

Once all the coding is in place, you just need to open the preview window and click the “Run Code” button to let the magic happen:

Picture_04.png

Testing in Lumira

Now that your script is ready in VizPacker, it is time for its final test in Lumira. Click on the “PACK” button and extract your files to your Lumira extension folder, typically “C:\Program Files\SAP\Lumira\Desktop\extensions”. Your folder hierarchy should look close to this picture.

Picture_05.png

Then, copy your “async.js” file into the “js” folder, so it is alongside the “render.js” file. Based on Manfred Schwarz’s research, we also know that some changes have to be made in render.js. Located the require.config section and update the path as follows (beware typos):

require.config({

    paths: {

    ‘async’: ‘sap/bi/bundles/com/costinggeek/googlemaps/com_costinggeek_googlemaps-src/js/async’

    }

});

Start Lumira (restart if it was open) and create a new dataset with the sample CSV file. In the Prepare section, remove all measures, except “Quantity”. In Dimensions, click on the gear icon next to Latitude and choose “Display Formatting”. Make sure the number of decimals is set to 6 (by default, Lumira truncates to 2 decimals and you would end up with only one marker). In Visualize, select your new extension. As Dimensions, select “Latitude”, “Longitude”, and “Description”. As Measure, select “Quantity”.

Picture_06.pngPicture_07.png

Here is the final result:

SAP_Lumira_Extension_Google_Maps_Result.png


Conclusion

I hope you were able to follow this process to extend your SAP Lumira with Google Maps. Do not hesitate to ask your questions in the comments section below. If this was useful, let us know what you did with it!

Update 11/18/14: Thanks to Robert Russell for sharing his application with crime data:

49 Comments
You must be Logged on to comment or reply to a post.