Fork me on GitHub
Show:

File: ../src/maps/routes/waypoint.js

define([
  'aeris/util',
  'aeris/maps/markers/marker',
  'aeris/togglebehavior',
  'aeris/helpers/validator/pathvalidator',
  'aeris/maps/polylines/polyline',
  'aeris/directions/googledirectionsservice',
  'aeris/directions/nonstopdirectionsservice',
  'aeris/maps/routes/errors/jsonparseerror',
  'aeris/maps/routes/errors/waypointnotinrouteerror',
  'aeris/errors/validationerror'
], function(
  _,
  Marker,
  ToggleBehavior,
  PathValidator,
  Polyline,
  GoogleDirectionsService,
  NonstopDirectionsService,
  JSONParseError,
  WaypointNotInRouteError,
  ValidationError
) {
  /**
   * A Waypoint is a marker along a route.
   * A waypoint has a path, as well as information
   * about how to get the path.
   *
   * @class aeris.maps.gmaps.route.Waypoint
   * @extends aeris.maps.markers.Marker
   *
   * @uses aeris.ToggleBehavior
   *
   * @constructor
   * @override
   *
   * @param {aeris.maps.extensions.MapObjectInterface=} opt_options.polyline
   * @param {aeris.helpers.validator.PathValidator=} opt_options.pathValidator
   * @param {aeris.directions.DirectionsServiceInterface=} opt_options.directionsService
   * @param {aeris.directions.DirectionsServiceInterface=} opt_options.nonstopDirectionsService
   */
  var Waypoint = function(opt_attrs, opt_options) {
    var attrs = _.defaults(opt_attrs || {}, {
      position: null,
      followDirections: true,
      travelMode: Waypoint.travelMode.WALKING,
      path: [],
      distance: 0
    });
    var options = opt_options || {};

    /**
     * @type {aeris.maps.extensions.MapObjectInterface}
     * @default {aeris.maps.polylines.Polyline}
     * @private
     * @property polyline_
     */
    this.polyline_ = options.polyline || new Polyline();


    /**
     * @type {aeris.directions.DirectionsServiceInterface}
     * @default {aeris.directions.GoogleDirectionsService}
     * @private
     * @property directionsService_
     */
    this.directionsService_ = options.directionsService || new GoogleDirectionsService();


    /**
     * @type {aeris.directions.DirectionsServiceInterface}
     * @default {aeris.directions.NonstopDirectionsService}
     * @private
     * @property nonstopDirectionsService_
     */
    this.nonstopDirectionsService_ = options.nonstopDirectionsService || new NonstopDirectionsService();


    /**
     * @type {aeris.helpers.validator.ValidatorInterface}
     * @default {aeris.helpers.validator.PathValidator}
     * @private
     * @property pathValidator_
     */
    this.pathValidator_ = options.pathValidator || new PathValidator();
    this.pathValidator_.setPath(attrs.path);


    Marker.call(this, attrs, options);
    ToggleBehavior.call(this);


    /**
     * @event reset
     */
    /**
     * @event path:click
     * @param {aeris.maps.LatLon} latLon
     * @param {aeris.maps.gmaps.route.Waypoint} waypoint
     */
    /**
     * When a waypoint fails retrieve directions data.
     *
     * @event directions:error
     * @param {aeris.directions.results.DirectionsResults} directionsResults
     */

    this.initializePolylineBindings_();
  };
  _.inherits(Waypoint, Marker);
  _.extend(Waypoint.prototype, ToggleBehavior.prototype);


  /**
   * @private
   * @method initializePolylineBindings_
   */
  Waypoint.prototype.initializePolylineBindings_ = function() {
    this.updatePolyline_();

    this.listenTo(this, {
      'change:map change:path reset': this.updatePolyline_
    });

    this.listenTo(this.polyline_, {
      'click': function(latLon) {
        this.trigger('path:click', latLon, this);
      }
    });
  };


  /**
   * Update a polyline to be in sync
   * with the waypoint's path.
   *
   * @private
   * @method updatePolyline_
   */
  Waypoint.prototype.updatePolyline_ = function() {
    this.polyline_.set({
      map: this.getMap(),
      path: this.get('path')
    }, { validate: true });
  };


  /**
   * @method validate
   */
  Waypoint.prototype.validate = function(attrs) {
    var pathError = this.validatePath_(attrs.path);

    if (pathError) { return pathError; }

    if (!_.isNumber(attrs.distance) || attrs.distance < 0) {
      return new ValidationError(attrs.distance + ' is not a valid waypoint distance');
    }

    if (!_.isBoolean(attrs.followDirections)) {
      return new ValidationError(attrs.followDirections + ' is not a valid followDirections option');
    }

    if (!_.isString(attrs.travelMode)) {
      return new ValidationError(attrs.travelMode + ' is not a valid travelMode options');
    }

    return Marker.prototype.validate.apply(this, arguments);
  };


  Waypoint.prototype.validatePath_ = function(path) {
    this.pathValidator_.setPath(path);

    if (!this.pathValidator_.isValid()) {
      return this.pathValidator_.getLastError();
    }
  };


  /**
   * @return {number}
   * @method getDistance
   */
  Waypoint.prototype.getDistance = function() {
    return this.get('distance');
  };


  /**
   * @return {Boolean}
   * @method hasPath
   */
  Waypoint.prototype.hasPath = function() {
    return !!this.get('path').length;
  };


  /**
   * @return {aeris.Collection|undefined}
   * @method getRoute
   */
  Waypoint.prototype.getRoute = function() {
    // Must check that collection contains the model.
    // See https://github.com/jashkenas/backbone/issues/2929
    var hasRoute = this.collection && this.collection.contains(this);
    return hasRoute ? this.collection : undefined;
  };


  Waypoint.prototype.getNextInRoute = function() {
    this.ensureHasRoute_();
    return this.getRoute().getNext(this);
  };


  Waypoint.prototype.getPreviousInRoute = function() {
    this.ensureHasRoute_();
    return this.getRoute().getPrevious(this);
  };

  Waypoint.prototype.ensureHasRoute_ = function() {
    if (!this.getRoute()) {
      throw new WaypointNotInRouteError();
    }
  };


  /**
   * Serialize Waypoint as a JSON object
   *
   * @return {Object} Exported waypoint as JSON object.
   * @method toJSON
   */
  Waypoint.prototype.toJSON = function() {
    var baseJSON = Marker.prototype.toJSON.apply(this, arguments);

    // Limit JSON to the specified properties
    return _.pick(baseJSON,
      'position',
      'followDirections',
      'travelMode',
      'path',
      'distance'
    );
  };


  /**
   * Exports the waypoint as a JSON string
   *
   * @return {string} JSON representation of a waypoint.
   * @method export
   */
  Waypoint.prototype.export = function() {
    return JSON.stringify(this);
  };


  /**
   * Import a JSON string as a waypoint.
   * Replaces any existing waypoint data.
   *
   * @param {string} jsonStr JSON representation of a waypoint.
   * @method import
   */
  Waypoint.prototype.import = function(jsonStr) {
    var obj;
    try {
      obj = JSON.parse(jsonStr);
    }
    catch (e) {
      if (e instanceof window.SyntaxError) {
        throw new JSONParseError('Invalid JSON string');
      }

      throw e;
    }

    this.reset(obj);
  };


  /**
   * Replaces entire model with new attributes,
   * and validates.
   *
   * Triggers a 'reset' event (and DOES NOT trigger a set event)
   *
   * @param {Object|aeris.maps.gmaps.route.Waypoint || Object} obj
   * @param {Object=} opt_options Same options as Backbone.Model#set.
   * @method reset
   */
  Waypoint.prototype.reset = function(obj, opt_options) {
    var options = _.defaults(opt_options || {}, {
      silent: true,
      validate: true
    });

    obj = (obj instanceof Waypoint) ? obj.toJSON() : obj;

    this.set({
      position: obj.position,
      followDirections: obj.followDirections,
      travelMode: obj.travelMode,
      path: obj.path,
      distance: obj.distance
    }, options);

    this.trigger('reset');
  };


  /**
   * Set style on the polyine
   * for the waypoint's path.
   *
   * @param {Object} styles
   * @method stylePath
   */
  Waypoint.prototype.stylePath = function(styles) {
    this.polyline_.setStyles(styles);
  };


  /**
   * @param {aeris.maps.LatLon} pathStartingPosition
   * @return {aeris.Promise}
   * @method setPathStartsAt
   */
  Waypoint.prototype.setPathStartsAt = function(pathStartingPosition) {
    var promiseToFetchDirections;
    var directionsService = this.getDirectionsService_();
    var toLatLon = this.getPosition();

    this.trigger('directions:request', this);

    promiseToFetchDirections = directionsService.fetchPath(pathStartingPosition, toLatLon, {
      travelMode: this.get('travelMode')
    });

    promiseToFetchDirections.
      done(this.updateFromDirectionsResults_, this).
      done(this.triggerDirectionsComplete_, this).
      fail(this.triggerDirectionsServiceError_, this);

    return promiseToFetchDirections;
  };


  Waypoint.prototype.getDirectionsService_ = function() {
    return this.get('followDirections') ?
      this.directionsService_ :
      this.nonstopDirectionsService_;
  };


  /**
   * @param {aeris.directions.results.DirectionsResults} directionsResults
   * @private
   * @method updateFromDirectionsResults_
   */
  Waypoint.prototype.updateFromDirectionsResults_ = function(directionsResults) {
    var lastPointInPath = directionsResults.path[directionsResults.path.length - 1];

    this.set({
      path: directionsResults.path,
      position: lastPointInPath,
      distance: directionsResults.distance
    }, { validate: true });
  };


  Waypoint.prototype.triggerDirectionsComplete_ = function() {
    this.trigger('directions:complete', this);
  };


  /**
   * @param {aeris.directions.results.DirectionsResults} directionsResults
   * @private
   * @method triggerDirectionsServiceError_
   */
  Waypoint.prototype.triggerDirectionsServiceError_ = function(directionsResults) {
    this.trigger('directions:error', this, directionsResults);
  };


  /**
   * @const
   * @static
   * @type {Object}
   */
  Waypoint.travelMode = {
    WALKING: 'WALKING',
    DRIVING: 'DRIVING',
    BICYCLING: 'BICYCLING',
    TRANSIT: 'TRANSIT'
  };


  return _.expose(Waypoint, 'aeris.maps.gmaps.route.Waypoint');
});