From b7b14d2407bca8a0f51c9ea3b9bf245959151fa7 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <piotr.gawron@uni.lu>
Date: Fri, 9 Feb 2018 14:08:47 +0100
Subject: [PATCH] visualzization of functions for kinetic law added

---
 frontend-js/src/main/css/global.css           |   5 +
 frontend-js/src/main/js/Functions.js          |   4 +
 frontend-js/src/main/js/ServerConnector.js    |  20 ++
 .../src/main/js/gui/leftPanel/GuiUtils.js     |   6 +-
 frontend-js/src/main/js/map/CustomMap.js      |   4 +-
 .../src/main/js/map/data/KineticLaw.js        |   1 -
 frontend-js/src/main/js/map/data/MapModel.js  | 241 ++++++++++--------
 .../main/js/map/window/ReactionInfoWindow.js  |  41 ++-
 .../src/test/js/ServerConnector-test.js       |   7 +
 frontend-js/src/test/js/helper.js             |  30 +++
 .../src/test/js/map/data/KineticLaw-test.js   |   2 +-
 .../js/map/window/ReactionInfoWindow-test.js  |  29 ++-
 .../all/functions/5/token=MOCK_TOKEN_ID&      |   1 +
 13 files changed, 269 insertions(+), 122 deletions(-)
 create mode 100644 frontend-js/testFiles/apiCalls/projects/sample/models/all/functions/5/token=MOCK_TOKEN_ID&

diff --git a/frontend-js/src/main/css/global.css b/frontend-js/src/main/css/global.css
index 83c544eb80..dab4b9ec0f 100644
--- a/frontend-js/src/main/css/global.css
+++ b/frontend-js/src/main/css/global.css
@@ -517,6 +517,11 @@ h1 {
     padding: 2px;
 }
 
+.borderTable div[style*="display: table-cell"] {
+    padding: 2px;
+    border: 1px solid black;
+}
+
 .minerva-datatable-toolbar {
     float: left;
 }
\ No newline at end of file
diff --git a/frontend-js/src/main/js/Functions.js b/frontend-js/src/main/js/Functions.js
index 6932f32af5..2a12658649 100644
--- a/frontend-js/src/main/js/Functions.js
+++ b/frontend-js/src/main/js/Functions.js
@@ -315,6 +315,10 @@ Functions.removeChildren = function (element) {
   }
 };
 
+/**
+ *
+ * @returns Promise resolved after javascript is loaded
+ */
 Functions.loadScript = function (url) {
   return new Promise(function (resolve) {
     var scriptExists = false;
diff --git a/frontend-js/src/main/js/ServerConnector.js b/frontend-js/src/main/js/ServerConnector.js
index a76aedaa23..e19e237c9e 100644
--- a/frontend-js/src/main/js/ServerConnector.js
+++ b/frontend-js/src/main/js/ServerConnector.js
@@ -30,6 +30,7 @@ var Project = require('./map/data/Project');
 var ProjectStatistics = require('./map/data/ProjectStatistics');
 var Reaction = require('./map/data/Reaction');
 var ReferenceGenome = require('./map/data/ReferenceGenome');
+var SbmlFunction = require('./map/data/SbmlFunction');
 var SecurityError = require('./SecurityError');
 var SessionData = require('./SessionData');
 var User = require('./map/data/User');
@@ -349,6 +350,12 @@ ServerConnector.getMeshUrl = function (queryParams, filterParams) {
   });
 };
 
+ServerConnector.getSbmlFunctionUrl = function (queryParams, filterParams) {
+  return this.getApiUrl({
+    url: this.getModelsUrl(queryParams) + "functions/" + queryParams.functionId,
+    params: filterParams
+  });
+};
 
 ServerConnector.getReferenceGenomeUrl = function (queryParams, filterParams) {
   var version = this.getIdOrAsterisk(queryParams.version);
@@ -1843,4 +1850,17 @@ ServerConnector.getMesh = function (params) {
   });
 };
 
+ServerConnector.getSbmlFunction = function (params) {
+  var self = this;
+  if (params === undefined) {
+    params = {};
+  }
+  return self.getProjectId(params.projectId).then(function (result) {
+    params.projectId = result;
+    return self.sendGetRequest(self.getSbmlFunctionUrl(params));
+  }).then(function (content) {
+    return new SbmlFunction(JSON.parse(content));
+  });
+};
+
 module.exports = ServerConnector;
diff --git a/frontend-js/src/main/js/gui/leftPanel/GuiUtils.js b/frontend-js/src/main/js/gui/leftPanel/GuiUtils.js
index daa4b747f7..ea45176d13 100644
--- a/frontend-js/src/main/js/gui/leftPanel/GuiUtils.js
+++ b/frontend-js/src/main/js/gui/leftPanel/GuiUtils.js
@@ -314,7 +314,11 @@ GuiUtils.prototype.createTableRow = function (elements) {
       type: "div",
       style: "display: table-cell;"
     });
-    cell.appendChild(elements[i]);
+    if (Functions.isDomElement(elements[i])) {
+      cell.appendChild(elements[i]);
+    } else {
+      cell.innerHTML = elements[i];
+    }
     row.appendChild(cell);
   }
   return row;
