Source: bemap-services/bemap-routing.js

/**
 * BeNomad BeMap JavaScript API - Routing
 */

/**
 * @classdesc
 * Base class for routing calculation.
 * @public
 * @constructor
 * @abstract
 * @param {bemap.Context} context BeMap-JS-API Context. Mandatory.
 * @param {object} options see below the available values.
 */
bemap.Routing = function(context, options) {
  bemap.Service.call(this, context, options);

  var opts = options || {};

  /**
   * Native parameters of BeMap server.
   * @type {String}
   * @protected
   */
  this.nativeBeMapParams = opts.nativeBeMapParams ? opts.nativeBeMapParams : null;

  /**
   * Calculated route(s).
   * @type {bemap.Route}
   * @protected
   */
  this.routes = [];

  /**
   * Marker(s) list.
   * @type {bemap.Marker}
   * @protected
   */
  this.markerMapObject = [];


  /**
   * Vias(s) list.
   * @type {Object}
   * @protected
   */
  this.stopPoints = [];


  /**
   * ID of geometry.
   * @type {String}
   * @protected
   */
  this.geometryId = 'routePolyline';

  /**
   * Draw a polygon with the polyline array.
   * @type {boolean}
   * @protected
   */
  this.poylineAsPolygon = false;

  // Internal resource.

  this.popup = undefined;

};
bemap.inherits(bemap.Routing, bemap.Service);

/**
 * Get the calculated route object.
 * @public
 * @param {integer} index of route.
 * @return {bemap.Route} the calculated route.
 */
bemap.Routing.prototype.getRoute = function(index) {
  return this.routes[index];
};

/**
 * Get the calculated route object.
 * @public
 * @param {integer} index of route.
 * @return {bemap.Route} the calculated route.
 */
bemap.Routing.prototype.getRoutes = function() {
  return this.routes;
};

/**
 * Get the calculated route object.
 * @public
 * @return {bemap.Route} the calculated route.
 */
bemap.Routing.prototype.getFirstRoute = function() {
  return this.routes[0];
};

/**
 * Reset the Routing object. Clear the previous result.
 * @public
 * @return {bemap.Routing} this
 */
bemap.Routing.prototype.reset = function() {
  if (this.routes) {
    for (var i = 0; i < this.routes.length; i++) {
      var route = this.routes[i];
      this.resetRoute(route);
    }
  }

  this.routes = [];

  this.stopPoints = [];

  if (this.popup) {
    this.popup.remove();
    this.popup = undefined;
  }

  //clear markers
  if (this.markerMapObject) {
    this.cleanMarkers();
  };

  return this;
};
/**
 * Reset the Routing object. Clear the previous result.
 * @public
 * @return {bemap.Routing} this
 */
bemap.Routing.prototype.resetRoute = function(route) {
  var i;

  if (route.chargingStationSteps) {
    for (i = 0; i < route.chargingStationSteps.length; i++) {
      var step = route.chargingStationSteps[i];
      if (step.markerMapObject) {
        step.markerMapObject.remove();
        step.markerMapObject = undefined;
      }
    }
  }

  if (route.events) {
    for (i = 0; i < route.events.length; i++) {
      var event = route.events[i];
      if (event.geometryMapObject) {
        event.geometryMapObject.remove();
        event.geometryMapObject = undefined;
      }
    }
  }

  if (route.geometryMapObject) {
    route.geometryMapObject.remove();
    route.geometryMapObject = undefined;
  }
};

/**
 * Generate the BeMap request in URL encoded format.
 * @private
 * @param {object} options See below the available values.
 * @param {String} options.geoserver Geoserver name will be used for this computation.
 * @return {String} the request URL encoded.
 */
