You can find the series and project from Real-time sentiment rating of movies on SAP HANA (part 6) – wrap-up and looking ahead

Intro

Hi everyone, welcome to the movie world again. In the previous blog post, we’ve used SAP HANA Info Access to build our smart app on desktop. With the simplicity of Info Access Dev Toolkit and its underneath text analysis feature in SAP HANA, it’s possible for us to build such a powerful and fancy app in a short time. However, the UI framework of Info Access Dev Toolkit is more focused on the desktop instead of tablet or phone. When I used my iPhone to visit the app, the UI looked like the following.

27.PNG28.PNG

Obviously the UI is not responsive as you can see, since it’s for big screens. Usually we don’t analyze so many things at the same time on our mobiles, as mobiles are for simple operations. It’s crazy for you to show six charts and one list in only one screen on your mobile with drag and drop features. Do you like that? I think nobody likes. Since the scenario of our movie sentiment rating app is very simple, an idea came to my mind, how about build our app on mobile devices? And you can use it to pick up the best movie when you go to the cinema. It sounds cool. 😎 Let’s do it together!

OData services

When we use SAP HANA Info Access, we just need an attribute view for the date layer. Since now we want to use SAPUI5 to build our movie app on mobile devices, first we need to expose few OData services for models in SAPUI5. Here I won’t explain what OData is and how to use OData in SAP HANA XS, but you can find details from Home | OData – The Protocol for REST APIs and Data Access with OData in SAP HANA XS – SAP HANA Developer Guide – SAP Library

In our case, we want to create the following two OData services. (It seems the syntax highlighting still doesn’t work. Sorry for the ugly formatting again. 🙁 )

1. movies.xsodata


service namespace "movieRating.services" {
"movieRating.models::CV_MOVIES" as "CV_MOVIES"
key generate local "GenID"
parameters via entity "InputParams"
;}

















The above OData service exposes the calculation view with input parameters described in Real-time sentiment rating of movies on SAP HANA (part 3) – text analysis and modeling. Since there are huge amounts of movies, we need to give two input parameters in order to get the limited results, one for start date and the other for end date. It will be used for displaying brief movie info including the movie title, poster, release date, rating and # of mentions. We can get the brief movie info in mention descending order with release date from 2014-11-11 to 2014-11-17 as follows.

30.PNG

2. tweets.xsodata


service namespace "movieRating.services" {
"movieRating.models::AT_TWEETS" as "AT_TWEETS"
with("movie_id", "user_screen_name", "user_profile_image_url", "sent", "created_at_str", "text", "id_counter")
key generate local "GenID"
;}
















For this OData service, it’s based on the attribute view in Real-time sentiment rating of movies on SAP HANA (part 3) – text analysis and modeling with limited attributes. We’ll use it to show related mentions when we click a specific movie. For each mention, we want to show the user name, his/her profile image, the creation time of the tweet, the text and detected sentiment. We can also test the OData service to get the latest 20 mentions given a specific movie ID.

31.PNG

SAPUI5 – sap.m

Now let’s use SAPUI5 to build the responsive UI for our movie app. So what is SAPUI5? SAPUI5 stands for SAP UI Development Toolkit for HTML5 and you can find details from SAPUI5 Developer Guide for SAP HANA – SAP Library. Since we want to build our movie app on mobile devices, we need to build views with sap.m library instead of sap.ui.commons library. So what is the difference between these two libraries? sap.ui.commons is for desktop/big screen and it’s not responsive, meanwhile sap.m is for desktop/tablet/phone and it’s responsive which means sap.m can adapt to the mobile OS, browser and screen size. You can also find more from this unit of “Next Steps in Software Development on SAP HANA” on openSAP. I just learned this unit today. 😛

Structure

We decided to use sap.m to build our mobile app, but how can we start? First I highly recommend you to have a look at Model View Controller (MVC) – SAPUI5 Developer Guide for SAP HANA – SAP Library, since MVC is a very important concept in SAPUI5.

32.PNG

