Technical Articles
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:
- List Report Custom View = Custom Fragment
- Custom Event Handler
- List Report Controller Extension
- 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:
- <<Custom Fragment>> is needed for the Geo Map.
This Fragment must be placed in a Custom Tab which is defined in the manifest.json. - <<Event handler>> is needed to handle the on click event of the spot to navigate to the object page.
- <<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
- Geo Map
- Geo Map Samples
- Contains some examples of the possibilities of the Geo Map.
- SAPUI5 Doc.: API Reference – class sap.ui.vbm.GeoMap
- Contains the methods and events of the Geo Map.
- Geo Map Samples
- Custom Fragment
- In List Report (OData V4 only)
- SAPUI5 Doc.: Extension Points for Views in the List Report (OData V4)
- Extension view in manifest.json: Property template
- SAPUI5 Doc.: Extension Points for Views in the List Report (OData V4)
- In generic
- SAPUI5 Doc.: Require Modules in XML View and Fragment
- See “core:require in Fragments”
- When generating the Custom Fragment and the Event handler, this is generated.
- SAPUI5 Doc.: Require Modules in XML View and Fragment
- In List Report (OData V4 only)
- Custom Event Handler Controller
- I could not find documentation specifically on Event Handler Controllers.
When generating the Custom Fragment and the Event handler, this is generated.
- I could not find documentation specifically on Event Handler Controllers.
- View Controller Extension (OData V4 only)
- List Report specific
- SAPUI5 Doc.: Extension Points for Views in the List Report (OData V4)
- Base controller override public methods: onViewNeedsRefresh(), onPendingFilters()
- SAPUI5 Doc.: Extending List Reports and Object Pages Using App Extensions
- SAPUI5 Doc.: Extension Points for Views in the List Report (OData V4)
- In generic
- SAPUI5 Doc.: Flexible Programming Model – Extension Points (List Report View is OData V4 only)
- See: Custom View in List Report
- SAPUI5 Doc.: Extending List Reports and Object Pages Using App Extensions
- Important note: “SAP Fiori elements provides support only for the official extensionAPI function”.
- When you use the wizard to create the Controller Extension, this code is generated to see how you get the Extension API.
var oModel = this.base.getExtensionAPI().getModel();
- SAPUI5 Doc.: class sap.ui.core.mvc.ControllerExtension (OData V4)
- SAPUI5 Doc.: Using Controller Extension
- This site describes the way Controller Extensions are designed with metadata and override.
- SAP Blog: Leverage the flexible programming model to extend your SAP Fiori elements apps for OData V4
- SAPUI5 Doc.: Flexible Programming Model – Extension Points (List Report View is OData V4 only)
- List Report specific
- Extension API
- List Report specific
- SAPUI5 Doc.: class sap.fe.templates.ListReport.ExtensionAPI (OData V4)
- Method: createFiltersFromFilterConditions(mFilterConditions)
- Method: createFiltersFromFilterConditions(mFilterConditions)
- SAPUI5 Doc.: class sap.suite.ui.generic.template.ListReport.extensionAPI.ExtensionAPI (OData V2)
- SAPUI5 Doc.: class sap.fe.templates.ListReport.ExtensionAPI (OData V4)
- In generic
- SAPUI5 Doc.: class sap.fe.core.ExtensionAPI (OData V4)
- SAPUI5 Doc.: Using the extensionAPI (OData V2 and V4)
- SAP Help: Using the extensionAPI (OData V2 and V4)
- “From the controller, you can access these services through getExtensionAPI.”
- OData V2: sap.suite.ui.generic.template.ListReport.extensionAPI.ExtensionAPI
- OData V4: sap.fe.templates.ListReport.ExtensionAPI
- List Report specific
- Non-View Controller Extensions
- SAPUI5 Doc.: Flexible Programming Model – Controller Extensions
- You can make controller extensions to the standard List Report Controller and standard Object Page Controller, which control the views. But there are more controllers which are not focussed on one-view, but more on screen navigation and editing like EditFlow, IntentBasedNavigation and Routing. That’s why I call it the Non-View Controllers.
These classes are also in package sap.fe.core.controllerextensions.
An instance of these controllers can be retrieved from the Extension API with methods like getEditFlow(), getRouting() and getIntentBasedNavigation(). See these methods in class sap.fe.core.ExtensionAPI (OData V4).
I haven’t tested it out, but it is likely you have to add an entry to the manifest.json in attribute “sap.ui5.extends.extensions.sap.ui.controllerExtensions”, just like a List Report Controller Extension or Object Page Controller Extension.
- You can make controller extensions to the standard List Report Controller and standard Object Page Controller, which control the views. But there are more controllers which are not focussed on one-view, but more on screen navigation and editing like EditFlow, IntentBasedNavigation and Routing. That’s why I call it the Non-View Controllers.
- SAPUI5 Doc.: Flexible Programming Model – Controller Extensions
- Others
- SAP Blog: SAP Fiori Elements Now Supports OData V4 – November 27, 2020
- YouTube List: SAP Fiori elements Flexible Programming Model
- SAP Fiori elements: Extending your application with custom controls
- This is a short video on adding a Custom Fragment and adding a Geo Map to an Object Page. The AnalyticalMap is used, which does not need the map configuration for a map provider. The spots are hard-coded.
- SAP Fiori elements: Extending your application with custom controls
- Outside the lines: SAP Fiori elements flexible programming model
- A time point 23:40 a geo map is added as an already created fragment.