bemap.Routing.prototype.buildRequest = function(options) {

  var opts = options || {};

  var data = '&geoserver=' + (opts.geoserver ? opts.geoserver : this.ctx.getGeoserver());

  for (i = 0; i < this.destinations.length; i++) {
    var des = this.destinations[i];
    if (des !== null && bemap.inheritsof(des, bemap.Destination)) {
      data += '&xy=' + des.getLon() + "," + des.getLat();
    } else if (des !== null && bemap.inheritsof(des, bemap.Coordinate)) {
      data += '&xy=' + des.getLon() + "," + des.getLat();
    } else {
      console.error("One of destinations is not a bemap.Destination or bemap.Coordinate object!");
      return;
    }
  }

  if (this.criterias && this.criterias !== null && this.criterias.length > 0) {
    data += '&criterias=';
    first = true;
    for (i = 0; i < this.criterias.length; i++) {
      if (first) {
        first = false;
      } else {
        data += ',';
      }
      data += this.criterias[i];
    }
  }

  if (this.departureTime > 0) {
    data += '&departureTime=' + this.departureTime;
  }

  if (this.evCnnType) {
    data += '&evCnnType=' + this.evCnnType;
  }

  if (this.evf) {
    data += '&evf=' + this.evf;
  }

  if (this.evRange) {
    data += '&evRange=' + this.evRange;
  }

  if (this.isoChroneLimit > 0) {
    data += '&isoChroneLimit=' + this.isoChroneLimit;
  }

  if (this.language) {
    data += '&language=' + this.language;
  }

  if (this.maxAlter > 0) {
    data += '&maxAlter=' + this.maxAlter;
  }

  if (this.options && this.options !== null && this.options.length > 0) {
    data += '&options=';
    first = true;
    for (i = 0; i < this.options.length; i++) {
      if (first) {
        first = false;
      } else {
        data += ',';
      }
      data += this.options[i];
    }
  }

  if (this.speed > 0) {
    data += '&speed=' + this.speed;
  }

  if (this.speedType) {
    data += '&speedType=' + this.speedType;
  }

  if (this.transportType) {
    data += '&transportType=' + this.transportType;
  }

  if (this.vf) {
    data += '&vf=' + this.vf;
  }

  if (this.xyRadius > 0) {
    data += '&xyRadius=' + this.xyRadius;
  }

  if (this.nativeBeMapParams && this.nativeBeMapParams !== null) {
    data += this.nativeBeMapParams;
  }
  return data;
};

/**
 * Execute the routing calculation.
 * @public
 * @param {object} options See below the available values.
 * @param {String} options.geoserver Geoserver name will be used for this computation.
 * @param {function} options.success the function to call in case of successed request.
 * @param {function} options.failed the function to call in case of failed request.
 * @return {bemap.Routing} this
 */
bemap.Routing.prototype.compute = function(options) {
  this.reset();

  var options = options || {};

  var opts = options.request || {};

  var geoserver = options.geoserver ? options.geoserver : this.ctx.getGeoserver();

  /**
   * Destinations
   * @type {bemap.Coordinates}
   * @protected
   */
  this.destinations = opts.destinations ? opts.destinations : [];

  /**
   * Array of route Criteria.
   * @type {bemap.Criteria}
   */
  this.criterias = opts.criterias ? opts.criterias : [];

  var d = new Date();
  var t = d.getTime();
  this.departureTime = opts.departureTime > 0 ? opts.departureTime : t;

  this.evCnnType = opts.evCnnType > 0 ? opts.evCnnType : null;

  this.evf = opts.evf > 0 ? opts.evf : null;

  this.evRange = opts.evRange > 0 ? opts.evRange : null;

  this.isoChroneLimit = opts.isoChroneLimit > 0 ? opts.isoChroneLimit : 0;

  this.language = opts.language ? opts.language : "xx";

  this.maxAlter = opts.maxAlter ? opts.maxAlter : 0;

  this.options = opts.options && opts.options.length > 0 ? opts.options : ['POLYLINE'];

  this.speed = opts.speed > 0 ? opts.speed : 0;

  this.speedType = opts.speedType > 0 ? opts.speedType : null;

  this.transportType = opts.transportType ? opts.transportType : "CAR";

  this.vf = opts.vf ? opts.vf : null;

  this.xyRadius = opts.xyRadius > 0 ? opts.xyRadius : 40;

  if (this.destinations && this.destinations.length < 2) {
    console.error("Minimum of 2 destionations are required!");
    return;
  }
  if (this.destinations && this.destinations.length > 2 && this.maxAlter !== 0) {
    console.error("For alternative roads 2 destionations are required");
    return;
  }
  var i = 0;
  var first = true;
  var url = this.ctx.getBaseUrl() + 'bnd';
  var data = 'version=1.0.0&action=routing&mode=MODE_VIAS&format=json';

  if (this.ctx.isAuthInPost()) {
    data += '&' + this.ctx.getAuthUrlParams();
  } else {
    url += '?' + this.ctx.getAuthUrlParams();
  }

  data += this.buildRequest(options);

  return this.execute(url, data, options);
};