And you can also find a very detailed demo from An MVC Demo Application – SAPUI5 Developer Guide for SAP HANA – SAP Library. It’s a perfect demo for MVC concept in SAPUI5 and the structure is similar with ours. The following image shows the structure of our mobile app.

29.PNG

  • The “appView” only holds sap.m.App control to handle the page navigation between two pages “homeView” and “detailView”.
  • The “homeView” is displayed initially with two controls. sap.m.DateRangeSelection is used for date range selection, while sap.m.Carousel is used to show the brief movie info.
  • The “detailView” will be showed when we click a movie in the carousel. It consists of several controls including sap.m.PullToRefresh and sap.m.List. With sap.m.PullToRefresh, we can pull to refresh sap.m.List to show the latest mentions. In addition, we also add the option “Load more mentions” which let the user to load more old mentions.

Implementation

index.html


<!DOCTYPE HTML>
<html>
  <head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta http-equiv='Content-Type' content='text/html;charset=UTF-8'/>
  <link rel="stylesheet" type="text/css" href="styles/style.css" />
  <script src="/sap/ui5/1/resources/sap-ui-core.js"
  id="sap-ui-bootstrap"
  data-sap-ui-libs="sap.m"
  data-sap-ui-theme="sap_bluecrystal">
  </script>
  <!-- only load the mobile lib "sap.m" and the "sap_bluecrystal" theme -->
  <script>
  sap.ui.localResources("movieratingmobile");
  var app = sap.ui.view({id:"appView", viewName:"movieratingmobile.app", type:sap.ui.core.mvc.ViewType.JS});
  app.placeAt("content");
  </script>
  </head>
  <body class="sapUiBody" role="application">
  <div id="content"></div>
  </body>
</html>


app.view.js


sap.ui.jsview("movieratingmobile.app", {
  /** Specifies the Controller belonging to this View.
  * In the case that it is not implemented, or that "null" is returned, this View does not have a Controller.
  * @memberOf movieratingmobile.app
  */
  getControllerName : function() {
  return "movieratingmobile.app";
  },
  /** Is initially called once after the Controller has been instantiated. It is the place where the UI is constructed.
  * Since the Controller is given to this method, its event handlers can be attached right away.
  * @memberOf movieratingmobile.app
  */
  createContent : function(oController) {
  var home = sap.ui.view({id:"homeView", viewName:"movieratingmobile.home", type:sap.ui.core.mvc.ViewType.JS});
  var detail = sap.ui.view({id:"detailView", viewName:"movieratingmobile.detail", type:sap.ui.core.mvc.ViewType.JS});
  return new sap.m.App("app").addPage(home).addPage(detail);
  }
});








app.controller.js


sap.ui.controller("movieratingmobile.app", {
/**
* 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 movieratingmobile.app
*/
  onInit: function() {
  var date_to = new Date();
  var date_from = new Date(date_to.getTime() - 6 * 24 * 60 * 60 * 1000);
  var oView = this.getView();
  var oModel = new sap.ui.model.json.JSONModel("/movieRating/services/movies.xsodata/InputParams(date_from=datetime'" + this.dateString(date_from) + "',date_to=datetime'" + this.dateString(date_to) + "')/Results/?$format=json&$orderby=mention desc");
  oView.setModel(oModel);
  var dateRange = sap.ui.getCore().byId("dateRange");
  dateRange.setDateValue(date_from);
  dateRange.setSecondDateValue(date_to);
  this.app = sap.ui.getCore().byId("app");
  var oBus = sap.ui.getCore().getEventBus();
  oBus.subscribe("nav", "to", this.navToHandler, this);
  oBus.subscribe("nav", "back", this.navBackHandler, this);
  },
/**
* Similar to onAfterRendering, but this hook is invoked before the controller's View is re-rendered
* (NOT before the first rendering! onInit() is used for that one!).
* @memberOf movieratingmobile.app
*/
// onBeforeRendering: function() {
//
// },
/**
* Called when the View has been rendered (so its HTML is part of the document). Post-rendering manipulations of the HTML could be done here.
* This hook is the same one that SAPUI5 controls get after being rendered.
* @memberOf movieratingmobile.app
*/
// onAfterRendering: function() {
//
// },
/**
* Called when the Controller is destroyed. Use this one to free resources and finalize activities.
* @memberOf movieratingmobile.app
*/
// onExit: function() {
//
// }
  dateString: function(date) {
  var year = date.getFullYear();
  var month = date.getMonth() + 1;
  month = month > 9 ? month : "0" + month;
  var day = date.getDate();
  day = day > 9 ? day : "0" + day;
  return year + '-' + month + '-' + day;
  },
  navToHandler: function(channelId, eventId, data) {
  if (data && data.id) {
  // lazy load view
  if (this.app.getPage(data.id) === null) {
  jQuery.sap.log.info("now loading page '" + data.id + "'");
  this.app.addPage(sap.ui.jsview(data.id, "movieratingmobile." + data.id));
  }
  // Navigate to given page (include bindingContext)
  this.app.to(data.id, data.data.context);
  } else {
  jQuery.sap.log.error("nav-to event cannot be processed. Invalid data: " + data);
  }
  },
  navBackHandler: function() {
  this.app.back();
  }
});







