Happy #APIFriday! I will be starting a new series where I will post (small) API apps and their how-tos every Friday (or Thursday night since I’m in California) to showcase how to integrate different APIs out there into your SAP UIs. I’ll share some pitfalls, tricks, and tips I pick up while building the apps as well as code snippets so your can try it yourself. If you have any suggestions of APIs you would like to see incorporated into SAP or you have some feedback on this series, send a tweet to me on Twitter. I can be found at @mlhassett and use the hashtag #APIFriday.

 

Let’s use Google Maps JS, OSRM, and UI5 to make a quick app for your Fiori launchpad!

 

Using the Google Maps JS library, you can make a quick app that routes you to your preselected destinations for Home, Work, and Airport. Google Maps provides the overlay of the traffic as well as the directions route, but the Google Maps Distance Matrix doesn’t figure current traffic conditions into its estimated time of travel. If you have every lived in or been to the Bay Area, you know that there isn’t a time of day when there isn’t traffic, so I needed to find something that considered road traffic conditions. Thats how I found OSRM, or Open Source Routing Machine. OSRM considers traffic when providing a route and time estimate.

 

To get started using the Google Maps JS library in your UI5 application, I would suggest using this GitHub project as a good starting point. This will help you understand how to invoke the library for use in your controllers and views, as well as how you have to manipulate the DOM for the library.

For my application, I added a start page with tiles to select the route you want to see. I updated the index.html file to use the UI5 standard of adding a Shell to the Core.

sap.ui.getCore().attachInit(function() {
				new sap.m.Shell({
					app: new sap.ui.core.ComponentContainer({
						height : "100%",
						name : "smartmove.appSmarMove"
					})
				}).placeAt("content");
			});

 

However, this means that your DOM manipulation is going to have to change a bit. In the GitHub Google Maps project, find the Maps.controller.js file. The line where the map is defined:

map = new google.maps.Map(document.getElementById('idMaps1--map_canvas'),mapOptions);

UI5 automatically generates the tags for the elements in the DOM even if you define them like they did in the view:

<HBox id="map_canvas" fitContainer="true" height="900px">
		</HBox>

If you add a page infront of the map like I did, you may have to play around with your app a bit by running your application to figure out the pregenerated tag pattern. I found in my app that __xmlview#-– was prepended to my view ID tags, so my code for invoking the map looks like this:

	map = new google.maps.Map(document.getElementById('__xmlview2--map_canvas'), mapOptions);

Another caveat to this is that the view # is based on ordered clicked meaning that if I don’t click on my tiles in a specific order, this call won’t work because the Work view is not long view2 and Home is no longer view3 — still working out this kink.

This is all just to get the map to render. Don’t forget, if you want to allow navigation in your application, you will have to enable it in your manifest.json file and Component.js. Instructions can be found in this tutorial. Since I didn’t need to pass data in the URL to know what view to load, my routes are a little simpler with no pattern matching needed.