/**
 * Excute the request by calling the BeMap server and wait the answer.
 * @private
 * @param {object} options Request options.
 * @return {bemap.Routing} this
 */
bemap.Routing.prototype.execute = function(url, data, options) {
  var opts = options || {};
  var _this = this;

  bemap.ajax(
    'POST',
    url,
    data,
    function(xhr, doc) {
      _this.responseParser(xhr, doc, opts);
    },
    function(xhr, doc) {
      _this.responseParser(xhr, doc, opts);
    }, {
      'requestFormat': 'urlencoded'
    }
  );

  return this;
};

/**
 * Convert the BeMap response to the BeMap JS API object.
 * @private
 **/
bemap.Routing.prototype.responseParser = function(xhr, doc, options) {
  var opts = options || {};

  doc = JSON.parse(doc);
  if (!doc || this.checkErrorParser(xhr, doc, options)) {
    return;
  }

  var bnd = doc.BND;
  if (!bnd) {
    return;
  }
  this.stopPointsParser(bnd);
  this.routeParser(bnd, options);

  if (opts.success) {
    opts.success(this, bnd, doc, xhr);
  }
};

/**
 * Convert the BeMap route(s) to the BeMap JS API Route object(s) and save into the routes array.
 * @private
 **/
bemap.Routing.prototype.routeParser = function(bnd, options) {
  var i = 0;
  var rts = bnd.Routes.Route;

  if (rts && rts.length > 0) {
    for (i = 0; i < rts.length; i++) {
      var r = rts[i];
      var route = new bemap.Route();

      route.length = r.Length;
      route.duration = r.Duration;
      route.averageSpeed = r.AverageSpeed;

      if (r.BoundingBox) {
        var rbbox = r.BoundingBox;
        route.extent = new bemap.BoundingBox(rbbox.minX, rbbox.minY, rbbox.maxX, rbbox.maxY);
      }

      if (r.Polyline && r.Polyline.Line && r.Polyline.Line.length > 0) {
        this.polylineParser(route, r.Polyline.Line);
      }
      if (r.Events && r.Events.length > 0) {
        this.eventsParser(route, r.Events);
      }
      this.routes.push(route);
    }
  }
};

bemap.Routing.prototype.stopPointsParser = function(bnd) {
  var sp = bnd.UsedDestinations.UsedDestination;

  if (sp && sp.length > 0) {
    for (i = 0; i < sp.length; i++) {
      var s = sp[i];

      s.hourMinute = this.toHourMinute(s.duration)
      s.dateString = this.msToDateString(s.duration * 1000 + this.departureTime)
      this.stopPoints.push(s);
    }
  }
};

/**
 * Convert the this duration to hours minutes string ex "3 h 14 min".
 * @private
 **/
bemap.Routing.prototype.toHourMinute = function(time) {
  if (!time || time == 0) {
    return '-';
  }

  var h = parseInt(time / 3600);
  var m = String(parseInt((time / 60) % 60)).padStart(2, '0');

  var v = '';
  if (h > 0) {
    v += h + ' h ';
  }

  return v + m + ' min';
};

/**
 * Convert the this duration to data string ex "23/01/2020 12:52".
 * @private
 **/
bemap.Routing.prototype.msToDateString = function(time) {
  if (!time || time == 0) {
    return '-';
  }
  var d = new Date(time);

  return String(d.getDate()).padStart(2, '0') + '/' + String(d.getMonth() + 1).padStart(2, '0') + '/' + d.getFullYear() + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
};


