Skip to Content
Technical Articles
Author's profile photo Alwin van de Put

Fiori Elements: Add Geo Map to List Report as Custom View (OData V4)

This blog post describes how you can add a Geo Map to a List Report as a custom view. Based on the Filter the entity locations will be shown as spots on the map. The different extension techniques to extend Fiori Elements are called the Flexible Programming Model.

In the example below there are two tabs called Geo Map and List. The List tab shows the normal list result. The Geo Map shows five moving objects as geo map spots which are selected based on the filter.

 

If the browser is made smaller and the Go button is pressed again, then the zoomlevel and center point will be recalculated so that all selected spots are shown within the screen.

 

The center position of the map and the zoomlevel are calculated in the List Report Controller Extension based on the longitude and latitude distance between the most outside spots. So if we select 2 spots which are further apart the map will zoom out like this example.

 

And zoom out further by selecting spots which are further apart.

 

When clicking on a spot, the app navigates to it’s object page.

The List tab shows the standard List of the List Report.

Table of Contents

  • Introduction
  • Object Overview
  • Create Custom Fragment and Custom Event Handler
  • 1. Custom Fragment
  • 2. Event Handler
  • Create the Controller Extension
  • 3. Controller Extension
  • Documentation

Introduction

A List Report Custom Tab is called a Custom View in a List Report. This topic is currently only applicable to SAP Fiori elements for OData V4, so you have to generate a Fiori Elements List Report app based on OData V4.

These objects are important for creating this extension:

  1. List Report Custom View = Custom Fragment
  2. Custom Event Handler
  3. List Report Controller Extension
  4. Extension API

It is important to use the Extension API Methods and the Controller Extension Override Methods in the logic of the Custom Event Handler and Controller Extension, so that the code stays upgrade-proof.

Object Overview

These development artifacts are needed:

  1. <<Custom Fragment>> is needed for the Geo Map.
    This Fragment must be placed in a Custom Tab which is defined in the manifest.json.
  2. <<Event handler>> is needed to handle the on click event of the spot to navigate to the object page.
  3. <<Controller extension>> is needed to set the map configuration, filter events and calculate the map center position and zoomlevel.

The Custom Event Handler and the Controller Extension have both a reference to the Extension API.

Create Custom Fragment and Custom Event Handler

The Custom Fragment can be generated in the Page Map.

  • Go to the Page Map and click on the pencil of the List Report.

  • Click on the Add Views button.

  • Fill the New Custom View screen

  • Also “Generate Event Handler (Controller)” is set to true to also generate the Event Handler file.
  • Push Add

See that the Fragment (.xml) and Event Handler (.js) files are generated.

And also the manifest.json is extended. In the target “MovingObjectList” the attribute views is added.

By default this shows:

You can change the tab name from “Table View” to “List” by adding annotation @UI.selectionPresentationVariant to the ABAP CDS view with a text attribute.

@UI.selectionPresentationVariant: [
  { qualifier: 'List',
    text: 'List2' }
]

And changing in the manifest.json the annotationPath.