"routes": [
				{
					"pattern": "",
					"name": "main",
					"target": "main"
				},
				{
					"pattern": "toWork",
					"name": "work",
					"target": "work"
				},

Back to the map!

If you look into Google Maps JS Library, it is pretty simple to add features such as traffic and directions. To later directions onto my map, I created a simple function that used the Google Maps directions services. Google Maps documentation has convenient code snippets. All their code is done in an index file, so you will have to copy the code out of a script tag and add it to a function in your controller. The main key is learning how to use the documentation in a MVC application.

getDirections: function(){
		var directionsService = new google.maps.DirectionsService;
		var directionsDisplay = new google.maps.DirectionsRenderer;
		directionsDisplay.setMap(map);
		directionsService.route({
	          origin: homeDefault,
	          destination: workDefault,
	          travelMode: 'DRIVING'
	        }, function(response, status) {
	          if (status === 'OK') {
	            directionsDisplay.setDirections(response);
	          } else {
	            window.alert('Directions request failed due to ' + status);
	          }
	        });	
}

Adding traffic is even easier. I add the code to my initMap function.

var trafficLayer = new google.maps.TrafficLayer();
trafficLayer.setMap(map);

Now routing gets a bit tricky. The OSRM api requires geocodes for addresses, so lat long coordinates. Fortunately, Google Maps does have a geocoder. Unfortunately, it is asynchronous and I need to encode 2 addresses and I need to know that both are done being geocoded before I can get my directions from OSRM. To accomplish this, I created another function in my controller that will geocode one address at a time. In order to force it to geocode in order, I geocode the 2nd address in the callback of the 1st geocode. Once the 2nd geocode completes, I know I can move on to getting the distance and time, so I will call that function in the 2nd callback.

codeAddress: function() {
			var self = this;
			geocoder.geocode( { 'address': homeDefault}, function(results1, status1) {
		      if (status1 == 'OK') {
		    	homeGeocode = results1[0].geometry.location;
		    	geocoder.geocode( { 'address': workDefault}, function(results2, status2) {
			      if (status2 == 'OK') {
			    	workGeocode = results2[0].geometry.location;
					self._getDistance();
			      }else {
			        alert('Geocode was not successful for the following reason: ' + status2);
			      }
		    	});
		      } else {
		        alert('Geocode was not successful for the following reason: ' + status1);
		      }
		    });
		  }

My _getDistance function will use the OSRM API to determine how long it will take to get from home to work or vice versa. I made it private because it should only be called when I know that geocoding is complete. I don;t want random button clicks in the view to be able to access it directly!

_getDistance: function(){
			
	var sUrl= 'https://router.project-osrm.org/route/v1/driving/';
				
	$.ajax({
		type: 'GET',
		url: sUrl + homeGeocode.lng() + "," + homeGeocode.lat() + ";" + workGeocode.lng() 
					+ "," + workGeocode.lat(), 
		async: true
		}).done(function(results){
		        var outputDiv = document.getElementById('__xmlview2--output');
	                outputDiv.innerHTML = '';
	                outputDiv.innerHTML += 'Home to Work: ' + Math.round(results.routes[0].distance/1000) + " KM;   Approx " 
		                    + Math.round(results.routes[0].duration/60) + " minutes " + '<br>';

	});
}

Since this AJAX call is asynchronous as well, we have to do all the DOM manipulation in the callback function, which in this case is done(). In my view, I have a toolbar with the id of output to display the distance and time. This will lay on top of the map to show these details.

<Toolbar>
	<ToolbarSpacer></ToolbarSpacer>
	<Label id="output"/>
	<ToolbarSpacer></ToolbarSpacer>
</Toolbar>

Now to actually have these cool new features show up on your map, you need to actually call the functions we just created. Add the function calls to your onAfterRender() function, and voila directions with traffic considerations!

this.getDirections();
this.codeAddress();

In the spirit of transparency, I did hard code a decent amount of this 😳 but I am always working on improvements. 2 things I noticed that are still presenting problems are 1. the xmlview# prepended to my ids for my tags and 2. launching this as a fiori app. When you run an app on the Fiori launchpad, it ignores the index.html file, which means we aren’t loading the Google Maps JS library and therefore it doesn’t understand what google.maps.Map is. I tried adding it to the Component.js file because we still have that in Fiori launchpad, but it doesn’t load the library before the app tries to render. There is a Google Maps library for OpenUI5, so maybe I try my app with that, but if you, the community, have any recommendations for my issues or suggestions on how to improve the app, I would love to hear them!

 

 

Join me next Friday for a how-to on creating a tile to Call an Uber in Fiori Launchpad!

To report this post you need to login first.

3 Comments

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

  1. Sergio Guerrero

    Meredith,

    this is a very good blog – thank you for sharing… in regards to your issue with the DOM elements having the prefix view name. It is frustrating, I understand 🙂 I have a way to hopefully help you out. This is what I have done.

    Since ui5 is based on html5, js, css, etc… you know that the html elements may have a data-* attribute, therefore, if you set your data-* attribute then the HTML rendering is done without altering that id. Furthermore, from your controller (js code) you can read the element that has that data attribute (without needing to know the view id)

    here is the documentation for the data-* attribute from the sapui5 page :

    https://sapui5.hana.ondemand.com/#docs/guide/1ef9fefa2a574735957dcf52502ab8d0.html

     

     

    Hope this helps you.. thank you again for sharing all these API blogs. Keep it up!

     

     

     

     

     

    (0) 

Leave a Reply