/**
 * Convert the BeMap polyline to the BeMap JS API Polyline object and save into the route object.
 * @private
 **/
bemap.Routing.prototype.polylineParser = function(route, line, options) {
  route.polyline = [];
  for (var i = 0; i < line.length; i++) {
    var pts = line[i];
    route.polyline.push(new bemap.Coordinate(pts.X, pts.Y));
  }
};

/**
 * Convert the events.
 * @private
 **/
bemap.Routing.prototype.eventsParser = function(route, events, options) {
  route.events = [];
  if (!events) {
    return;
  }
  for (var i = 0; i < events.length; i++) {
    this.evtMarkersParser(route, events[i].markers, options);
  }
};

/**
 * Convert the markers.
 * @private
 **/
bemap.Routing.prototype.evtMarkersParser = function(route, markers, options) {
  if (!markers) {
    return;
  }

  for (var i = 0; i < markers.length; i++) {

    this.evtEntriesParser(route, markers[i].entries, options);
  }
};

/**
 * Convert the entries.
 * @private
 **/
bemap.Routing.prototype.evtEntriesParser = function(route, entries, options) {
  if (!entries) {
    return;
  }

  var routeEvent = new bemap.Route.Event();
  route.events.push(routeEvent);
  for (var i = 0; i < entries.length; i++) {
    var entry = entries[i];
    if (entry.type === 'String' && entry.name === 'countryCode' && entry.value) {
      routeEvent.countryCode = entry.value;

    } else if (entry.type === 'long' && entry.name === 'length' && entry.value) {
      routeEvent.length = entry.value;

    } else if (entry.type === 'double' && entry.name === 'duration' && entry.value) {
      routeEvent.duration = entry.value;

    } else if (entry.type === 'Geometry' && entry.name === 'polyline' && entry.value) {
      if (!routeEvent.polyline) {
        routeEvent.polyline = [];
      }

      var line = entry.value.split(' ');
      for (var j = 0; j < line.length; j++) {
        var pts = line[j].split(',');
        var x = Number(pts[0]);
        var y = Number(pts[1]);
        routeEvent.polyline.push(new bemap.Coordinate(x, y));
      }

    } else if (entry.type === 'Routesheet' && entry.name === 'routesheet' && entry.instruction) {
      this.evtEntryRoutesheetInstructionParser(routeEvent, entry.instruction, options);

    } else if (entry.type === 'ChargingStationStep' && entry.name === 'chargingStationStep') {
      this.evtEntryChargingStationStepParser(routeEvent, entry, options);
    }
  }
};

/**
 * Convert the entry route-sheet instruction.
 * @private
 **/
bemap.Routing.prototype.evtEntryRoutesheetInstructionParser = function(routeEvent, instruction, options) {
  var routesheetInstruction = new bemap.Route.RoutesheetInstruction();
  var c = new bemap.Coordinate(instruction.coordinate.x, instruction.coordinate.y);
  routesheetInstruction.coordinate = c;
  bemap.fillFields(instruction, routesheetInstruction);
  routeEvent.routesheetInstruction = routesheetInstruction;
};

/**
 * Convert the entry ChargingStationStep.
 * @private
 **/
bemap.Routing.prototype.evtEntryChargingStationStepParser = function(routeEvent, chargingStationStep, options) {
  var step = new bemap.ChargingStationStep();
  step.entrance = new bemap.Coordinate(chargingStationStep.longitude, chargingStationStep.latitude);
  bemap.fillFields(chargingStationStep, step);
  routeEvent.chargingStationSteps.push(step);
};

/**
 * Show the route(s) on map.
 * @param {bemap.Map} map the new color of text border to set.
 * @param {Object} options See below the available values.
 * @param {bemap.LineStyle} options.polylineStyle Style of line used by the renderer.
 * @param {String} options.chargingStationStepImageSrc Image path of pool stations.
 * @param {String} options.chargingStationStepNoPopup If set to true, disable the popup when the icon is clicked.
 * @param {function} options.chargingStationPopupTextCallback Function called wehen a pool statiion is clicked, the called function get the step point object and return the text of popup.
 * @return {bemap.Routing} Return this.
 */