home.view.js


sap.ui.jsview("movieratingmobile.home", {
  /** Specifies the Controller belonging to this View.
  * In the case that it is not implemented, or that "null" is returned, this View does not have a Controller.
  * @memberOf movieratingmobile.home
  */
  getControllerName : function() {
  return "movieratingmobile.home";
  },
  /** Is initially called once after the Controller has been instantiated. It is the place where the UI is constructed.
  * Since the Controller is given to this method, its event handlers can be attached right away.
  * @memberOf movieratingmobile.home
  */
  createContent : function(oController) {
  var poster = new sap.m.Image({
  densityAware: false,
  src: {
  path: "poster",
  formatter: function(fValue) {
  return fValue.replace(/tmb/, "det");
  }
  },
  press: [oController.showDetail, oController]
  });
  var title = new sap.m.Text({
  text: "{title}"
  }).addStyleClass("title");
  var release_date = new sap.m.Text({
  text: "{release_date}"
  });
  var rating = new sap.m.RatingIndicator({
  iconSize: "12px",
  value: {
  path: "rating",
  formatter: function(fValue) {
  return parseFloat(fValue) / 2;
  }
  }
  });
  var score = new sap.m.Text({
  text: {
  path: "rating",
  formatter: function(fValue) {
  return " " + fValue;
  }
  }
  }).addStyleClass("score");
  var rating_score = new sap.m.HBox({
  items: [rating, score]
  });
  var mention = new sap.m.Text({
  text: {
  path: "mention",
  formatter: function(fValue) {
  return "(" + fValue + " Mentions)";
  }
  }
  });
  var vbox = new sap.m.VBox({
  alignItems: sap.m.FlexAlignItems.Center,
         items: [poster, title, release_date, rating_score, mention]
  });
  var carousel = new sap.m.Carousel({
  loop: true,
  showPageIndicator: false,
  pages: {
  path: "/d/results",
  template: vbox
  }
  });
  var dateRange = new sap.m.DateRangeSelection("dateRange", {
  change: [oController.changeDate, oController]
  });
  var page = new sap.m.Page({
  title: "Movie Sentiment Rating",
  content: [dateRange, carousel]
  });
  return page;
  }
});







home.controller.js


