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:
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
Locating the Chicago Cloud Gate using Google Maps API
Note the following:
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!
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.
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:
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:
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.
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”.
Here is the final result:
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:
Mapping crime data in HANA cloud with google maps thanks to @jdelvat's Lumira extension http://t.co/4s1ZOjqjnM pic.twitter.com/PQnp1mEQPf
— Robert Russell (@rjruss) November 18, 2014
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
8 | |
7 | |
4 | |
4 | |
4 | |
3 | |
3 | |
3 | |
3 | |
3 |