bemap.Routing.prototype.showOnMap = function(map, options, listener) {
  if (!this.routes) {
    return;
  }

  var opts = options || {};

  if (!opts.polylineStyle || !bemap.inheritsof(opts.polylineStyle, bemap.LineStyle)) {
    opts.polylineStyle = new bemap.LineStyle({
      width: 3,
      color: new bemap.Color(13, 80, 157, 1)
    });
  }

  if (!opts.polygonStyle || !bemap.inheritsof(opts.polygonStyle, bemap.PolygonStyle)) {
    opts.polygonStyle = new bemap.PolygonStyle({
      fillColor: new bemap.Color(255, 0, 255, 0.25),
      borderColor: new bemap.Color(255, 0, 255, 0.25),
      borderWidth: 3
    });
  }

  var red = 13;
  var green = 80;
  var blue = 157;
  for (var i = 0; i < this.routes.length; i++) {

    var color = new bemap.Color(red, green, blue)
    red = red + 50;
    green = green + 40;
    blue = blue + 30

    if (opts.changeColor) {
      opts.polylineStyle.color = color
    }

    var route = this.routes[i];
    opts.polylineId = i;

    if (this.poylineAsPolygon) {
      this.showPolygon(map, route, opts);
    } else {
      if (route.polyline) {
        this.showPolyline(map, route, opts, function(data) {
          if (listener) {
            listener(data);
          };
        });
      }
      if (route.events && route.events.length > 0) {
        this.showEventPolyline(map, route, opts, function(data) {
          if (listener) {
            listener(data);
          };
        });
      }
    }

    if (route.extent) {
      map.moveToBoundingBox(route.extent);
    }

    this.showChargingStationSteps(map, route.chargingStationSteps, opts);
  }

  return this;
};

/**
 * Show the destinations markers on map.
 * @public
 * @param {bemap.Map} map the new marker to set.
 * @param {Object} options See below the available values.
 * @param {bemap.Layer} options.layer Layer for markers.
 * @param {Object} options.response Optionally response from geocoding or list of coords.
 * @param {listener} listener return click on marker.
 * @return {bemap.markerMapObject} Return this.
 */
bemap.Routing.prototype.showOnMapMarkers = function(options, listener) {

  if (!options) {
    console.error("Options required");
  };
  var map = options.map ? options.map : bemap.map;
  var layer = options.layer ? options.layer : '';
  var icone = options.icon ? options.icon : {};
  var coord = options.coord ? options.coord : {};
  var doc = {};

  if (!options.response) {
    doc.destinations = this.destinations;
  } else {
    doc = options.response;
  };

  var optionsMarker = {
    map: map,
    properties: '',
    coord: coord,
    icon: icone,
    layer: layer
  }

  this.cleanMarkers();
  //create merkers from parsed response - object UsedDestinations
  if (doc.UsedDestinations) {

    for (var i = 0; i < doc.UsedDestinations.UsedDestination.length; i++) {
      e = doc.UsedDestinations.UsedDestination[i]
      optionsMarker.properties = doc.UsedDestinations.UsedDestination[i];
      optionsMarker.coord = new bemap.Coordinate(e.usedX, e.usedY)
      this.createMarker(optionsMarker, function(data) {
        if (listener) {
          listener(data);
        };
      });
    };
  }
  //create merkers from notparsed response - object destinations
  //or from this.destinations
  else if (doc.destinations) {
    for (var i = 0; i < doc.destinations.length; i++) {
      e = doc.destinations[i]
      optionsMarker.properties = doc.stopPoints[i];
      optionsMarker.coord = new bemap.Coordinate(e.lon, e.lat)
      this.createMarker(optionsMarker, function(data) {
        if (listener) {
          listener(data);
        };
      });
    };
  } else {

    optionsMarker.coord = options.coord;
    this.createMarker(optionsMarker, function(data) {
      if (listener) {
        listener(data);
      };
    });
  };

  return this.markerMapObject;
};

/**
 * Clean the markerMapObject object.
 * @public
 * @return {bemap.markerMapObject} this
 */