diff --git a/frontend-js/src/main/js/map/CustomMap.js b/frontend-js/src/main/js/map/CustomMap.js
index 2dcf1c7a9f..e3b7e7cdeb 100644
--- a/frontend-js/src/main/js/map/CustomMap.js
+++ b/frontend-js/src/main/js/map/CustomMap.js
@@ -813,7 +813,7 @@ CustomMap.prototype.getOverlayDataForAlias = function (alias, general) {
  * @param general
  *          if true then all elements will be returned, if false then only ones
  *          available right now in the overlay
- * @returns data from all {@link OverlayCollection} for a given alias
+ * @returns Promise of data from all {@link OverlayCollection} for a given alias
  */
 CustomMap.prototype.getOverlayDataForReaction = function (reaction, general) {
   var identifiedElement = new IdentifiedElement(reaction);
@@ -839,7 +839,7 @@ CustomMap.prototype.getOverlayDataForPoint = function (point, general) {
  *
  * @param identifiedElement
  *          {@link IdentifiedElement} for which overlay data will be returned
- * @returns data from all {@link OverlayCollection} for a given
+ * @returns Promise of data from all {@link OverlayCollection} for a given
  *          {@link IdentifiedElement}
  */
 CustomMap.prototype.getOverlayDataForIdentifiedElement = function (identifiedElement, general) {
diff --git a/frontend-js/src/main/js/map/data/KineticLaw.js b/frontend-js/src/main/js/map/data/KineticLaw.js
index 39c9b02470..fb4ecc6deb 100644
--- a/frontend-js/src/main/js/map/data/KineticLaw.js
+++ b/frontend-js/src/main/js/map/data/KineticLaw.js
@@ -5,7 +5,6 @@ function KineticLaw(jsonObject) {
   self.setParameterIds(jsonObject.parameterIds);
   self.setFunctionIds(jsonObject.functionIds);
   self.setDefinition(jsonObject.definition);
-  self.setDefinition(jsonObject.definition);
   self.setMathMlPresentation(jsonObject.mathMlPresentation);
 }
 
diff --git a/frontend-js/src/main/js/map/data/MapModel.js b/frontend-js/src/main/js/map/data/MapModel.js
index 7bbd84fc8d..53e0a82877 100644
--- a/frontend-js/src/main/js/map/data/MapModel.js
+++ b/frontend-js/src/main/js/map/data/MapModel.js
@@ -15,7 +15,7 @@ var Reaction = require('./Reaction');
 
 /**
  * Default constructor.
- * 
+ *
  */
 
 function MapModel(configuration) {
@@ -45,6 +45,8 @@ function MapModel(configuration) {
 
   this._submodels = [];
 
+  this._sbmlFunctions = [];
+
   if (configuration !== undefined) {
     if (configuration instanceof MapModel) {
       this.setId(configuration.getId());
@@ -86,12 +88,12 @@ function MapModel(configuration) {
 
 /**
  * Returns list of {@link LayoutData} on this model.
- * 
+ *
  * @returns {Array} with list of {@link LayoutData} on this model
  */
-MapModel.prototype.getLayoutsData = function() {
+MapModel.prototype.getLayoutsData = function () {
   var result = [];
-  for ( var id in this._layoutsData) {
+  for (var id in this._layoutsData) {
     if (this._layoutsData.hasOwnProperty(id)) {
       result.push(this._layoutsData[id]);
     }
@@ -99,23 +101,23 @@ MapModel.prototype.getLayoutsData = function() {
   return result;
 };
 
-MapModel.prototype.getLayouts = function() {
+MapModel.prototype.getLayouts = function () {
   return this.getLayoutsData();
 };
 
 /**
  * Return list of all aliases that were added to the model.
  */
-MapModel.prototype.getAliases = function(params) {
+MapModel.prototype.getAliases = function (params) {
   var self = this;
   return ServerConnector.getAliases({
-    columns : "id,modelId",
-    type : params.type,
-    modelId : self.getId(),
-    includedCompartmentIds : params.includedCompartmentIds,
-    excludedCompartmentIds : params.excludedCompartmentIds,
+    columns: "id,modelId",
+    type: params.type,
+    modelId: self.getId(),
+    includedCompartmentIds: params.includedCompartmentIds,
+    excludedCompartmentIds: params.excludedCompartmentIds,
 
-  }).then(function(lightAliases) {
+  }).then(function (lightAliases) {
     var identifiedElements = [];
     for (var i = 0; i < lightAliases.length; i++) {
       self.addAlias(lightAliases[i]);
@@ -127,12 +129,12 @@ MapModel.prototype.getAliases = function(params) {
 
 /**
  * Returns {@link Alias} by identifier.
- * 
+ *
  * @param id
  *          identifier of the {@link Alias}
  * @returns {@link Alias} by identifier
  */
-MapModel.prototype.getAliasById = function(id, complete) {
+MapModel.prototype.getAliasById = function (id, complete) {
   var self = this;
   if (complete) {
     return this.getCompleteAliasById(id);
@@ -141,21 +143,21 @@ MapModel.prototype.getAliasById = function(id, complete) {
     return Promise.resolve(self._aliases[id]);
   } else {
     return self.getMissingElements({
-      aliasIds : [ id ]
-    }).then(function() {
+      aliasIds: [id]
+    }).then(function () {
       return self._aliases[id];
     });
   }
 };
 
-MapModel.prototype.getCompleteAliasById = function(id) {
+MapModel.prototype.getCompleteAliasById = function (id) {
   var self = this;
   if (self._aliases[id] !== undefined && self._aliases[id].isComplete()) {
     return Promise.resolve(self._aliases[id]);
   } else {
     return ServerConnector.getAliases({
-      ids : id
-    }).then(function(aliases) {
+      ids: id
+    }).then(function (aliases) {
       if (self._aliases[id] === undefined) {
         self._aliases[id] = aliases[0];
       } else {
@@ -168,12 +170,12 @@ MapModel.prototype.getCompleteAliasById = function(id) {
 
 /**
  * Returns {@link Reaction} by identifier.
- * 
+ *
  * @param id
  *          identifier of the {@link Reaction}
  * @returns {@link Reaction} by identifier
  */
-MapModel.prototype.getReactionById = function(id, complete) {
+MapModel.prototype.getReactionById = function (id, complete) {
   var self = this;
   if (complete) {
     return this.getCompleteReactionById(id);
@@ -182,14 +184,14 @@ MapModel.prototype.getReactionById = function(id, complete) {
     return Promise.resolve(self._reactions[id]);
   } else {
     return self.getMissingElements({
-      reactionIds : [ id ]
-    }).then(function() {
+      reactionIds: [id]
+    }).then(function () {
       return self._reactions[id];
     });
   }
 };
 
-MapModel.prototype._getMissingReactionsElementIds = function(reactions) {
+MapModel.prototype._getMissingReactionsElementIds = function (reactions) {
   var self = this;
   var result = [];
   var ids = [];
@@ -211,19 +213,19 @@ MapModel.prototype._getMissingReactionsElementIds = function(reactions) {
   return result;
 };
 
-MapModel.prototype.getCompleteReactionById = function(id) {
+MapModel.prototype.getCompleteReactionById = function (id) {
   var self = this;
   if (self._reactions[id] instanceof Reaction && self._reactions[id].isComplete()) {
     return Promise.resolve(self._reactions[id]);
   } else {
     var result;
-    return self.getReactionById(id).then(function(result) {
-      var ids = self._getMissingReactionsElementIds([ result ]);
+    return self.getReactionById(id).then(function (result) {
+      var ids = self._getMissingReactionsElementIds([result]);
       return self.getMissingElements({
-        aliasIds : ids,
-        complete : true
+        aliasIds: ids,
+        complete: true
       });
-    }).then(function() {
+    }).then(function () {
       var i;
       result = self._reactions[id];
       for (i = 0; i < result.getReactants().length; i++) {
@@ -246,7 +248,7 @@ MapModel.prototype.getCompleteReactionById = function(id) {
   }
 };
 
-MapModel.prototype.getMissingElements = function(elements) {
+MapModel.prototype.getMissingElements = function (elements) {
   var self = this;
 
   var layouts = this._getLayouts();
@@ -293,8 +295,8 @@ MapModel.prototype.getMissingElements = function(elements) {
   var reactionPromise = null;
   if (reactionIds.length > 0) {
     reactionPromise = ServerConnector.getReactions({
-      ids : reactionIds,
-      complete : elements.complete
+      ids: reactionIds,
+      complete: elements.complete
     });
   }
 
@@ -302,19 +304,19 @@ MapModel.prototype.getMissingElements = function(elements) {
   if (aliasIds.length > 0) {
     if (elements.complete) {
       aliasPromise = ServerConnector.getAliases({
-        ids : aliasIds
+        ids: aliasIds
       });
     } else {
       aliasPromise = ServerConnector.getAliases({
-        ids : aliasIds,
-        columns : "id,bounds,modelId"
+        ids: aliasIds,
+        columns: "id,bounds,modelId"
       });
 
     }
   }
 
   var result = [];
-  return Promise.all([ reactionPromise, aliasPromise ]).then(function(values) {
+  return Promise.all([reactionPromise, aliasPromise]).then(function (values) {
     var i;
     var reactions = values[0];
     var aliases = values[1];
@@ -337,30 +339,30 @@ MapModel.prototype.getMissingElements = function(elements) {
     }
     if (ids.length > 0) {
       return self.getMissingElements({
-        aliasIds : ids,
-        complete : true
+        aliasIds: ids,
+        complete: true
       });
     } else {
       return Promise.resolve([]);
     }
-  }).then(function() {
+  }).then(function () {
     return result;
   });
 };
 
 /**
  * Returns layout data for a given layout identifier.
- * 
+ *
  * @param layoutId
  *          layout identifier
  * @returns {LayoutData} for a given layout identifier
  */
-MapModel.prototype.getLayoutDataById = function(layoutId) {
+MapModel.prototype.getLayoutDataById = function (layoutId) {
   var self = this;
   if (self._layoutsData[layoutId] !== undefined) {
     return Promise.resolve(self._layoutsData[layoutId]);
   } else {
-    return ServerConnector.getOverlayById(layoutId).then(function(layout) {
+    return ServerConnector.getOverlayById(layoutId).then(function (layout) {
       self.addLayout(layout);
       return self._layoutsData[layoutId];
     });
@@ -369,11 +371,11 @@ MapModel.prototype.getLayoutDataById = function(layoutId) {
 
 /**
  * Adds information about alias.
- * 
+ *
  * @param aliasData
  *          raw data about alias
  */
-MapModel.prototype.addAlias = function(aliasData) {
+MapModel.prototype.addAlias = function (aliasData) {
   var alias = aliasData;
   if (!(aliasData instanceof Alias)) {
     alias = new Alias(aliasData);
@@ -391,11 +393,11 @@ MapModel.prototype.addAlias = function(aliasData) {
 
 /**
  * Adds information about reaction.
- * 
+ *
  * @param reactionData
  *          raw data about reaction
  */
-MapModel.prototype.addReaction = function(reactionData) {
+MapModel.prototype.addReaction = function (reactionData) {
   var reaction = null;
   if (reactionData instanceof Reaction) {
     reaction = reactionData;
@@ -415,12 +417,12 @@ MapModel.prototype.addReaction = function(reactionData) {
 
 /**
  * Returns {@link PointData} for a given point on the map.
- * 
+ *
  * @param point
  *          {@link google.maps.Point} where we are requesting data
  * @returns {@link PointData} for a given point on the map
  */
-MapModel.prototype.getPointDataByPoint = function(inputPoint) {
+MapModel.prototype.getPointDataByPoint = function (inputPoint) {
   if (inputPoint instanceof google.maps.Point) {
     var point = this._roundPoint(inputPoint);
     var id = this._pointToId(point);
@@ -438,13 +440,13 @@ MapModel.prototype.getPointDataByPoint = function(inputPoint) {
 
 /**
  * Returns point where x and y coordinate are rounded to 2 decimal places.
- * 
+ *
  * @param point
  *          input point
  * @returns {google.maps.Point} point where x and y coordinate are rounded to 2
  *          decimal places
  */
-MapModel.prototype._roundPoint = function(point) {
+MapModel.prototype._roundPoint = function (point) {
   var x = parseFloat(point.x).toFixed(2);
   var y = parseFloat(point.y).toFixed(2);
   return new google.maps.Point(x, y);
@@ -452,12 +454,12 @@ MapModel.prototype._roundPoint = function(point) {
 
 /**
  * Transform point into string identifier.
- * 
+ *
  * @param point
  *          {google.maps.Point} to transform
  * @returns {String} string identifier for a given point
  */
-MapModel.prototype._pointToId = function(point) {
+MapModel.prototype._pointToId = function (point) {
   if (point instanceof google.maps.Point) {
     return "(" + point.x + ", " + point.y + ")";
   } else {
@@ -465,75 +467,75 @@ MapModel.prototype._pointToId = function(point) {
   }
 };
 
-MapModel.prototype.getId = function() {
+MapModel.prototype.getId = function () {
   return this.id;
 };
 
-MapModel.prototype.setId = function(id) {
+MapModel.prototype.setId = function (id) {
   this.id = parseInt(id);
 };
 
-MapModel.prototype.getWidth = function() {
+MapModel.prototype.getWidth = function () {
   return this._width;
 };
 
-MapModel.prototype.setWidth = function(width) {
+MapModel.prototype.setWidth = function (width) {
   this._width = width;
 };
 
-MapModel.prototype.getHeight = function() {
+MapModel.prototype.getHeight = function () {
   return this._height;
 };
 
-MapModel.prototype.setHeight = function(height) {
+MapModel.prototype.setHeight = function (height) {
   this._height = height;
 };
 
-MapModel.prototype.getName = function() {
+MapModel.prototype.getName = function () {
   return this._name;
 };
 
-MapModel.prototype.setName = function(name) {
+MapModel.prototype.setName = function (name) {
   this._name = name;
 };
 
-MapModel.prototype.getMinZoom = function() {
+MapModel.prototype.getMinZoom = function () {
   return this._minZoom;
 };
 
-MapModel.prototype.setMinZoom = function(minZoom) {
+MapModel.prototype.setMinZoom = function (minZoom) {
   this._minZoom = minZoom;
 };
 
-MapModel.prototype.getDefaultZoomLevel = function() {
+MapModel.prototype.getDefaultZoomLevel = function () {
   return this._defaultZoomLevel;
 };
 
-MapModel.prototype.setDefaultZoomLevel = function(defaultZoomLevel) {
+MapModel.prototype.setDefaultZoomLevel = function (defaultZoomLevel) {
   this._defaultZoomLevel = defaultZoomLevel;
 };
 
-MapModel.prototype.getDefaultCenterX = function() {
+MapModel.prototype.getDefaultCenterX = function () {
   return this._defaultCenterX;
 };
 
-MapModel.prototype.setDefaultCenterX = function(defaultCenterX) {
+MapModel.prototype.setDefaultCenterX = function (defaultCenterX) {
   this._defaultCenterX = defaultCenterX;
 };
 
-MapModel.prototype.getDefaultCenterY = function() {
+MapModel.prototype.getDefaultCenterY = function () {
   return this._defaultCenterY;
 };
 
-MapModel.prototype.setDefaultCenterY = function(defaultCenterY) {
+MapModel.prototype.setDefaultCenterY = function (defaultCenterY) {
   this._defaultCenterY = defaultCenterY;
 };
 
-MapModel.prototype.getSubmodelType = function() {
+MapModel.prototype.getSubmodelType = function () {
   return this._submodelType;
 };
 
-MapModel.prototype.setSubmodelType = function(submodelType) {
+MapModel.prototype.setSubmodelType = function (submodelType) {
   this._submodelType = submodelType;
 };
 
@@ -548,7 +550,7 @@ function createLatLng(param) {
   }
 }
 
-MapModel.prototype.setCenterLatLng = function(centerLatLng) {
+MapModel.prototype.setCenterLatLng = function (centerLatLng) {
   var newVal = createLatLng(centerLatLng);
   if (newVal === null) {
     logger.warn("centerLatLng is invalid");
@@ -557,11 +559,11 @@ MapModel.prototype.setCenterLatLng = function(centerLatLng) {
   }
 };
 
-MapModel.prototype.getCenterLatLng = function() {
+MapModel.prototype.getCenterLatLng = function () {
   return this._centerLatLng;
 };
 
-MapModel.prototype.setTopLeftLatLng = function(topLeftLatLng) {
+MapModel.prototype.setTopLeftLatLng = function (topLeftLatLng) {
   var newVal = createLatLng(topLeftLatLng);
   if (newVal === null) {
     logger.warn("topLeftLatLng is invalid");
@@ -570,11 +572,11 @@ MapModel.prototype.setTopLeftLatLng = function(topLeftLatLng) {
   }
 };
 
-MapModel.prototype.getTopLeftLatLng = function() {
+MapModel.prototype.getTopLeftLatLng = function () {
   return this._topLeftLatLng;
 };
 
-MapModel.prototype.setBottomRightLatLng = function(bottomRightLatLng) {
+MapModel.prototype.setBottomRightLatLng = function (bottomRightLatLng) {
   var newVal = createLatLng(bottomRightLatLng);
   if (newVal === null) {
     logger.warn("bottomRightLatLng is invalid");
@@ -583,31 +585,31 @@ MapModel.prototype.setBottomRightLatLng = function(bottomRightLatLng) {
   }
 };
 
-MapModel.prototype.getBottomRightLatLng = function() {
+MapModel.prototype.getBottomRightLatLng = function () {
   return this._bottomRightLatLng;
 };
 
-MapModel.prototype.getMaxZoom = function() {
+MapModel.prototype.getMaxZoom = function () {
   return this._maxZoom;
 };
 
-MapModel.prototype.setMaxZoom = function(maxZoom) {
+MapModel.prototype.setMaxZoom = function (maxZoom) {
   this._maxZoom = maxZoom;
 };
 
-MapModel.prototype.getTileSize = function() {
+MapModel.prototype.getTileSize = function () {
   return this._tileSize;
 };
 
-MapModel.prototype.getPictureSize = function() {
+MapModel.prototype.getPictureSize = function () {
   return Math.max(this.getWidth(), this.getHeight());
 };
 
-MapModel.prototype.setTileSize = function(tileSize) {
+MapModel.prototype.setTileSize = function (tileSize) {
   this._tileSize = tileSize;
 };
 
-MapModel.prototype.addLayouts = function(layouts) {
+MapModel.prototype.addLayouts = function (layouts) {
   if (layouts === undefined) {
     logger.warn("Layouts are undefined...");
   } else {
@@ -616,7 +618,7 @@ MapModel.prototype.addLayouts = function(layouts) {
     }
   }
 };
-MapModel.prototype.addLayout = function(layout) {
+MapModel.prototype.addLayout = function (layout) {
   var layoutData = null;
   if (layout instanceof LayoutData) {
     layoutData = layout;
@@ -631,7 +633,7 @@ MapModel.prototype.addLayout = function(layout) {
   }
 };
 
-MapModel.prototype.addSubmodels = function(submodels) {
+MapModel.prototype.addSubmodels = function (submodels) {
   if (submodels !== undefined) {
     for (var i = 0; i < submodels.length; i++) {
       this.addSubmodel(submodels[i]);
@@ -639,18 +641,18 @@ MapModel.prototype.addSubmodels = function(submodels) {
   }
 };
 
-MapModel.prototype.addSubmodel = function(submodel) {
+MapModel.prototype.addSubmodel = function (submodel) {
   if (!(submodel instanceof MapModel)) {
     submodel = new MapModel(submodel);
   }
   this._submodels.push(submodel);
 };
 
-MapModel.prototype.getSubmodels = function() {
+MapModel.prototype.getSubmodels = function () {
   return this._submodels;
 };
 
-MapModel.prototype.getSubmodelById = function(id) {
+MapModel.prototype.getSubmodelById = function (id) {
   if (this.getId() === id) {
     return this;
   }
@@ -662,9 +664,9 @@ MapModel.prototype.getSubmodelById = function(id) {
   return null;
 };
 
-MapModel.prototype._getLayouts = function() {
+MapModel.prototype._getLayouts = function () {
   var result = [];
-  for ( var id in this._layoutsData) {
+  for (var id in this._layoutsData) {
     if (this._layoutsData.hasOwnProperty(id)) {
       result.push(this._layoutsData[id]);
     }
@@ -672,7 +674,7 @@ MapModel.prototype._getLayouts = function() {
   return result;
 };
 
-MapModel.prototype.getByIdentifiedElement = function(ie, complete) {
+MapModel.prototype.getByIdentifiedElement = function (ie, complete) {
   var self = this;
   if (ie.getType() === "ALIAS") {
     return self.getAliasById(ie.getId(), complete);
@@ -691,7 +693,7 @@ MapModel.prototype.getByIdentifiedElement = function(ie, complete) {
   }
 };
 
-MapModel.prototype.getByIdentifiedElements = function(identifiedElements, complete) {
+MapModel.prototype.getByIdentifiedElements = function (identifiedElements, complete) {
   var self = this;
   var missingAliases = [];
   var missingReactions = [];
@@ -710,10 +712,10 @@ MapModel.prototype.getByIdentifiedElements = function(identifiedElements, comple
   }
 
   return self.getMissingElements({
-    aliasIds : missingAliases,
-    reactionIds : missingReactions,
-    complete : complete
-  }).then(function() {
+    aliasIds: missingAliases,
+    reactionIds: missingReactions,
+    complete: complete
+  }).then(function () {
     var promises = [];
     for (var i = 0; i < identifiedElements.length; i++) {
       promises.push(self.getByIdentifiedElement(identifiedElements[i], complete));
@@ -723,7 +725,7 @@ MapModel.prototype.getByIdentifiedElements = function(identifiedElements, comple
 
 };
 
-MapModel.prototype.isAvailable = function(ie, complete) {
+MapModel.prototype.isAvailable = function (ie, complete) {
   var element;
   if (ie.getType() === "ALIAS") {
     element = this._aliases[ie.getId()];
@@ -749,11 +751,11 @@ MapModel.prototype.isAvailable = function(ie, complete) {
   }
 };
 
-MapModel.prototype.getReactionsForElement = function(element, complete) {
-  return this.getReactionsForElements([ element ], complete);
+MapModel.prototype.getReactionsForElement = function (element, complete) {
+  return this.getReactionsForElements([element], complete);
 };
 
-MapModel.prototype.getReactionsForElements = function(elements, complete) {
+MapModel.prototype.getReactionsForElements = function (elements, complete) {
   var self = this;
   var ids = [];
   var i;
@@ -777,8 +779,8 @@ MapModel.prototype.getReactionsForElements = function(elements, complete) {
   var result = [];
   return ServerConnector.getReactions({
     modelId: self.getId(),
-    participantId : ids
-  }).then(function(reactions) {
+    participantId: ids
+  }).then(function (reactions) {
     result = reactions;
 
     for (var i = 0; i < reactions.length; i++) {
@@ -792,10 +794,10 @@ MapModel.prototype.getReactionsForElements = function(elements, complete) {
     }
     var ids = self._getMissingReactionsElementIds(reactions);
     return self.getMissingElements({
-      aliasIds : ids,
-      complete : true
+      aliasIds: ids,
+      complete: true
     });
-  }).then(function() {
+  }).then(function () {
     var promises = [];
     for (var i = 0; i < result.length; i++) {
       promises.push(self.getCompleteReactionById(result[i].getId()));
@@ -804,25 +806,42 @@ MapModel.prototype.getReactionsForElements = function(elements, complete) {
   });
 };
 
-MapModel.prototype.getCompartments = function() {
+MapModel.prototype.getCompartments = function () {
   var self = this;
 
   var promise = Promise.resolve();
   if (self._compartments === undefined) {
     promise = ServerConnector.getAliases({
-      columns : "id,bounds,modelId",
-      type : "Compartment",
-      modelId : self.getId()
-    }).then(function(compartments) {
+      columns: "id,bounds,modelId",
+      type: "Compartment",
+      modelId: self.getId()
+    }).then(function (compartments) {
       self._compartments = [];
       for (var i = 0; i < compartments.length; i++) {
         self._compartments.push(new IdentifiedElement(compartments[i]));
       }
     });
   }
-  return promise.then(function() {
+  return promise.then(function () {
     return self.getByIdentifiedElements(self._compartments, true);
   });
 };
 
+MapModel.prototype.addSbmlFunction = function (sbmlFunction) {
+  this._sbmlFunctions[sbmlFunction.getId()] = sbmlFunction;
+};
+
+MapModel.prototype.getSbmlFunctionById = function (id) {
+  var self = this;
+  if (self._sbmlFunctions[id] !== undefined) {
+    return Promise.resolve(self._sbmlFunctions[id]);
+  } else {
+    return ServerConnector.getSbmlFunction({modelId: self.getId(), functionId: id}).then(function (sbmlFunction) {
+      self.addSbmlFunction(sbmlFunction);
+      return sbmlFunction;
+    })
+  }
+};
+
+
 module.exports = MapModel;
diff --git a/frontend-js/src/main/js/map/window/ReactionInfoWindow.js b/frontend-js/src/main/js/map/window/ReactionInfoWindow.js
index 346eab845d..b32de18635 100644
--- a/frontend-js/src/main/js/map/window/ReactionInfoWindow.js
+++ b/frontend-js/src/main/js/map/window/ReactionInfoWindow.js
@@ -3,6 +3,7 @@
 var Promise = require("bluebird");
 
 var AbstractInfoWindow = require('./AbstractInfoWindow');
+var GuiUtils = require('../../gui/leftPanel/GuiUtils');
 var IdentifiedElement = require('../data/IdentifiedElement');
 var Reaction = require('../data/Reaction');
 var Functions = require('../../Functions');
@@ -30,9 +31,9 @@ function ReactionInfoWindow(params) {
     content: this.content,
     position: latLng
   });
-  if (params.reaction.getKineticLaw()!==undefined) {
+  if (params.reaction.getKineticLaw() !== undefined) {
     self.addListener("onShow", function () {
-      return MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
+      return MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
     });
   }
   self.googleInfowindow.open(self.getCustomMap().getGoogleMap(), self.getGoogleMarker());
@@ -44,7 +45,7 @@ ReactionInfoWindow.prototype.constructor = ReactionInfoWindow;
 /**
  * Methods that creates and return html code with the content of the window.
  *
- * @returns {String} representing html code for content of the info window
+ * @returns Promise representing html code for content of the info window
  */
 ReactionInfoWindow.prototype.createContentDiv = function () {
   var self = this;
@@ -71,12 +72,44 @@ ReactionInfoWindow.prototype.createContentDiv = function () {
         }));
         logger.warn("Problematic MathML: " + kineticLaw.getDefinition());
       }
+      var promises = [];
+      for (var i = 0; i < kineticLaw.getFunctionIds().length; i++) {
+        promises.push(self.getCustomMap().getModel().getSbmlFunctionById(kineticLaw.getFunctionIds()[i]));
+      }
+      return Promise.all(promises);
+    }).then(function (functions) {
+      result.appendChild(self.createSbmlFunctionDiv(functions));
+
       return Promise.resolve(result);
     });
   }
 
 };
 
+ReactionInfoWindow.prototype.createSbmlFunctionDiv = function (functions) {
+  var result = Functions.createElement({type: "div"});
+  if (functions.length > 0) {
+    result.appendChild(Functions.createElement({type: "h5", content: "Functions: "}));
+    var guiUtils = new GuiUtils();
+    var table = Functions.createElement({type: "div", style: "display: table;", className: "borderTable"});
+    table.appendChild(guiUtils.createTableRow(["Name", "Definition", "Arguments"]));
+    for (var i = 0; i < functions.length; i++) {
+      var sbmlFunction = functions[i];
+      var mathML;
+      if (sbmlFunction.getMathMlPresentation() !== undefined) {
+        mathML = sbmlFunction.getMathMlPresentation();
+      } else {
+        mathML = "<p>Problematic MathML</p>";
+        logger.warn("Problematic MathML: " + sbmlFunction.getDefinition());
+      }
+      var functionArguments = sbmlFunction.getArguments().join(", ");
+      table.appendChild(guiUtils.createTableRow([sbmlFunction.getName(), mathML, functionArguments]));
+    }
+    result.appendChild(table);
+  }
+  return result;
+};
+
 ReactionInfoWindow.prototype.init = function () {
   var self = this;
   return AbstractInfoWindow.prototype.init.call(this).then(function () {
@@ -87,7 +120,7 @@ ReactionInfoWindow.prototype.init = function () {
 /**
  * Returns array with data taken from all known {@link OverlayCollection}.
  *
- * @returns array with data from {@link OverlayCollection}
+ * @returns Promise of an array with data from {@link OverlayCollection}
  */
 ReactionInfoWindow.prototype.getOverlaysData = function (general) {
   return this.getCustomMap().getTopMap().getOverlayDataForReaction(this.getReactionData(), general);
diff --git a/frontend-js/src/test/js/ServerConnector-test.js b/frontend-js/src/test/js/ServerConnector-test.js
index b674de121f..e0fc3fd77b 100644
--- a/frontend-js/src/test/js/ServerConnector-test.js
+++ b/frontend-js/src/test/js/ServerConnector-test.js
@@ -378,5 +378,12 @@ describe('ServerConnector', function () {
     });
   });
 
+  it('getSbmlFunction', function () {
+    return ServerConnector.getSbmlFunction({functionId: 5}).then(function (sbmlFunction) {
+      assert.equal("fun", sbmlFunction.getName());
+      assert.equal(5, sbmlFunction.getId());
+    });
+  });
+
 
 });
diff --git a/frontend-js/src/test/js/helper.js b/frontend-js/src/test/js/helper.js
index 80d1efba9d..9996400b76 100644
--- a/frontend-js/src/test/js/helper.js
+++ b/frontend-js/src/test/js/helper.js
@@ -13,6 +13,7 @@ var CustomMap = require("../../main/js/map/CustomMap");
 var CustomMapOptions = require("../../main/js/map/CustomMapOptions");
 var DrugDbOverlay = require("../../main/js/map/overlay/DrugDbOverlay");
 var IdentifiedElement = require("../../main/js/map/data/IdentifiedElement");
+var KineticLaw = require("../../main/js/map/data/KineticLaw");
 var LayoutAlias = require("../../main/js/map/data/LayoutAlias");
 var LayoutData = require("../../main/js/map/data/LayoutData");
 var LayoutReaction = require("../../main/js/map/data/LayoutReaction");
@@ -21,6 +22,7 @@ var Model = require("../../main/js/map/data/MapModel");
 var AbstractDbOverlay = require("../../main/js/map/overlay/AbstractDbOverlay");
 var Project = require("../../main/js/map/data/Project");
 var Reaction = require("../../main/js/map/data/Reaction");
+var SbmlFunction = require("../../main/js/map/data/SbmlFunction");
 var SearchDbOverlay = require("../../main/js/map/overlay/SearchDbOverlay");
 var User = require("../../main/js/map/data/User");
 
@@ -397,6 +399,34 @@ Helper.prototype.readFile = function (filename) {
   });
 };
 
+Helper.prototype.createSbmlFunction = function () {
+  return new SbmlFunction({
+    "functionId": "fun",
+    "name": "fun name",
+    "definition": "<lambda>" +
+    "<bvar><ci> x </ci></bvar>" +
+    "<bvar><ci> y </ci></bvar>" +
+    "<apply><plus/><ci> x </ci><ci> y </ci><cn type=\"integer\"> 2 </cn></apply>" +
+    "</lambda>\n\n",
+    "arguments": ["x", "y"],
+    "id": this.idCounter++
+  });
+};
+
+Helper.prototype.createKineticLaw = function () {
+  return new KineticLaw({
+    "parameterIds": [],
+    "definition": '<math xmlns="http://www.w3.org/1998/Math/MathML">' +
+    '<apply xmlns="http://www.w3.org/1998/Math/MathML">' +
+    '<divide/><apply><times/><ci>ca1</ci><apply><plus/><ci>sa1</ci><ci>sa2</ci></apply></apply><cn type=\"integer\"> 2 </cn>' +
+    '</apply></math>',
+    "functionIds": []
+  });
+};
+
+
+
+
 Helper.EPSILON = 1e-6;
 
 module.exports = Helper;
diff --git a/frontend-js/src/test/js/map/data/KineticLaw-test.js b/frontend-js/src/test/js/map/data/KineticLaw-test.js
index ea819abb40..878706ca2f 100644
--- a/frontend-js/src/test/js/map/data/KineticLaw-test.js
+++ b/frontend-js/src/test/js/map/data/KineticLaw-test.js
@@ -5,7 +5,7 @@ var chai = require('chai');
 var assert = chai.assert;
 var logger = require('../../logger');
 
-describe('Comment', function () {
+describe('KineticLaw', function () {
   beforeEach(function () {
     logger.flushBuffer();
   });
diff --git a/frontend-js/src/test/js/map/window/ReactionInfoWindow-test.js b/frontend-js/src/test/js/map/window/ReactionInfoWindow-test.js
index d6ebce1267..4c36cdd7fa 100644
--- a/frontend-js/src/test/js/map/window/ReactionInfoWindow-test.js
+++ b/frontend-js/src/test/js/map/window/ReactionInfoWindow-test.js
@@ -37,7 +37,9 @@ describe('ReactionInfoWindow', function () {
         definition: '<math xmlns="http://www.w3.org/1998/Math/MathML">' +
         '<apply xmlns="http://www.w3.org/1998/Math/MathML">' +
         '<divide/><apply><times/><ci>ca1</ci><apply><plus/><ci>sa1</ci><ci>sa2</ci></apply></apply><cn type=\"integer\"> 2 </cn>' +
-        '</apply></math>'
+        '</apply></math>',
+        functionIds: [],
+        parameterIds: []
       }));
       var reactionWindow = new ReactionInfoWindow({
         reaction: reaction,
@@ -59,7 +61,9 @@ describe('ReactionInfoWindow', function () {
         '</apply></math>',
         mathMlPrenestation: "<math xmlns=\"http://www.w3.org/1998/Math/MathML\">\n" +
         "<mrow><mfrac><mrow><mrow><mi> c1 </mi><mo>⁢</mo><mfenced separators=\"\"><mrow><mi> s1 </mi><mo>+</mo><mi> s2 </mi></mrow></mfenced></mrow></mrow><mrow><mn> 2 </mn></mrow></mfrac></mrow>\n" +
-        "</math>\n"
+        "</math>\n",
+        functionIds: [],
+        parameterIds: []
       }));
       var reactionWindow = new ReactionInfoWindow({
         reaction: reaction,
@@ -71,5 +75,26 @@ describe('ReactionInfoWindow', function () {
         assert.notOk(div.innerHTML.indexOf("apply") >= 0);
       });
     });
+
+    it("with kinetic parameters", function () {
+      var map = helper.createCustomMap();
+      var sbmlFunction = helper.createSbmlFunction();
+      map.getModel().addSbmlFunction(sbmlFunction);
+
+      var reaction = helper.createReaction(map);
+      reaction.setKineticLaw(helper.createKineticLaw());
+      reaction.getKineticLaw().getFunctionIds().push(sbmlFunction.getId());
+
+      var reactionWindow = new ReactionInfoWindow({
+        reaction: reaction,
+        map: map
+      });
+
+      return reactionWindow.createContentDiv().then(function (div) {
+        assert.ok(div.innerHTML.indexOf("Functions") >= 0);
+        assert.ok(div.innerHTML.indexOf(sbmlFunction.getName()) >= 0, "Function name doesn't exist in the output div");
+        assert.ok(div.innerHTML.indexOf(sbmlFunction.getArguments()[0]) >= 0, "Function argument doesn't exist in the output div");
+      });
+    });
   });
 });
diff --git a/frontend-js/testFiles/apiCalls/projects/sample/models/all/functions/5/token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/projects/sample/models/all/functions/5/token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..6e768cd95b
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/projects/sample/models/all/functions/5/token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+{"functionId":"fun","name":"fun","definition":"\n<lambda>\n<bvar>\n<ci> x </ci>\n</bvar>\n<bvar>\n<ci> y </ci>\n</bvar>\n<apply>\n<plus/>\n<ci> x </ci>\n<ci> y </ci>\n<cn type=\"integer\"> 2 </cn>\n</apply>\n</lambda>\n\n","arguments":["x","y"],"id":5}
\ No newline at end of file
-- 
GitLab