"views": {
    "paths": [
        {
            "key": "tableView",
            "annotationPath": "com.sap.vocabularies.UI.v1.SelectionVariant#List"
        },
        ...

The default generated fragment looks like this.

<core:FragmentDefinition xmlns:core="sap.ui.core" xmlns="sap.m">
  <Button 
    core:require="{ handler: 'zgeomovingobjectlistcustomtabv43/ext/fragment/GeoMapListView.controller'}"
    text="GeoMapListView" press="handler.onPress" />
</core:FragmentDefinition>

Because we selected to generate also the Event Handler, the Button attribute core:require and handler.onPress are added.

The Event Handler files contains an example of handling the handler.onPress event.

sap.ui.define([
    "sap/m/MessageToast"
], function(MessageToast) {
    'use strict';

    return {
        onPress: function(oEvent) {
            MessageToast.show("Custom handler invoked.");
        }
    };
});

1. Custom Fragment

The custom fragment will be filled with the Geo Map.

<core:FragmentDefinition 
    xmlns="sap.m" 
    xmlns:core="sap.ui.core" 

    xmlns:vbm="sap.ui.vbm"
	xmlns:html="http://www.w3.org/1999/xhtml" 
	xmlns:commons="sap.ui.commons"
	xmlns:l="sap.ui.layout"

    xmlns:template="http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1">
		
	<vbm:GeoMap 
		id="GeoMapControllerId" 
		class="zgeo_movingobject-GeoMap"
		width="100%"
		height="99%"

		initialPosition="9.933573;51.898695;0"
		centerPosition="{GeoMapSettings>/centerPosition}"
		zoomlevel="{GeoMapSettings>/zoomlevel}"

		core:require="{handler: 'zgeomovingobjectlistcustomtabv42/ext/fragment/GeomapListViewHandler'}">

		<vbm:vos>
			<vbm:Spots id="zdummy5" 
				click="onClickItem" 
				posChangeable="true"
				scaleChangeable="false"
				items="{path: 'GeoMapSpots>/MovingObject'}">
				<vbm:items>
					<vbm:Spot 
						id="Spot"
						position="{GeoMapSpots>GeoLongitude};{GeoMapSpots>GeoLatitude};0" 
						tooltip="{GeoMapSpots>ObjectTypeText}: {GeoMapSpots>ObjectId} - Long.:{GeoMapSpots>GeoLongitude}, Lat.:{GeoMapSpots>GeoLatitude}"
						type="Success" 
						icon="shipping-status"
						text="{GeoMapSpots>ObjectTypeText}: {GeoMapSpots>ObjectId}" 
						contentOffset="0;0"
						
						click="handler.onSpotClick">
					</vbm:Spot>
				</vbm:items>
			</vbm:Spots>

		</vbm:vos>

	</vbm:GeoMap>

</core:FragmentDefinition>

Highlights

  • Model GeoMapSettings is used for the centerPosition and zoomlevel.
  • Model GeoMapSpots is used for the spots.
  • The spot click event is handled by the Event Handler.

2. Event Handler

When clicking on the Geo Map Spot the app will navigate to the object page. This code is needed for doing that. The comment in the code explains the code.

sap.ui.define([], 
function() {
        
"use strict";

return {

    onSpotClick: function(oControlEvent){

        //Get Moving Object Id
        var oGeoMapViewSpotEvent = oControlEvent.getSource();
        var oGeoMapSpotBindingContext = oGeoMapViewSpotEvent.getBindingContext('GeoMapSpots');
        var MovingObjectId = oGeoMapSpotBindingContext.getProperty('ObjectId');

        //Navigate to Object Page
        var oExtensionAPI = this;
        var oRouting = oExtensionAPI.getRouting();
        oRouting.navigateToRoute(
            'MovingObjectObjectPage',
            { key : MovingObjectId });

    }

}; //Event Handler Object
}); //End function

Create the List Report Controller Extension

Go to the Page Map.

Click in the List Report on the button Show Controller Extensions.

Now two Controller Extensions are selected. The “ListReport” and “#MovingObjectList”. The description in the screen explain the differences.¬†Push in the #MovingObject block the button “Add Controller Extension”.

Fill the screen

  • Push Add

The controller extension is added to the manifest.json. The manifest.json is always the starting point of a Fiori Elements application.

The folder controller is created and the Controller Extension file is added.

It contains the example code:

sap.ui.define(['sap/ui/core/mvc/ControllerExtension'], function (ControllerExtension) {
	'use strict';

	return ControllerExtension.extend('zgeomovingobjectlistcustomtabv43.ext.controller.MovingObjectListCtlrExt', {
		// this section allows to extend lifecycle hooks or hooks provided by Fiori elements
		override: {
			/**
             * Called when a controller is instantiated and its View controls (if available) are already created.
             * Can be used to modify the View before it is displayed, to bind event handlers and do other one-time initialization.
             * @memberOf zgeomovingobjectlistcustomtabv43.ext.controller.MovingObjectListCtlrExt
             */
			onInit: function () {
				// you can access the Fiori elements extensionAPI via this.base.getExtensionAPI
				var oModel = this.base.getExtensionAPI().getModel();
			}
		}
	});
});

3. Controller Extension

The functions / methods are:

  • override onAfterRendering
    This function is called after the screen is rendered. It will call the functions to set the map configuration, attach the filter events and set the initial geo map center position.
  • override onViewNeedsRefresh
    Call function refreshGeoMap.
  • zz_setMapConfiguration
    Set the map configuration to set the geo map provider.
  • zz_setInitialGeoMapPosition
    Sets the initial geo map position to longitude = 0, latitude = 0 and zoomlevel = 1.
  • zz_refreshGeoMap
    It first calls readModelData. The results are added to the models of the GeoMap.
  • zz_readModelData
    First it gets the filter from the filter bar via the extension API. Then it will call the OData service for the requested Moving Objects.
  • zz_getGeoMapSettingsData
    It calculates the geo map center point and the zoom level.
  • zz_getLongitudeRange
    Determines the most left and right longitude values.
  • zz_getLatitudeRange
    Determines the most top and bottom longitude values.
  • zz_calculateZoomLevel
    Calculates based on the longitude range and latitude range the zoomlevel.

Controller Extension code:

sap.ui.define(['sap/ui/core/mvc/ControllerExtension'], function (ControllerExtension) {
	'use strict';

	return ControllerExtension.extend('zgeomovingobjectlistcustomtabv42.ext.controller.MovingObjectListCtlrExt', {

		override: {

			onInit : function(){

			},

			onBeforeRendering: function() {
			},

			onAfterRendering: function() {

				this.zz_ExtensionAPI     = this.base.getExtensionAPI();
				this.zz_GeomapViewTab    = this.zz_ExtensionAPI.byId("fe::CustomTab::GeoMapViewKey");
				this.zz_GeomapController = this.zz_ExtensionAPI.byId("fe::CustomTab::GeoMapViewKey--GeoMapControllerId");

				//This is a work around for keeping a reference to this Controller Extension
				Window.zzMovingObjectListCtlExt = this;

				this.zz_refreshGeoMap.bind(this);

				this.zz_setMapConfiguration();

				this.zz_setInitialGeoMapPosition();

			},

			onViewNeedsRefresh: function(mParameters) {
				//Not mParameters.filterConditions will be used, but extensionAPI.createFiltersFromFilterConditions(mFilterConditions).
				//When the parameter mFilterConditions is not filled, it will retrieve the filter from the Smart Filter bar.
				//Because that one contains the filters in a format which can be used in a model.
				this.zz_refreshGeoMap();

				//TODO: Undo make Geo Map Grey
			},

			onPendingFilters: function(mParameters) {
				//TODO: Make Geo Map Grey
			}

		},



		zz_setMapConfiguration: function(){
			
			//Set Map configuration
			var oMapConfig = {
				"MapProvider": [{
					"name": "OSM",
					"type": "",
					"description": "",
					"tileX": "256",
					"tileY": "256",
					"maxLOD": "20",
					"copyright": "OpenStreetMap",
					"Source": [{
						"id": "s1",
						"url": "https://a.tile.openstreetmap.org/{LOD}/{X}/{Y}.png"
					}]
				}],
				"MapLayerStacks": [{
					"name": "DEFAULT",
					"MapLayer": [{
						"name": "OSMLayter",
						"refMapProvider": "OSM",
						"opacity": "1.0",
						"colBkgnd": "RGB(255,255,255)"
					}]
				}]
			};

			this.zz_GeomapController.setMapConfiguration(oMapConfig);
			this.zz_GeomapController.setRefMapLayerStack("DEFAULT");

		},

		zz_setInitialGeoMapPosition: function(oEvent){

			var oGeoMapSettingsData = {
				centerPosition: "0;0",
				zoomlevel : 1
			}
			var oGeoMapSettingsModel = new sap.ui.model.json.JSONModel(oGeoMapSettingsData);
			this.zz_GeomapController.setModel(null, 'GeoMapSettings');
			this.zz_GeomapController.setModel(oGeoMapSettingsModel, 'GeoMapSettings');

		},

		zz_refreshGeoMap: async function(){

			//Read data
			var oMovingObjectsData = await this.zz_readModelData();

			//Set Geo Map Spots model			
			var oNewModel = new sap.ui.model.json.JSONModel(oMovingObjectsData);
			this.zz_GeomapController.setModel(null, 'GeoMapSpots');
			this.zz_GeomapController.setModel(oNewModel, 'GeoMapSpots');

			//Set Map Position
			var oGeoMapSettingsData = this.zz_getGeoMapSettingsData(oMovingObjectsData);
			var oGeoMapSettingsModel = new sap.ui.model.json.JSONModel(oGeoMapSettingsData);
			this.zz_GeomapController.setModel(null, 'GeoMapSettings');
			this.zz_GeomapController.setModel(oGeoMapSettingsModel, 'GeoMapSettings');

		},

		zz_readModelData : async function(){
		  	//See url: https://www.sapui5tutors.com/2023/06/filtering-sapui5-odata-v4-expanded.html?m=1

		  	//Get to filter array via Extension API
			var oControllerExtension = this;
			var oExtensionApi = oControllerExtension.base.getExtensionAPI();
			var oFilters = oExtensionApi.createFiltersFromFilterConditions();
			var aFilters = oFilters.filters;

			//Create model
			var oCurrentGeoMapModel  = new sap.ui.model.odata.v4.ODataModel({
				groupId : "$direct",
				serviceUrl: "/sap/opu/odata4/sap/zgeo_movingobject_sb_v4/srvd/sap/zgeo_movingobject_sd/0001/",
				synchronizationMode: "None",
				operationMode : sap.ui.model.odata.OperationMode.Server
		  	});
		  	var movingObjectBinding = oCurrentGeoMapModel.bindList("/MovingObject");
			movingObjectBinding.filter(aFilters, false);

			//Read data
			var aMovingObjectsContextArray = await movingObjectBinding.requestContexts();

			//Convert data
			var Result = { "MovingObject": []};

			aMovingObjectsContextArray.map(
				function(MovingObjectContext){
					var MovingObjectData = MovingObjectContext.getObject();
					Result.MovingObject.push(MovingObjectData);
				}
		  	);

			return Result;

	  	},

		zz_getGeoMapSettingsData : function(oMovingObjectsData){
            
			if(oMovingObjectsData.MovingObject.length == 0){
				return  {
					centerPosition: "0;0",
					zoomlevel : 1 };
			}

			//Determine shortest side
			var iWidthPx  = this.zz_GeomapController.$().width();
			var iHeightPx = this.zz_GeomapController.$().height();

			//Longitude
			var oLongitudeRange    = this.zz_getLongitudeRange(oMovingObjectsData);
			var iDistance = Math.abs(oLongitudeRange.Left - oLongitudeRange.Right);
			var iLongitudeZoomLevel = this.zz_calculateZoomLevel(iDistance, iWidthPx, 1);
			var iCenterLongitude = (oLongitudeRange.Left + oLongitudeRange.Right ) /2;

			//Latitude
			var oLatitudeRange    = this.zz_getLatitudeRange(oMovingObjectsData);
			var iDistance         = Math.abs(oLatitudeRange.Left - oLatitudeRange.Right);
			var iLatitudeZoomLevel = this.zz_calculateZoomLevel(iDistance, iHeightPx, 0.8);
			var iCenterLatitude   = (oLatitudeRange.Left + oLatitudeRange.Right ) /2;

			//Determine Zoom Level
			if(iLongitudeZoomLevel < iLatitudeZoomLevel){
				var iZoomlevel = iLongitudeZoomLevel;
			}else{
				var iZoomlevel = iLatitudeZoomLevel;
			}

			//Set Geo Map Settings
			var oGeoMap = {
				centerPosition: `${iCenterLongitude};${iCenterLatitude}`,
				zoomlevel : iZoomlevel }
			
			oGeoMap.ItemCountText = "Test";

			return oGeoMap;

		},

		zz_getLongitudeRange : function(oMovingObjectsData){

			var oLongitudeRange = {
				Left : 0,
				Right : 0
			}

			for (let i = 0; i < oMovingObjectsData.MovingObject.length; i++) {
				var MovingObject = oMovingObjectsData.MovingObject[i];
				if(i === 0){
					oLongitudeRange.Left  = Number(MovingObject.GeoLongitude);
					oLongitudeRange.Right = Number(MovingObject.GeoLongitude); 
				}else{
					if(Number(MovingObject.GeoLongitude) < oLongitudeRange.Left ){ oLongitudeRange.Left  = Number(MovingObject.GeoLongitude) };
					if(Number(MovingObject.GeoLongitude) > oLongitudeRange.Right){ oLongitudeRange.Right = Number(MovingObject.GeoLongitude) };
				}
			}

			return oLongitudeRange;

		},

		zz_getLatitudeRange : function(oMovingObjectsData){

			var oLatitudeRange = {
				Left : 0,
				Right : 0
			}

			for (let i = 0; i < oMovingObjectsData.MovingObject.length; i++) {
				var MovingObject = oMovingObjectsData.MovingObject[i];
				if(i === 0){
					oLatitudeRange.Left  = Number(MovingObject.GeoLatitude);
					oLatitudeRange.Right = Number(MovingObject.GeoLatitude); 
				}else{

					if(Number(MovingObject.GeoLatitude) < oLatitudeRange.Left ){
						oLatitudeRange.Left  = Number(MovingObject.GeoLatitude) };

					if(Number(MovingObject.GeoLatitude) > oLatitudeRange.Right){
						oLatitudeRange.Right = Number(MovingObject.GeoLatitude) };
				}
			}

			return oLatitudeRange;

		},

		zz_calculateZoomLevel : function(iDistance, iLengthPx, iCorrectionFactor){

			//When wanting to zoom out more, then iCorrectionFactor < 1.

			var LongitudeZoomLevel = 0;
			var iFactor = ( iLengthPx / 500 ) * iCorrectionFactor;

			if(iDistance <=   0.001  * iFactor ){ LongitudeZoomLevel = 19 } else
			if(iDistance <=   0.002  * iFactor ){ LongitudeZoomLevel = 18 } else
			if(iDistance <=   0.004  * iFactor ){ LongitudeZoomLevel = 17 } else
			if(iDistance <=   0.008  * iFactor ){ LongitudeZoomLevel = 16 } else
			if(iDistance <=   0.015  * iFactor ){ LongitudeZoomLevel = 15 } else
			if(iDistance <=   0.03   * iFactor ){ LongitudeZoomLevel = 14 } else
			if(iDistance <=   0.06   * iFactor ){ LongitudeZoomLevel = 13 } else
			if(iDistance <=   0.12   * iFactor ){ LongitudeZoomLevel = 12 } else
			if(iDistance <=   0.25   * iFactor ){ LongitudeZoomLevel = 11 } else
			if(iDistance <=   0.5    * iFactor ){ LongitudeZoomLevel = 10 } else
			if(iDistance <=   1      * iFactor ){ LongitudeZoomLevel =  9 } else
			if(iDistance <=   2      * iFactor ){ LongitudeZoomLevel =  8 } else
			if(iDistance <=   4      * iFactor ){ LongitudeZoomLevel =  7 } else
			if(iDistance <=   8      * iFactor ){ LongitudeZoomLevel =  6 } else
			if(iDistance <=  16      * iFactor ){ LongitudeZoomLevel =  5 } else
			if(iDistance <=  30      * iFactor ){ LongitudeZoomLevel =  4 } else
			if(iDistance <=  60      * iFactor ){ LongitudeZoomLevel =  3 } else
			if(iDistance <= 120      * iFactor ){ LongitudeZoomLevel =  2 } else
			{ LongitudeZoomLevel = 1 };

			return LongitudeZoomLevel;

		}

	});
});

Documentation

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.