bemap.Routing.prototype.cleanMarkers = function() {
  if (this.markerMapObject) {
    for (i = 0; i < this.markerMapObject.length; i++) {
      var marker = this.markerMapObject[i];
      marker.remove();
      marker = undefined;
    };
    this.markerMapObject = [];
  } else {
    this.markerMapObject = [];
  };
};
/**
 * Show the destinations markers on map.
 * @public
 * @param {bemap.Map} options.map the new marker to set.
 * @param {Object} options See below the available values.
 * @param {listener} listener return click on marker.
 * @return {bemap.markerMapObject} Return this.
 */
bemap.Routing.prototype.createMarker = function(options, listener) {

  var opts = options ? options : {};
  var map = opts.map ? opts.map : bemap.map;
  var properties = opts.properties ? opts.properties : opts.coord;
  var coord = opts.coord ? opts.coord : console.error("Coords required");
  var icone = opts.icon ? opts.icon : '';
  var layer = opts.layer ? opts.layer : false;
  var icon = new bemap.Icon({
    src: icone.src ? icone.src : console.error("required icon/icon adress is wrong"),
    anchorX: icone.anchorX ? icone.anchorX : 0.25,
    anchorY: icone.anchorY ? icone.anchorY : 1,
    height: icone.height ? icone.height : '',
    width: icone.width ? icone.width : '',
    anchorXUnits: icone.anchorXUnits ? icone.anchorXUnits : 'fraction',
    anchorYUnits: icone.anchorYUnits ? icone.anchorYUnits : 'fraction',
    scale: icone.scale ? icone.scale : 1
  });

  var marker = new bemap.Marker(
    coord, {
      properties: properties,
      icon: icon
    }
  );

  if (layer) {
    map.addMarker(marker, {
      layer: layer
    });
  } else {
    map.addMarker(marker);
  };

  this.markerMapObject.push(marker);

  marker.on(bemap.Map.EventType.CLICK, function(mapEvent) {
    if (listener) {
      listener(mapEvent);
    };
  });

  return marker
};


/**
 * Show a polyline on map
 * @private
 **/
bemap.Routing.prototype.showPolyline = function(map, route, options, listener) {

  var opts = options || {};
  var layer = opts.layer ? opts.layer : '';
  var p = new bemap.Polyline(
    route.polyline, {
      style: opts.polylineStyle,
      id: this.geometryId + opts.polylineId
    }
  );
  route.geometryMapObject = p;

  if (layer) {
    map.addPolyline(p, {
      layer: layer
    });
  } else {
    map.addPolyline(p);
  };


  p.on(bemap.Map.EventType.CLICK, function(mapEvent) {
    if (listener) {
      listener(mapEvent);
    };
  })
};

/**
 * Show a polygon on map
 * @private
 **/
bemap.Routing.prototype.showPolygon = function(map, route, options) {
  var opts = options || {};
  var p = new bemap.Polygon(
    route.polyline, {
      style: opts.polygonStyle,
      id: this.geometryId + opts.polygonId
    }
  );
  route.geometryMapObject = p;
  map.addPolygon(p);

};

/**
 * Show a polyline on map
 * @private
 **/
bemap.Routing.prototype.showEventPolyline = function(map, route, options, listener) {
  var opts = options || {};
  var layer = opts.layer ? opts.layer : '';
  var events = route.events;
  for (var i = 0; i < events.length; i++) {
    var event = events[i];
    var polyline = event.polyline;

    var p = new bemap.Polyline(
      polyline, {
        style: opts.polylineStyle,
        id: this.geometryId + opts.polylineId + '#evt' + i
      }
    );
    event.geometryMapObject = p;
    if (layer) {
      map.addPolyline(p, {
        layer: layer
      });
    } else {
      map.addPolyline(p);
    };
    p.on(bemap.Map.EventType.CLICK, function(mapEvent) {
      if (listener) {
        listener(mapEvent);
      };
    })
  }
};