sap.ui.controller("movieratingmobile.home", {
/**
* 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 movieratingmobile.home
*/
// onInit: function() {
//
// },
/**
* Similar to onAfterRendering, but this hook is invoked before the controller's View is re-rendered
* (NOT before the first rendering! onInit() is used for that one!).
* @memberOf movieratingmobile.home
*/
// onBeforeRendering: function() {
//
// },
/**
* Called when the View has been rendered (so its HTML is part of the document). Post-rendering manipulations of the HTML could be done here.
* This hook is the same one that SAPUI5 controls get after being rendered.
* @memberOf movieratingmobile.home
*/
// onAfterRendering: function() {
//
// },
/**
* Called when the Controller is destroyed. Use this one to free resources and finalize activities.
* @memberOf movieratingmobile.home
*/
// onExit: function() {
//
// }
  changeDate: function(oEvent) {
  this.getView().getModel().loadData("/movieRating/services/movies.xsodata/InputParams(date_from=datetime'" + sap.ui.controller("movieratingmobile.app").dateString(oEvent.getParameters().from) + "',date_to=datetime'" + sap.ui.controller("movieratingmobile.app").dateString(oEvent.getParameters().to) + "')/Results/?$format=json&$orderby=mention desc");
  },
  showDetail: function(oEvent) {
  var oBindingContext = oEvent.getSource().getParent().getBindingContext();
  var oBus = sap.ui.getCore().getEventBus();
  oBus.publish("nav", "to", {
  id: "detail",
  data: {
  context : oBindingContext
  }
  });
  }
});







detail.view.js


sap.ui.jsview("movieratingmobile.detail", {
  /** Specifies the Controller belonging to this View.
  * In the case that it is not implemented, or that "null" is returned, this View does not have a Controller.
  * @memberOf movieratingmobile.detail
  */
  getControllerName : function() {
  return "movieratingmobile.detail";
  },
  /** Is initially called once after the Controller has been instantiated. It is the place where the UI is constructed.
  * Since the Controller is given to this method, its event handlers can be attached right away.
  * @memberOf movieratingmobile.detail
  */
  createContent : function(oController) {
  var pullToRefresh = new sap.m.PullToRefresh({
  refresh: [oController.refreshList, oController]
  });
  var poster = new sap.m.Image({
  densityAware: false,
  src: "{poster}"
  });
  var title = new sap.m.Text({
  text: "{title}"
  }).addStyleClass("title");
  var release_date = new sap.m.Text({
  text: "{release_date}"
  });
  var rating = new sap.m.RatingIndicator({
  iconSize: "12px",
  value: {
  path: "rating",
  formatter: function(fValue) {
  return parseFloat(fValue) / 2;
  }
  }
  });
  var score = new sap.m.Text({
  text: {
  path: "rating",
  formatter: function(fValue) {
  return " " + fValue;
  }
  }
  });
  var rating_score = new sap.m.HBox({
  items: [rating, score]
  });
  var mention = new sap.m.Text({
  text: {
  path: "mention",
  formatter: function(fValue) {
  return "(" + fValue + " Mentions)";
  }
  }
  });
  var vbox = new sap.m.VBox({
         items: [title, release_date, rating_score, mention]
  }).addStyleClass("info");
  var hbox = new sap.m.HBox({
  items: [poster, vbox]
  }).addStyleClass("movieInfo");
  var oBar = new sap.m.Toolbar({
  height: "90px",
  content: [hbox]
  });
  var listItem = new sap.m.FeedListItem({
  iconDensityAware: false,
  sender: "{user_screen_name}",
  icon: "{user_profile_image_url}",
  info: "{sent}",
  timestamp: "{created_at_str}",
  text: "{text}"
  });
  var oList = new sap.m.List({
  items: {
  path: "/d/results",
  template: listItem
  }
  });
  oList.setModel(model);
  var loader = new sap.m.StandardListItem({
  type: sap.m.ListType.Active,
  title: "Load 20 more mentions...",
  press: [oController.loadMore, oController]
  });
  var page = new sap.m.Page({
  title: "Movie Detail",
  showNavButton: true,
  subHeader: oBar,
  content: [pullToRefresh, oList, loader],
  navButtonPress: [oController.showHome, oController]
  });
  this.addEventDelegate({
  onBeforeShow: function(evt) {
  this.setBindingContext(evt.data);
  oController.initializeList(evt);
  }
  }, this);
  return page;
  }
});







detail.controller.js