/**
 * Show a charging station step(s) on map
 * @param {object} options See below the available values.
 * @param {String} options.chargingStationStepImageSrc Image path of pool stations.
 * @param {String} options.chargingStationStepNoPopup If set to true, disable the popup when the icon is clicked.
 * @param {function} options.chargingStationPopupTextCallback Function called wehen a pool statiion is clicked, the called function get the step point object and return the text of popup.
 * @private
 **/
bemap.Routing.prototype.showChargingStationSteps = function(map, chargingStationSteps, options) {
  if (!chargingStationSteps || chargingStationSteps.lengh == 0) {
    return;
  }

  for (var i = 0; i < chargingStationSteps.length; i++) {
    this.showChargingStationStep(map, chargingStationSteps[i], options);
  }
};

/**
 * Show a charging station step on map
 * @private
 **/
bemap.Routing.prototype.showChargingStationStep = function(map, step, options) {
  if (!step.entrance) {
    return;
  }

  var _this = this;
  var src = options.chargingStationStepImageSrc ? options.chargingStationStepImageSrc : 'images/map-marker-red.svg';
  var marker = new bemap.Marker(
    step.entrance, {
      icon: new bemap.Icon({
        src: src,
        anchorX: options.chargingStationStepImageAnchorX ? options.chargingStationStepImageAnchorX : 0.50,
        anchorY: options.chargingStationStepImageAnchorY ? options.chargingStationStepImageAnchorY : 1,
        anchorXUnits: 'fraction',
        anchorYUnits: 'fraction'
      })
    });
  map.addMarker(marker);

  step.markerMapObject = marker;

  if (!options.chargingStationStepNoPopup) {
    marker.on(bemap.Map.EventType.CLICK, function() {
      _this.popupChargingStationStep(map, step, options);
    });
  }
};

/**
 * Show a charging station step on map
 * @param {object} options See below the available values.
 * @param {function} options.chargingStationPopupTextCallback Function called wehen a pool statiion is clicked, the called function get the step point object and return the text of popup.
 * @private
 **/
bemap.Routing.prototype.popupChargingStationStep = function(map, step, options) {
  if (!this.popup) {
    this.popup = new bemap.Popup({
      coordinate: step.entrance,
      visible: false
    });
    map.addPopup(this.popup);
  }

  var html;
  if (options.chargingStationPopupTextCallback) {
    html = options.chargingStationPopupTextCallback(step);

  } else {
    html = '<p>';

    if (step.id) {
      html += 'Id: ' + step.id + '<br/>';
    }

    if (step.entrance) {
      var entrance = step.entrance;
      html += 'Latitude, longitude: ' + entrance.getLat() + ', ' + entrance.getLon() + '<br/>';
    }

    if (step.country || step.city || step.street) {
      html += 'Address:<br/>';

      if (step.streeNumber && step.street) {
        html += step.streeNumber + ' ' + step.street + '<br/>';
      } else {
        if (step.streeNumber) {
          html += step.streeNumber;
        }
        if (step.street) {
          html += step.street;
        }
        html += '<br/>';
      }

      if (step.postalCode && step.city) {
        html += step.postalCode + ' ' + step.city + '<br/>';
      } else {
        if (step.postalCode) {
          html += step.postalCode;
        }
        if (step.city) {
          html += step.city;
        }
        html += '<br/>';
      }

      if (step.country) {
        html += step.country + '<br/>';
      }
    }

    if (step.batteryChargeLevel) {
      html += 'Battery: ' + step.batteryChargeLevel + '%<br/>';
    }
    if (step.chargingTime) {
      html += 'Charging time: ' + step.chargingTime + 'ms<br/>';
    }
    if (step.consumedFromPreviousStop) {
      html += 'Consumed: ' + step.consumedFromPreviousStop + 'kW<br/>';
    }
    if (step.maxNominalPower) {
      html += 'Maximum power: ' + step.maxNominalPower + 'kW<br/>';
    }
    if (step.numberOfChargingPoint) {
      html += 'Number of charge points: ' + step.numberOfChargingPoint + '<br/>';
    }

    html += '</p>';
  }

  this.popup.setInformation(html);
  this.popup.setCoordinate(step.entrance).show();
};