var movie_id, max_id_counter;
var model = new sap.ui.model.json.JSONModel();
sap.ui.controller("movieratingmobile.detail", {
/**
* 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 movieratingmobile.detail
*/
// onInit: function() {
//
// },
/**
* Similar to onAfterRendering, but this hook is invoked before the controller's View is re-rendered
* (NOT before the first rendering! onInit() is used for that one!).
* @memberOf movieratingmobile.detail
*/
// onBeforeRendering: function() {
//
// },
/**
* Called when the View has been rendered (so its HTML is part of the document). Post-rendering manipulations of the HTML could be done here.
* This hook is the same one that SAPUI5 controls get after being rendered.
* @memberOf movieratingmobile.detail
*/
// onAfterRendering: function() {
//
// },
/**
* Called when the Controller is destroyed. Use this one to free resources and finalize activities.
* @memberOf movieratingmobile.detail
*/
// onExit: function() {
//
// }
  showHome: function() {
  var oBus = sap.ui.getCore().getEventBus();
  oBus.publish("nav", "back");
  },
  initializeList: function(oEvent) {
  movie_id = new sap.ui.model.json.JSONModel(oEvent.data).getProperty("/oModel/oData" + oEvent.data.sPath + "/id");
  model.loadData("/movieRating/services/tweets.xsodata/AT_TWEETS/?$format=json&$filter=movie_id eq " + movie_id + "&$top=20&$orderby=id_counter desc", null, false);
  this.updateMax(model);
  this.getView().getContent()[0].getContent()[0].setDescription("Last updated time: " + new Date().toUTCString());
  },
  refreshList: function(oEvent) {
  model.loadData("/movieRating/services/tweets.xsodata/AT_TWEETS/?$format=json&$filter=movie_id eq " + movie_id + "&$top=20&$orderby=id_counter desc", null, false);
  this.updateMax(model);
  this.getView().getContent()[0].getContent()[0].setDescription("Last updated time: " + new Date().toUTCString()).hide();
  },
  loadMore: function() {
  var mentionModel = new sap.ui.model.json.JSONModel();
  mentionModel.loadData("/movieRating/services/tweets.xsodata/AT_TWEETS/?$format=json&$filter=movie_id eq " + movie_id + " and id_counter lt '" + max_id_counter + "'&$top=20&$orderby=id_counter desc", null, false);
  var mentions = this.updateMax(mentionModel);
  for (var i in mentions) {
  this.getView().getContent()[0].getContent()[1].addItem(new sap.m.FeedListItem({
  iconDensityAware: false,
  sender: mentions[i].user_screen_name,
  icon: mentions[i].user_profile_image_url,
  info: mentions[i].sent,
  timestamp: mentions[i].created_at_str,
  text: mentions[i].text
  }));
  }
  },
  updateMax: function(model) {
  var mentions = model.getProperty("/d/results");
  max_id_counter = mentions[mentions.length - 1].id_counter;
  return mentions;
  }
});







Look & feel

1. Homepage displaying new release movies info (e.g., rating and # of mentions) within carousel in the last seven days

33.PNG

2. Swipe the carousel to see various movies

34.PNG

3. Click a movie to see the latest 20 tweet mentions/sentiments of this movie

35.PNG

4. Scroll down/up to see mentions

36.PNG

5. Pull to refresh the latest 20 mentions

37.PNG

6. You can always load 20 more old mentions

38.PNG

7. Select the release date range to get movies you want to see

39.PNG

Movie Sentiment Rating using sap.m

Similar with Real-time sentiment rating of movies on SAP HANA (part 4) – SAP HANA info access, I’ve also recorded a video for you. Have fun.

Next steps

In this blog post, we’ve learned how to use SAPUI5, especially the sap.m library and we’ve built our movie sentiment rating app with sap.m. So till now, we’ve completely developed two smart apps, one based on SINA in previous blog and the other using sap.m which is described in this blog. In the next blog, let’s have an open discussion. I’ll show you the source code on GitHub and some future work.

Hope you enjoyed the development journey and picking up your favorite movie on your phone. 🙂

To report this post you need to login first.

2 Comments

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

Leave a Reply