<commit_before>goog.provide('ngeo.geolocation.mobile');

goog.require('ngeo');
goog.require('ngeo.Notification');
goog.require('ol.easing');
goog.require('ol.events');
goog.require('ol.Feature');
goog.require('ol.Geolocation');
goog.require('ol.Map');
goog.require('ol.geom.Point');

goog.require('ngeo.map.FeatureOverlayMgr');

/**
 * @type {!angular.Module}
 */
ngeo.geolocation.mobile = angular.module('ngeoMobileGeolocation', [
  ngeo.map.FeatureOverlayMgr.module.name
]);

ngeo.module.requires.push(ngeo.geolocation.mobile.name);

/**
 * @enum {string}
 */
ngeo.geolocation.mobile.GeolocationEventType = {
  /**
   * Triggered when an error occurs.
   */
  ERROR: 'mobile-geolocation-error'
};

/**
 * Provide a "mobile geolocation" directive.
 *
 * Example:
 *
 *      <button ngeo-mobile-geolocation
 *        ngeo-mobile-geolocation-map="ctrl.map"
 *        ngeo-mobile-geolocation-options="ctrl.mobileGeolocationOptions">
 *      </button>
 *
 * See our live example: [../examples/mobilegeolocation.html](../examples/mobilegeolocation.html)
 *
 * @htmlAttribute {ol.Map} ngeo-mobile-geolocation-map The map.
 * @htmlAttribute {ngeox.MobileGeolocationDirectiveOptions} ngeo-mobile-geolocation-options The options.
 * @return {angular.Directive} The Directive Definition Object.
 * @ngInject
 * @ngdoc directive
 * @ngname ngeoMobileGeolocation
 */
ngeo.geolocation.mobile.directive_ = function() {
  return {
    restrict: 'A',
    scope: {
      'getMobileMapFn': '&ngeoMobileGeolocationMap',
      'getMobileGeolocationOptionsFn': '&ngeoMobileGeolocationOptions'
    },
    controller: 'ngeoGeolocationMobileController'
  };
};

ngeo.geolocation.mobile.directive('ngeoMobileGeolocation', ngeo.geolocation.mobile.directive_);

/**
 * @constructor
 * @private
 * @struct
 * @param {angular.Scope} $scope The directive's scope.
 * @param {angular.JQLite} $element Element.
 * @param {angularGettext.Catalog} gettextCatalog Gettext service.
 * @param {ngeo.map.FeatureOverlayMgr} ngeoFeatureOverlayMgr The ngeo feature
 *     overlay manager service.
 * @param {ngeo.Notification} ngeoNotification Ngeo notification service.
 * @ngInject
 * @ngdoc controller
 * @ngname NgeoMobileGeolocationController
 */
ngeo.geolocation.mobile.Controller_ = function($scope, $element,
  gettextCatalog, ngeoFeatureOverlayMgr, ngeoNotification) {

  $element.on('click', this.toggleTracking.bind(this));

  const map = $scope['getMobileMapFn']();
  goog.asserts.assertInstanceof(map, ol.Map);

  /**
   * @type {!angular.Scope}
   * @private
   */
  this.$scope_ = $scope;

  /**
   * @type {!ol.Map}
   * @private
   */
  this.map_ = map;

  const options = $scope['getMobileGeolocationOptionsFn']() || {};
  goog.asserts.assertObject(options);

  /**
   * @type {ngeo.Notification}
   * @private
   */
  this.notification_ = ngeoNotification;

  /**
   * @type {ngeo.map.FeatureOverlay}
   * @private
   */
  this.featureOverlay_ = ngeoFeatureOverlayMgr.getFeatureOverlay();

  /**
   * @type {ol.Geolocation}
   * @private
   */
  this.geolocation_ = new ol.Geolocation({
    projection: map.getView().getProjection(),
    trackingOptions: /** @type {GeolocationPositionOptions} */ ({
      enableHighAccuracy: true
    })
  });

  if (options.autorotate) {
    this.autorotateListener();
  }

  // handle geolocation error.
  this.geolocation_.on('error', function(error) {
    this.untrack_();
    let msg;
    switch (error.code) {
      case 1:
        msg = gettextCatalog.getString('User denied the request for Geolocation.');
        break;
      case 2:
        msg = gettextCatalog.getString('Location information is unavailable.');
        break;
      case 3:
        msg = gettextCatalog.getString('The request to get user location timed out.');
        break;
      default:
        msg = gettextCatalog.getString('Geolocation: An unknown error occurred.');
        break;
    }
    this.notification_.error(msg);
    $scope.$emit(ngeo.geolocation.mobile.GeolocationEventType.ERROR, error);
  }, this);

  /**
   * @type {ol.Feature}
   * @private
   */
  this.positionFeature_ = new ol.Feature();

  if (options.positionFeatureStyle) {
    this.positionFeature_.setStyle(options.positionFeatureStyle);
  }

  /**
   * @type {ol.Feature}
   * @private
   */
  this.accuracyFeature_ = new ol.Feature();

  if (options.accuracyFeatureStyle) {
    this.accuracyFeature_.setStyle(options.accuracyFeatureStyle);
  }

  /**
   * @type {number|undefined}
   * @private
   */
  this.zoom_ = options.zoom;

  /**
   * Whether to recenter the map at the position it gets updated
   * @type {boolean}
   * @private
   */
  this.follow_ = false;

  /**
   * A flag used to determine whether the view was changed by me or something
   * else. In the latter case, stop following.
   * @type {boolean}
   * @private
   */
  this.viewChangedByMe_ = false;

  ol.events.listen(this.geolocation_, 'change:accuracyGeometry', () => {
    this.accuracyFeature_.setGeometry(this.geolocation_.getAccuracyGeometry());
    this.setPosition_();
  });

  ol.events.listen(this.geolocation_, 'change:position', () => {
    this.setPosition_();
  });

  const view = map.getView();

  ol.events.listen(view, 'change:center', this.handleViewChange_, this);

  ol.events.listen(view, 'change:resolution', this.handleViewChange_, this);

};

/**
 * @export
 */
ngeo.geolocation.mobile.Controller_.prototype.toggleTracking = function() {
  if (this.geolocation_.getTracking()) {
    // if map center is different than geolocation position, then track again
    const currentPosition = this.geolocation_.getPosition();
    // if user is using Firefox and selects the "not now" option, OL geolocation
    // doesn't return an error
    if (currentPosition === undefined) {
      this.untrack_();
      this.$scope_.$emit(ngeo.geolocation.mobile.GeolocationEventType.ERROR, null);
      return;
    }
    goog.asserts.assert(currentPosition !== undefined);
    // stop tracking if the position is close to the center of the map.
    const center = this.map_.getView().getCenter();
    const diff = Math.abs(currentPosition[0] - center[0]) + Math.abs(currentPosition[1] - center[1]);
    if (diff < 2) {
      this.untrack_();
    } else {
      this.untrack_();
      this.track_();
    }
  } else {
    this.track_();
  }
};

/**
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.track_ = function() {
  this.featureOverlay_.addFeature(this.positionFeature_);
  this.featureOverlay_.addFeature(this.accuracyFeature_);
  this.follow_ = true;
  this.geolocation_.setTracking(true);
};

/**
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.untrack_ = function() {
  this.featureOverlay_.clear();
  this.follow_ = false;
  this.geolocation_.setTracking(false);
  this.notification_.clear();
};

/**
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.setPosition_ = function() {
  const position = /** @type {ol.Coordinate} */ (this.geolocation_.getPosition());
  const point = new ol.geom.Point(position);

  this.positionFeature_.setGeometry(point);
  const accuracy = this.accuracyFeature_.getGeometry();

  if (this.follow_) {
    this.viewChangedByMe_ = true;
    if (this.zoom_ !== undefined) {
      this.map_.getView().setCenter(position);
      this.map_.getView().setZoom(this.zoom_);
    } else if (accuracy) {
      const size = /** @type {!ol.Size} */ (this.map_.getSize());
      this.map_.getView().fit(/** @type {!ol.geom.Polygon} */ (accuracy), size);
    }
    this.viewChangedByMe_ = false;
  }
};

/**
 * @param {ol.Object.Event} event Event.
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.handleViewChange_ = function(event) {
  if (this.follow_ && !this.viewChangedByMe_) {
    this.follow_ = false;
  }
};

// Orientation control events
ngeo.geolocation.mobile.Controller_.prototype.autorotateListener = function() {
  let currentAlpha = 0;
  if (window.hasOwnProperty('ondeviceorientationabsolute')) {
    window.addEventListener('deviceorientationabsolute', (evt) => {
      currentAlpha = this.handleRotate_(evt.alpha, currentAlpha);
    }, true);
  } else if (window.hasOwnProperty('ondeviceorientation')) {
    window.addEventListener('deviceorientation', (evt) => {
      if (evt.webkitCompassHeading) { // check for iOS property
        currentAlpha = this.handleRotate_(-evt.webkitCompassHeading, currentAlpha);
      } else { // non iOS
        currentAlpha = this.handleRotate_(evt.alpha - 270, currentAlpha);
      }
    }, true);
  } else {
    console.error('Orientation is not supported on this device');
  }
};

/**
 * Handle rotation.
 * @param {number} eventAlpha .
 * @param {number} currentAlpha .
 * @return {number} .
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.handleRotate_ = function(eventAlpha, currentAlpha) {
  if (Math.abs(eventAlpha - currentAlpha) > 0.2) {
    currentAlpha = eventAlpha;
    const radAlpha = currentAlpha * Math.PI / 180;
    this.map_.getView().animate({
      rotation: radAlpha,
      duration: 350,
      easing: ol.easing.linear
    });
  }
  return currentAlpha;
};

ngeo.geolocation.mobile.controller('ngeoGeolocationMobileController',
  ngeo.geolocation.mobile.Controller_);
<commit_msg>Don't update the view rotation if the geolocation is not active<commit_after>goog.provide('ngeo.geolocation.mobile');

goog.require('ngeo');
goog.require('ngeo.Notification');
goog.require('ol.easing');
goog.require('ol.events');
goog.require('ol.Feature');
goog.require('ol.Geolocation');
goog.require('ol.Map');
goog.require('ol.geom.Point');

goog.require('ngeo.map.FeatureOverlayMgr');

/**
 * @type {!angular.Module}
 */
ngeo.geolocation.mobile = angular.module('ngeoMobileGeolocation', [
  ngeo.map.FeatureOverlayMgr.module.name
]);

ngeo.module.requires.push(ngeo.geolocation.mobile.name);

/**
 * @enum {string}
 */
ngeo.geolocation.mobile.GeolocationEventType = {
  /**
   * Triggered when an error occurs.
   */
  ERROR: 'mobile-geolocation-error'
};

/**
 * Provide a "mobile geolocation" directive.
 *
 * Example:
 *
 *      <button ngeo-mobile-geolocation
 *        ngeo-mobile-geolocation-map="ctrl.map"
 *        ngeo-mobile-geolocation-options="ctrl.mobileGeolocationOptions">
 *      </button>
 *
 * See our live example: [../examples/mobilegeolocation.html](../examples/mobilegeolocation.html)
 *
 * @htmlAttribute {ol.Map} ngeo-mobile-geolocation-map The map.
 * @htmlAttribute {ngeox.MobileGeolocationDirectiveOptions} ngeo-mobile-geolocation-options The options.
 * @return {angular.Directive} The Directive Definition Object.
 * @ngInject
 * @ngdoc directive
 * @ngname ngeoMobileGeolocation
 */
ngeo.geolocation.mobile.directive_ = function() {
  return {
    restrict: 'A',
    scope: {
      'getMobileMapFn': '&ngeoMobileGeolocationMap',
      'getMobileGeolocationOptionsFn': '&ngeoMobileGeolocationOptions'
    },
    controller: 'ngeoGeolocationMobileController'
  };
};

ngeo.geolocation.mobile.directive('ngeoMobileGeolocation', ngeo.geolocation.mobile.directive_);

/**
 * @constructor
 * @private
 * @struct
 * @param {angular.Scope} $scope The directive's scope.
 * @param {angular.JQLite} $element Element.
 * @param {angularGettext.Catalog} gettextCatalog Gettext service.
 * @param {ngeo.map.FeatureOverlayMgr} ngeoFeatureOverlayMgr The ngeo feature
 *     overlay manager service.
 * @param {ngeo.Notification} ngeoNotification Ngeo notification service.
 * @ngInject
 * @ngdoc controller
 * @ngname NgeoMobileGeolocationController
 */
ngeo.geolocation.mobile.Controller_ = function($scope, $element,
  gettextCatalog, ngeoFeatureOverlayMgr, ngeoNotification) {

  $element.on('click', this.toggleTracking.bind(this));

  const map = $scope['getMobileMapFn']();
  goog.asserts.assertInstanceof(map, ol.Map);

  /**
   * @type {!angular.Scope}
   * @private
   */
  this.$scope_ = $scope;

  /**
   * @type {!ol.Map}
   * @private
   */
  this.map_ = map;

  const options = $scope['getMobileGeolocationOptionsFn']() || {};
  goog.asserts.assertObject(options);

  /**
   * @type {ngeo.Notification}
   * @private
   */
  this.notification_ = ngeoNotification;

  /**
   * @type {ngeo.map.FeatureOverlay}
   * @private
   */
  this.featureOverlay_ = ngeoFeatureOverlayMgr.getFeatureOverlay();

  /**
   * @type {ol.Geolocation}
   * @private
   */
  this.geolocation_ = new ol.Geolocation({
    projection: map.getView().getProjection(),
    trackingOptions: /** @type {GeolocationPositionOptions} */ ({
      enableHighAccuracy: true
    })
  });

  if (options.autorotate) {
    this.autorotateListener();
  }

  // handle geolocation error.
  this.geolocation_.on('error', function(error) {
    this.untrack_();
    let msg;
    switch (error.code) {
      case 1:
        msg = gettextCatalog.getString('User denied the request for Geolocation.');
        break;
      case 2:
        msg = gettextCatalog.getString('Location information is unavailable.');
        break;
      case 3:
        msg = gettextCatalog.getString('The request to get user location timed out.');
        break;
      default:
        msg = gettextCatalog.getString('Geolocation: An unknown error occurred.');
        break;
    }
    this.notification_.error(msg);
    $scope.$emit(ngeo.geolocation.mobile.GeolocationEventType.ERROR, error);
  }, this);

  /**
   * @type {ol.Feature}
   * @private
   */
  this.positionFeature_ = new ol.Feature();

  if (options.positionFeatureStyle) {
    this.positionFeature_.setStyle(options.positionFeatureStyle);
  }

  /**
   * @type {ol.Feature}
   * @private
   */
  this.accuracyFeature_ = new ol.Feature();

  if (options.accuracyFeatureStyle) {
    this.accuracyFeature_.setStyle(options.accuracyFeatureStyle);
  }

  /**
   * @type {number|undefined}
   * @private
   */
  this.zoom_ = options.zoom;

  /**
   * Whether to recenter the map at the position it gets updated
   * @type {boolean}
   * @private
   */
  this.follow_ = false;

  /**
   * A flag used to determine whether the view was changed by me or something
   * else. In the latter case, stop following.
   * @type {boolean}
   * @private
   */
  this.viewChangedByMe_ = false;

  ol.events.listen(this.geolocation_, 'change:accuracyGeometry', () => {
    this.accuracyFeature_.setGeometry(this.geolocation_.getAccuracyGeometry());
    this.setPosition_();
  });

  ol.events.listen(this.geolocation_, 'change:position', () => {
    this.setPosition_();
  });

  const view = map.getView();

  ol.events.listen(view, 'change:center', this.handleViewChange_, this);

  ol.events.listen(view, 'change:resolution', this.handleViewChange_, this);

};

/**
 * @export
 */
ngeo.geolocation.mobile.Controller_.prototype.toggleTracking = function() {
  if (this.geolocation_.getTracking()) {
    // if map center is different than geolocation position, then track again
    const currentPosition = this.geolocation_.getPosition();
    // if user is using Firefox and selects the "not now" option, OL geolocation
    // doesn't return an error
    if (currentPosition === undefined) {
      this.untrack_();
      this.$scope_.$emit(ngeo.geolocation.mobile.GeolocationEventType.ERROR, null);
      return;
    }
    goog.asserts.assert(currentPosition !== undefined);
    // stop tracking if the position is close to the center of the map.
    const center = this.map_.getView().getCenter();
    const diff = Math.abs(currentPosition[0] - center[0]) + Math.abs(currentPosition[1] - center[1]);
    if (diff < 2) {
      this.untrack_();
    } else {
      this.untrack_();
      this.track_();
    }
  } else {
    this.track_();
  }
};

/**
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.track_ = function() {
  this.featureOverlay_.addFeature(this.positionFeature_);
  this.featureOverlay_.addFeature(this.accuracyFeature_);
  this.follow_ = true;
  this.geolocation_.setTracking(true);
};

/**
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.untrack_ = function() {
  this.featureOverlay_.clear();
  this.follow_ = false;
  this.geolocation_.setTracking(false);
  this.notification_.clear();
};

/**
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.setPosition_ = function() {
  const position = /** @type {ol.Coordinate} */ (this.geolocation_.getPosition());
  const point = new ol.geom.Point(position);

  this.positionFeature_.setGeometry(point);
  const accuracy = this.accuracyFeature_.getGeometry();

  if (this.follow_) {
    this.viewChangedByMe_ = true;
    if (this.zoom_ !== undefined) {
      this.map_.getView().setCenter(position);
      this.map_.getView().setZoom(this.zoom_);
    } else if (accuracy) {
      const size = /** @type {!ol.Size} */ (this.map_.getSize());
      this.map_.getView().fit(/** @type {!ol.geom.Polygon} */ (accuracy), size);
    }
    this.viewChangedByMe_ = false;
  }
};

/**
 * @param {ol.Object.Event} event Event.
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.handleViewChange_ = function(event) {
  if (this.follow_ && !this.viewChangedByMe_) {
    this.follow_ = false;
  }
};

// Orientation control events
ngeo.geolocation.mobile.Controller_.prototype.autorotateListener = function() {
  let currentAlpha = 0;
  if (window.hasOwnProperty('ondeviceorientationabsolute')) {
    window.addEventListener('deviceorientationabsolute', (evt) => {
      currentAlpha = this.handleRotate_(evt.alpha, currentAlpha);
    }, true);
  } else if (window.hasOwnProperty('ondeviceorientation')) {
    window.addEventListener('deviceorientation', (evt) => {
      if (evt.webkitCompassHeading) { // check for iOS property
        currentAlpha = this.handleRotate_(-evt.webkitCompassHeading, currentAlpha);
      } else { // non iOS
        currentAlpha = this.handleRotate_(evt.alpha - 270, currentAlpha);
      }
    }, true);
  } else {
    console.error('Orientation is not supported on this device');
  }
};

/**
 * Handle rotation.
 * @param {number} eventAlpha .
 * @param {number} currentAlpha .
 * @return {number} .
 * @private
 */
ngeo.geolocation.mobile.Controller_.prototype.handleRotate_ = function(eventAlpha, currentAlpha) {
  if (this.geolocation_.getTracking() && Math.abs(eventAlpha - currentAlpha) > 0.2) {
    currentAlpha = eventAlpha;
    const radAlpha = currentAlpha * Math.PI / 180;
    this.map_.getView().animate({
      rotation: radAlpha,
      duration: 350,
      easing: ol.easing.linear
    });
  }
  return currentAlpha;
};

ngeo.geolocation.mobile.controller('ngeoGeolocationMobileController',
  ngeo.geolocation.mobile.Controller_);
<|endoftext|>


<commit_before>define([
	'goo/math/Vector3',
	'goo/math/Matrix3x3',
	'goo/math/Matrix4x4'
],
/** @lends */
function (
	Vector3,
	Matrix3x3,
	Matrix4x4
) {
	"use strict";

	/**
	 * @class Transform models a transformation in 3d space as: Y = M*X+T, with M being a Matrix3 and T is a Vector3. Generally M will be a rotation
	 *        only matrix in which case it is represented by the matrix and scale fields as R*S, where S is a positive scale vector. For non-uniform
	 *        scales and reflections, use setMatrix, which will consider M as being a general 3x3 matrix and disregard anything set in scale.
	 */
	function Transform() {
		/** Read only, will be updated automatically by {@link Transform.update}
		 * @type {Matrix4x4}
		 */
		this.matrix = new Matrix4x4();
		this.normalMatrix = new Matrix4x4();

		/** @type {Vector3} */
		this.translation = new Vector3();
		/** @type {Matrix3x3} */
		this.rotation = new Matrix3x3();
		/** @type {Vector3} */
		this.scale = new Vector3(1, 1, 1);
	}

	Transform.tmpVec = new Vector3();
	Transform.tmpVec2 = new Vector3();
	Transform.tmpMat1 = new Matrix3x3();

	/**
	 * Combines two transforms into one. This will only work if scaling in the left hand transform is uniform
	 * @param {Transform} lhs left hand side transform
	 * @param {Transform} rhs right hand side transform
	 * @param {Transform} target
	 * @returns {Transform} target
	 */
	Transform.combine = function(lhs, rhs, target) {
		if(lhs.scale.data[0] !== lhs.scale.data[1] || lhs.scale.data[0] !== lhs.scale.data[2]) {
			throw {
				name: 'NonUniformScaleException',
				message: 'Non-uniform scaling in left hand transform, cannot resolve combined transform'
			};
		}
		target = target || new Transform();

		// Translation
		var tmpVec = target.tmpVec;
		tmpVec.setv(rhs.translation);
		// Rotate translation
		lhs.rotation.applyPost(tmpVec);
		// Scale translation
		tmpVec.mulv(lhs.scale);
		// Translate translation
		tmpVec.addv(lhs.translation);

		// Scale
		var tmpVec2 = target.tmpVec2;
		tmpVec2.setv(rhs.scale);
		// Scale scale
		tmpVec2.mulv(lhs.scale);

		// Rotation
		var tmpMat1 = target.tmpMat1;
		// Rotate rotation
		Matrix3x3.combine(lhs.rotation, rhs.rotation, tmpMat1);

		target.rotation.copy(tmpMat1);
		target.scale.setv(tmpVec2);
		target.translation.setv(tmpVec);

		target.update();

		return target;
	};

	/**
	 * Combines new transform into this one. This will only work if scaling in the left hand transform is uniform
	 * @param {Transform} rhs right hand side transform
	 * @returns {Transform} this for chaining
	 */
	Transform.prototype.combine = function(rhs) {
		return Transform.combine(this, rhs, this);
	};

	// TODO: sort this crap out!
	Transform.prototype.multiply = function (a, b) {
		Matrix4x4.combine(a.matrix, b.matrix, this.matrix);

		Transform.tmpMat1.data.set(a.rotation.data);
		//Transform.tmpMat1.multiplyDiagonalPost(a.scale, Transform.tmpMat1);
		this.rotation.data.set(b.rotation.data);
		//this.rotation.multiplyDiagonalPost(b.scale, this.rotation);
		Matrix3x3.combine(Transform.tmpMat1, this.rotation, this.rotation);
		this.translation.setv(b.translation);
		this.translation.mulv(a.scale);
		Transform.tmpMat1.applyPost(this.translation).addv(a.translation);

		Transform.tmpVec.setv(a.scale).mulv(b.scale);
		this.scale.setv(Transform.tmpVec);
	};

	/**
	 * Set Transform to identity
	 */
	Transform.prototype.setIdentity = function () {
		this.matrix.setIdentity();

		this.translation.setv(Vector3.ZERO);
		this.rotation.setIdentity();
		this.scale.setv(Vector3.ONE);
	};

	/**
	 * Applies this transform to supplied vector as a point
	 * @param {Vector3} point
	 * @param {Vector3} store
	 * @returns {Vector3} store
	 * @example
	 * // Vector3 object, one unit right, two units up, two units back
	 * var v1 = new Vector3(1, 2, 2);
	 * // Vector3 to store the local position
	 * var localPos = new Vector3();
	 * // converts v1 to be in 'world space' based on the entities postion / rotation
	 * entity.transformComponent.transform.applyForward(v1, localPos);
	 */
	Transform.prototype.applyForward = function (point, store) {
		store.setv(point);

		// store.set(store.x * this.scale.x, store.y * this.scale.y, store.z * this.scale.z);
		// this.rotation.applyPost(store);
		// store.add(this.translation);

		this.matrix.applyPostPoint(store);

		return store;
	};

	/**
	 * Applies this transform to supplied vector as a direction-vector (translation will not affect it)
	 * @param {Vector3} vector
	 * @param {Vector3} store
	 * @returns {Vector3} store
	 * @example
	 * // Vector3 pointing in the direction we want
	 * var back = new Vector3(0, 0, 1);
	 * // Vector3 to store the local 'back'
	 * var localBack = new Vector3();
	 * // converts 'back' to a localized direction based on the entities rotation
	 * entity.transformComponent.transform.applyForwardVector(back, localBack);
	 */
	Transform.prototype.applyForwardVector = function (vector, store) {
		store.copy(vector);

		store.set(store.x * this.scale.x, store.y * this.scale.y, store.z * this.scale.z);
		this.rotation.applyPost(store);

		return store;
	};

	/**
	 * Updates the transform according to set scaling, rotation and translation. This is done automatically by the engine
	 */
	Transform.prototype.update = function () {
		var target = this.matrix.data;
		var rotation = this.rotation.data;
		var scale = this.scale.data;
		var translation = this.translation.data;

		target[0] = scale[0] * rotation[0];
		target[1] = scale[0] * rotation[1];
		target[2] = scale[0] * rotation[2];
		target[3] = 0.0;
		target[4] = scale[1] * rotation[3];
		target[5] = scale[1] * rotation[4];
		target[6] = scale[1] * rotation[5];
		target[7] = 0.0;
		target[8] = scale[2] * rotation[6];
		target[9] = scale[2] * rotation[7];
		target[10] = scale[2] * rotation[8];
		target[11] = 0.0;
		target[12] = translation[0];
		target[13] = translation[1];
		target[14] = translation[2];
		target[15] = 1.0;
	};

	/**
	 * Copy supplied transform into this transform
	 * @param {Transform} transform
	 */
	Transform.prototype.copy = function (transform) {
		this.matrix.copy(transform.matrix);

		this.translation.setv(transform.translation);
		this.rotation.copy(transform.rotation);
		this.scale.setv(transform.scale);
	};

	/**
	 * Set this transform's rotation to rotation around X, Y and Z axis.
	 * The rotation is applied in XYZ order.
	 * @param {number} x
	 * @param {number} y
	 * @param {number} z
	 */
	Transform.prototype.setRotationXYZ = function (x, y, z) {
		this.rotation.fromAngles(x, y, z);
	};

	/**
	 * Sets the transform to look in a specific direction.
	 * Please note: This function contains a known bug resulting in looking in the opposite direction
	 * for non-camera and non-light entities.
	 * @param {Vector3} position Target position.
	 * @param {Vector3} [up=(0, 1, 0)] Up vector.
	 */
	Transform.prototype.lookAt = function (position, up) {
		if (!up) {
			up = Vector3.UNIT_Y;
		}
		// REVIEW: this is actually using the wrong direction, it should be reversed.
		//   Transform.tmpVec.setv(position).subv(this.translation).normalize();
		// However we might need the old behavior for lights and cameras.
		Transform.tmpVec.setv(this.translation).subv(position).normalize();
		this.rotation.lookAt(Transform.tmpVec, up);
	};

	/**
	 * Invert this transform and store it in supplied transform
	 * @param {Transform} store
	 * @returns {Transform} store
	 */
	Transform.prototype.invert = function (store) {
		var result = store;
		if (!result) {
			result = new Transform();
		}

		// if (_identity) {
		// result.setIdentity();
		// return result;
		// }

		result.matrix.copy(this.matrix);
		result.matrix.invert();

		var newRotation = result.rotation.copy(this.rotation);
		// if (_uniformScale) {
		// var sx = this.scale.x;
		// newRotation.transposeLocal();
		// if (sx !== 1.0) {
		// newRotation.multiplyLocal(1.0 / sx);
		// }
		// } else {
		newRotation.multiplyDiagonalPost(this.scale, newRotation).invert();
		// }

		result.translation.copy(this.translation);
		result.rotation.applyPost(result.translation).invert();

		// result.update();

		return result;
	};

	Transform.prototype.toString = function () {
		return '' + this.matrix;
	};

	return Transform;
});<commit_msg>bugfix in Transform.combine<commit_after>define([
	'goo/math/Vector3',
	'goo/math/Matrix3x3',
	'goo/math/Matrix4x4'
],
/** @lends */
function (
	Vector3,
	Matrix3x3,
	Matrix4x4
) {
	"use strict";

	/**
	 * @class Transform models a transformation in 3d space as: Y = M*X+T, with M being a Matrix3 and T is a Vector3. Generally M will be a rotation
	 *        only matrix in which case it is represented by the matrix and scale fields as R*S, where S is a positive scale vector. For non-uniform
	 *        scales and reflections, use setMatrix, which will consider M as being a general 3x3 matrix and disregard anything set in scale.
	 */
	function Transform() {
		/** Read only, will be updated automatically by {@link Transform.update}
		 * @type {Matrix4x4}
		 */
		this.matrix = new Matrix4x4();
		this.normalMatrix = new Matrix4x4();

		/** @type {Vector3} */
		this.translation = new Vector3();
		/** @type {Matrix3x3} */
		this.rotation = new Matrix3x3();
		/** @type {Vector3} */
		this.scale = new Vector3(1, 1, 1);
	}

	Transform.tmpVec = new Vector3();
	Transform.tmpVec2 = new Vector3();
	Transform.tmpMat1 = new Matrix3x3();

	/**
	 * Combines two transforms into one. This will only work if scaling in the left hand transform is uniform
	 * @param {Transform} lhs left hand side transform
	 * @param {Transform} rhs right hand side transform
	 * @param {Transform} target
	 * @returns {Transform} target
	 */
	Transform.combine = function (lhs, rhs, target) {
		if (lhs.scale.data[0] !== lhs.scale.data[1] || lhs.scale.data[0] !== lhs.scale.data[2]) {
			throw {
				name: 'NonUniformScaleException',
				message: 'Non-uniform scaling in left hand transform, cannot resolve combined transform'
			};
		}
		target = target || new Transform();

		// Translation
		var tmpVec = Transform.tmpVec;
		tmpVec.setv(rhs.translation);
		// Rotate translation
		lhs.rotation.applyPost(tmpVec);
		// Scale translation
		tmpVec.mulv(lhs.scale);
		// Translate translation
		tmpVec.addv(lhs.translation);

		// Scale
		var tmpVec2 = Transform.tmpVec2;
		tmpVec2.setv(rhs.scale);
		// Scale scale
		tmpVec2.mulv(lhs.scale);

		// Rotation
		var tmpMat1 = Transform.tmpMat1;
		// Rotate rotation
		Matrix3x3.combine(lhs.rotation, rhs.rotation, tmpMat1);

		target.rotation.copy(tmpMat1);
		target.scale.setv(tmpVec2);
		target.translation.setv(tmpVec);

		target.update();

		return target;
	};

	/**
	 * Combines new transform into this one. This will only work if scaling in the left hand transform is uniform
	 * @param {Transform} rhs right hand side transform
	 * @returns {Transform} this for chaining
	 */
	Transform.prototype.combine = function (rhs) {
		return Transform.combine(this, rhs, this);
	};

	// TODO: sort this crap out!
	Transform.prototype.multiply = function (a, b) {
		Matrix4x4.combine(a.matrix, b.matrix, this.matrix);

		Transform.tmpMat1.data.set(a.rotation.data);
		//Transform.tmpMat1.multiplyDiagonalPost(a.scale, Transform.tmpMat1);
		this.rotation.data.set(b.rotation.data);
		//this.rotation.multiplyDiagonalPost(b.scale, this.rotation);
		Matrix3x3.combine(Transform.tmpMat1, this.rotation, this.rotation);
		this.translation.setv(b.translation);
		this.translation.mulv(a.scale);
		Transform.tmpMat1.applyPost(this.translation).addv(a.translation);

		Transform.tmpVec.setv(a.scale).mulv(b.scale);
		this.scale.setv(Transform.tmpVec);
	};

	/**
	 * Set Transform to identity
	 */
	Transform.prototype.setIdentity = function () {
		this.matrix.setIdentity();

		this.translation.setv(Vector3.ZERO);
		this.rotation.setIdentity();
		this.scale.setv(Vector3.ONE);
	};

	/**
	 * Applies this transform to supplied vector as a point
	 * @param {Vector3} point
	 * @param {Vector3} store
	 * @returns {Vector3} store
	 * @example
	 * // Vector3 object, one unit right, two units up, two units back
	 * var v1 = new Vector3(1, 2, 2);
	 * // Vector3 to store the local position
	 * var localPos = new Vector3();
	 * // converts v1 to be in 'world space' based on the entities postion / rotation
	 * entity.transformComponent.transform.applyForward(v1, localPos);
	 */
	Transform.prototype.applyForward = function (point, store) {
		store.setv(point);

		// store.set(store.x * this.scale.x, store.y * this.scale.y, store.z * this.scale.z);
		// this.rotation.applyPost(store);
		// store.add(this.translation);

		this.matrix.applyPostPoint(store);

		return store;
	};

	/**
	 * Applies this transform to supplied vector as a direction-vector (translation will not affect it)
	 * @param {Vector3} vector
	 * @param {Vector3} store
	 * @returns {Vector3} store
	 * @example
	 * // Vector3 pointing in the direction we want
	 * var back = new Vector3(0, 0, 1);
	 * // Vector3 to store the local 'back'
	 * var localBack = new Vector3();
	 * // converts 'back' to a localized direction based on the entities rotation
	 * entity.transformComponent.transform.applyForwardVector(back, localBack);
	 */
	Transform.prototype.applyForwardVector = function (vector, store) {
		store.copy(vector);

		store.set(store.x * this.scale.x, store.y * this.scale.y, store.z * this.scale.z);
		this.rotation.applyPost(store);

		return store;
	};

	/**
	 * Updates the transform according to set scaling, rotation and translation. This is done automatically by the engine
	 */
	Transform.prototype.update = function () {
		var target = this.matrix.data;
		var rotation = this.rotation.data;
		var scale = this.scale.data;
		var translation = this.translation.data;

		target[0] = scale[0] * rotation[0];
		target[1] = scale[0] * rotation[1];
		target[2] = scale[0] * rotation[2];
		target[3] = 0.0;
		target[4] = scale[1] * rotation[3];
		target[5] = scale[1] * rotation[4];
		target[6] = scale[1] * rotation[5];
		target[7] = 0.0;
		target[8] = scale[2] * rotation[6];
		target[9] = scale[2] * rotation[7];
		target[10] = scale[2] * rotation[8];
		target[11] = 0.0;
		target[12] = translation[0];
		target[13] = translation[1];
		target[14] = translation[2];
		target[15] = 1.0;
	};

	/**
	 * Copy supplied transform into this transform
	 * @param {Transform} transform
	 */
	Transform.prototype.copy = function (transform) {
		this.matrix.copy(transform.matrix);

		this.translation.setv(transform.translation);
		this.rotation.copy(transform.rotation);
		this.scale.setv(transform.scale);
	};

	/**
	 * Set this transform's rotation to rotation around X, Y and Z axis.
	 * The rotation is applied in XYZ order.
	 * @param {number} x
	 * @param {number} y
	 * @param {number} z
	 */
	Transform.prototype.setRotationXYZ = function (x, y, z) {
		this.rotation.fromAngles(x, y, z);
	};

	/**
	 * Sets the transform to look in a specific direction.
	 * Please note: This function contains a known bug resulting in looking in the opposite direction
	 * for non-camera and non-light entities.
	 * @param {Vector3} position Target position.
	 * @param {Vector3} [up=(0, 1, 0)] Up vector.
	 */
	Transform.prototype.lookAt = function (position, up) {
		if (!up) {
			up = Vector3.UNIT_Y;
		}
		// REVIEW: this is actually using the wrong direction, it should be reversed.
		//   Transform.tmpVec.setv(position).subv(this.translation).normalize();
		// However we might need the old behavior for lights and cameras.
		Transform.tmpVec.setv(this.translation).subv(position).normalize();
		this.rotation.lookAt(Transform.tmpVec, up);
	};

	/**
	 * Invert this transform and store it in supplied transform
	 * @param {Transform} store
	 * @returns {Transform} store
	 */
	Transform.prototype.invert = function (store) {
		var result = store;
		if (!result) {
			result = new Transform();
		}

		// if (_identity) {
		// result.setIdentity();
		// return result;
		// }

		result.matrix.copy(this.matrix);
		result.matrix.invert();

		var newRotation = result.rotation.copy(this.rotation);
		// if (_uniformScale) {
		// var sx = this.scale.x;
		// newRotation.transposeLocal();
		// if (sx !== 1.0) {
		// newRotation.multiplyLocal(1.0 / sx);
		// }
		// } else {
		newRotation.multiplyDiagonalPost(this.scale, newRotation).invert();
		// }

		result.translation.copy(this.translation);
		result.rotation.applyPost(result.translation).invert();

		// result.update();

		return result;
	};

	Transform.prototype.toString = function () {
		return '' + this.matrix;
	};

	return Transform;
});<|endoftext|>


<commit_before>/* global angular */
(function(){
    angular.module('googlechart', [])
        .run(registerResizeEvent);

    registerResizeEvent.$inject = ['$rootScope', '$window'];

    function registerResizeEvent($rootScope, $window){
        angular.element($window).on('resize', function () {
                $rootScope.$emit('resizeMsg');
            });
    }
})();<commit_msg>check window width has actually changed before firing event<commit_after>/* global angular */
(function(){
    angular.module('googlechart', [])
        .run(registerResizeEvent);

    registerResizeEvent.$inject = ['$rootScope', '$window'];

    function registerResizeEvent($rootScope, $window){
        $window.actualSize = $window.width();
        angular.element($window).on('resize', function () {
                if($window.actualSize != $window.width()) {
                    $rootScope.$emit('resizeMsg');
                    $window.actualSize = $window.width();
                }
            });
    }
})();
<|endoftext|>


<commit_before>/**
 * Returns token for use with Google Picker
 */
function getOAuthToken() {
    return ScriptApp.getOAuthToken();
}

/**
 * Returns metadata for input file ID
 *
 * @param {string} id the folder ID for which to return metadata
 * @return {object} the metadata for the folder
 */
function getMetadata(id) {
    return Drive.Files.get(id);
}

/**
 * Returns metadata for input file ID
 *
 * @param {string} id the folder ID for which to return metadata
 * @return {object} the permissions for the folder
 */
function getPermissions(id) {
    return Drive.Permissions.list(id);
}

/**
 * Gets files from query and returns fileList with metadata
 *
 * @param {string} query the query to select files from the Drive
 * @param {string} pageToken the pageToken (if any) for the existing query
 * @return {object} fileList object where fileList.items is an array of children files
 */
function getFiles(query, pageToken) {
    var fileList;

    fileList = Drive.Files.list({
                    q: query,
                    maxResults: 1000,
                    pageToken: pageToken
                });

    return fileList;
}

/**
 *
 * @param {object} ss instance of Sheet class representing the logger spreadsheet
 * @param {Array} values array of values to be written to the spreadsheet
 */
function log(ss, values) {
    if (ss) {
        return ss.getRange(ss.getLastRow()+1, 1, 1, values.length).setValues([values]);
    } else {
        var userProperties = PropertiesService.getUserProperties().getProperties();
        return SpreadsheetApp.openById(userProperties.spreadsheetId).getSheetByName("Log")
            .getRange(ss.getLastRow()+1, 1, 1, values.length).setValues([values]);
    }
}<commit_msg>fix log to handle calls when ss=null<commit_after>/**
 * Returns token for use with Google Picker
 */
function getOAuthToken() {
    return ScriptApp.getOAuthToken();
}

/**
 * Returns metadata for input file ID
 *
 * @param {string} id the folder ID for which to return metadata
 * @return {object} the metadata for the folder
 */
function getMetadata(id) {
    return Drive.Files.get(id);
}

/**
 * Returns metadata for input file ID
 *
 * @param {string} id the folder ID for which to return metadata
 * @return {object} the permissions for the folder
 */
function getPermissions(id) {
    return Drive.Permissions.list(id);
}

/**
 * Gets files from query and returns fileList with metadata
 *
 * @param {string} query the query to select files from the Drive
 * @param {string} pageToken the pageToken (if any) for the existing query
 * @return {object} fileList object where fileList.items is an array of children files
 */
function getFiles(query, pageToken) {
    var fileList;

    fileList = Drive.Files.list({
                    q: query,
                    maxResults: 1000,
                    pageToken: pageToken
                });

    return fileList;
}

/**
 *
 * @param {object} ss instance of Sheet class representing the logger spreadsheet
 * @param {Array} values array of values to be written to the spreadsheet
 */
function log(ss, values) {
    if (ss) {
        return ss.getRange(ss.getLastRow()+1, 1, 1, values.length).setValues([values]);
    } else {
        ss = SpreadsheetApp.openById(PropertiesService.getUserProperties().getProperties().spreadsheetId).getSheetByName("Log");
        return ss.getRange(ss.getLastRow()+1, 1, 1, values.length).setValues([values]);
    }
}<|endoftext|>


<commit_before>
import Wreqr from 'backbone.wreqr';
import Marionette from 'backbone.marionette';
import leafletControlGeocoder from 'leaflet-control-geocoder';
import template from '../../templates/geocodeWidget.ejs';
import templateResultItem from '../../templates/geocodeResultItem.ejs';
import CONST from '../const';

export default Marionette.LayoutView.extend({
    template: template,
    templateResultItem: templateResultItem,

    behaviors: {
        'l20n': {},
        'widget': {},
    },

    ui: {
        'widget': '#geocode_widget',
        'query': 'input',
        'resultList': '.results',
    },

    events: {
        'keyup @ui.query': 'onKeyUpQuery',
        'keydown @ui.query': 'onKeyDownQuery',
    },

    initialize: function () {
        this._radio = Wreqr.radio.channel('global');

        this.on('open', this.onOpen);
    },

    open: function () {
        this._setGeocoder();
        this.triggerMethod('open');
        return this;
    },

    close: function () {
        this.triggerMethod('close');
        return this;
    },

    toggle: function () {
        this._setGeocoder();
        this.triggerMethod('toggle');
    },

    onAfterOpen: function () {
        this._radio.vent.trigger('column:closeAll', [ this.cid ]);

        this.ui.widget.one('transitionend', () => {
            this.ui.query.focus();
        });
    },

    onKeyUpQuery: function (e) {
        if ( this._queryInterval ) {
            clearInterval(this._queryInterval);
        }

        var query = this.ui.query.val();

        if ( this._lastQuery && this._lastQuery === query ) {
            return false;
        }

        this._queryInterval = setTimeout(() => {
            this.geocode( query );
        }, 350);
    },

    onKeyDownQuery: function (e) {
        if ( [9, 13, 38, 40].indexOf(e.keyCode) > -1 ) {
            e.preventDefault();
        }

        switch ( e.keyCode ) {
            case 27:
                this.close();
                break;

            case 40: // Down arrow
            case 9: // Tab
                this.activeNextResult();
                break;

            case 38: // Up arrow
                this.activePreviousResult();
                break;

            case 13: // Enter
                this.visitResult();
                break;
        }
    },

    geocode: function (query) {
        var elements = [];

        this._lastQuery = query;

        if ( !query ) {
            this.ui.resultList.empty();

            return;
        }

        this.options.icon.addClass('hide');
        this.options.spinner.removeClass('hide');

        this._geocoder.geocode(query, (results) => {
            let i = 0;

            for (let result of results) {
                elements.push(
                    $( this.templateResultItem(
                        this._buildGeocodeResultName(result)
                    ))
                    .on('click', this.onGeocodeResultClick.bind(this, result))
                );

                i++;

                if (i === 5) {
                    break;
                }
            }

            this.ui.resultList.html( elements );

            this.options.spinner.addClass('hide');
            this.options.icon.removeClass('hide');
        });

    },

    onGeocodeResultClick: function (result) {
        this._radio.commands.execute('map:fitBounds', result.bbox);

        this.close();
    },

    _buildGeocodeResultName: function (result) {
        let details = [];

        switch ( this.model.get('geocoder') ) {
            case CONST.geocoder.nominatim:
                let splittedResult = result.name.split(', ');

                return {
                    'name': splittedResult.shift(),
                    'detail': splittedResult.join(', ')
                };

            case CONST.geocoder.photon:
                if (result.properties.city) {
                    details.push( result.properties.city );
                }

                if (result.properties.country) {
                    details.push( result.properties.country );
                }

                if (result.properties.state) {
                    details.push( result.properties.state );
                }

                return {
                    'name': result.properties.name,
                    'detail': details.join(', ')
                };
        }
    },

    activeNextResult: function () {
        var current = this.ui.resultList.find('.active');

        if ( !current.length ) {
            this.ui.resultList
            .children()
            .first()
            .addClass('active');
        }
        else {
            current
            .removeClass('active')
            .next()
            .addClass('active');
        }
    },

    activePreviousResult: function () {
        var current = this.ui.resultList.find('.active');

        if ( !current.length ) {
            this.ui.resultList
            .children()
            .last()
            .addClass('active');
        }
        else {
            current
            .removeClass('active')
            .prev()
            .addClass('active');
        }
    },

    visitResult: function () {
        var current = this.ui.resultList.find('.active');

        if ( !current.length ) {
            this.ui.resultList
            .children()
            .first()
            .addClass('active')
            .click();
        }
        else {
            current.click();
        }
    },

    _setGeocoder: function () {
        switch ( this.model.get('geocoder') ) {
            case CONST.geocoder.nominatim:
                this._geocoder = leafletControlGeocoder.nominatim();
                break;
            default:
                this._geocoder = leafletControlGeocoder.photon();
        }
    }
});
<commit_msg>Uses the right name in Photon results<commit_after>
import Wreqr from 'backbone.wreqr';
import Marionette from 'backbone.marionette';
import leafletControlGeocoder from 'leaflet-control-geocoder';
import template from '../../templates/geocodeWidget.ejs';
import templateResultItem from '../../templates/geocodeResultItem.ejs';
import CONST from '../const';

export default Marionette.LayoutView.extend({
    template: template,
    templateResultItem: templateResultItem,

    behaviors: {
        'l20n': {},
        'widget': {},
    },

    ui: {
        'widget': '#geocode_widget',
        'query': 'input',
        'resultList': '.results',
    },

    events: {
        'keyup @ui.query': 'onKeyUpQuery',
        'keydown @ui.query': 'onKeyDownQuery',
    },

    initialize: function () {
        this._radio = Wreqr.radio.channel('global');

        this.on('open', this.onOpen);
    },

    open: function () {
        this._setGeocoder();
        this.triggerMethod('open');
        return this;
    },

    close: function () {
        this.triggerMethod('close');
        return this;
    },

    toggle: function () {
        this._setGeocoder();
        this.triggerMethod('toggle');
    },

    onAfterOpen: function () {
        this._radio.vent.trigger('column:closeAll', [ this.cid ]);

        this.ui.widget.one('transitionend', () => {
            this.ui.query.focus();
        });
    },

    onKeyUpQuery: function (e) {
        if ( this._queryInterval ) {
            clearInterval(this._queryInterval);
        }

        var query = this.ui.query.val();

        if ( this._lastQuery && this._lastQuery === query ) {
            return false;
        }

        this._queryInterval = setTimeout(() => {
            this.geocode( query );
        }, 350);
    },

    onKeyDownQuery: function (e) {
        if ( [9, 13, 38, 40].indexOf(e.keyCode) > -1 ) {
            e.preventDefault();
        }

        switch ( e.keyCode ) {
            case 27:
                this.close();
                break;

            case 40: // Down arrow
            case 9: // Tab
                this.activeNextResult();
                break;

            case 38: // Up arrow
                this.activePreviousResult();
                break;

            case 13: // Enter
                this.visitResult();
                break;
        }
    },

    geocode: function (query) {
        var elements = [];

        this._lastQuery = query;

        if ( !query ) {
            this.ui.resultList.empty();

            return;
        }

        this.options.icon.addClass('hide');
        this.options.spinner.removeClass('hide');

        this._geocoder.geocode(query, (results) => {
            let i = 0;

            for (let result of results) {
                elements.push(
                    $( this.templateResultItem(
                        this._buildGeocodeResultName(result)
                    ))
                    .on('click', this.onGeocodeResultClick.bind(this, result))
                );

                i++;

                if (i === 5) {
                    break;
                }
            }

            this.ui.resultList.html( elements );

            this.options.spinner.addClass('hide');
            this.options.icon.removeClass('hide');
        });

    },

    onGeocodeResultClick: function (result) {
        this._radio.commands.execute('map:fitBounds', result.bbox);

        this.close();
    },

    _buildGeocodeResultName: function (result) {
        let details = [];

        switch ( this.model.get('geocoder') ) {
            case CONST.geocoder.nominatim:
                let splittedResult = result.name.split(', ');

                return {
                    'name': splittedResult.shift(),
                    'detail': splittedResult.join(', ')
                };

            case CONST.geocoder.photon:
                if (result.properties.city) {
                    details.push( result.properties.city );
                }

                if (result.properties.country) {
                    details.push( result.properties.country );
                }

                if (result.properties.state) {
                    details.push( result.properties.state );
                }

                return {
                    'name': result.name,
                    'detail': details.join(', ')
                };
        }
    },

    activeNextResult: function () {
        var current = this.ui.resultList.find('.active');

        if ( !current.length ) {
            this.ui.resultList
            .children()
            .first()
            .addClass('active');
        }
        else {
            current
            .removeClass('active')
            .next()
            .addClass('active');
        }
    },

    activePreviousResult: function () {
        var current = this.ui.resultList.find('.active');

        if ( !current.length ) {
            this.ui.resultList
            .children()
            .last()
            .addClass('active');
        }
        else {
            current
            .removeClass('active')
            .prev()
            .addClass('active');
        }
    },

    visitResult: function () {
        var current = this.ui.resultList.find('.active');

        if ( !current.length ) {
            this.ui.resultList
            .children()
            .first()
            .addClass('active')
            .click();
        }
        else {
            current.click();
        }
    },

    _setGeocoder: function () {
        switch ( this.model.get('geocoder') ) {
            case CONST.geocoder.nominatim:
                this._geocoder = leafletControlGeocoder.nominatim();
                break;
            default:
                this._geocoder = leafletControlGeocoder.photon();
        }
    }
});
<|endoftext|>


<commit_before>var PanelWin3js	= PanelWin3js	|| {}

//////////////////////////////////////////////////////////////////////////////////
//		Comments
//////////////////////////////////////////////////////////////////////////////////

/**
 * Handle panel for object3d
 *
 * @constructor
 */
PanelWin3js.PanelTreeView	= function(){

	var container	= UI.CollapsiblePanelHelper.createContainer('BROWSER', 'leftSidebarSceneBrowser', false)

	container.dom.addEventListener('click', function(){
		treeView.clearActive()
		PanelWin3js.plainFunction(function(){
			console.log('in panel-ui-treeview.js: unselecting item')
			InspectedWin3js.selectUuid(null)
		})
		console.log('click on empty panel', arguments)
	})

	//////////////////////////////////////////////////////////////////////////////////
	//		popupMenu
	//////////////////////////////////////////////////////////////////////////////////

	var popupMenu	= UI.PopupMenuHelper.createSelect({
		''			: '--- Options ---',
		'collapseAll'		: 'Collapse All',
		'expandAll'		: 'Expand All',
		'captureScene'		: 'Capture Scene',
	}, onPopupMenuChange)
	container.titleElement.add(popupMenu)
	container.dom.appendChild( document.createElement('br') )

	function onPopupMenuChange(value){
		if( value === 'collapseAll' ){
			treeView.getRoot().children.forEach(function(child){
				child.collapseAll()
			})
		}else if( value === 'expandAll' ){
			treeView.getRoot().children.forEach(function(child){
				child.expandAll()
			})
		}else if( value === 'captureScene' ){
		        console.log('in panel-ui-treeview.js: detecting click on a button capture_scene')
		        PanelWin3js.injectInspectedWinScripts()

			PanelWin3js.plainFunction(function(uuid){
		                InspectedWin3js.captureScene(scene)
			})
		}else{
			console.assert(false)
		}
	}

	//////////////////////////////////////////////////////////////////////////////////
	//		Comments
	//////////////////////////////////////////////////////////////////////////////////

	container.content.appendChild( document.createElement('br') )

	// create TreeView
	var treeView = new TreeView( container.content );
	treeView.onSelect = function( object3dUuid ){
		PanelWin3js.plainFunction(function(uuid){
			console.log('trying to select object3d uuid', uuid)
			InspectedWin3js.selectUuid(uuid)
		}, [object3dUuid])
	}
	treeView.onToggleVisibility = function(object3dUuid){
		PanelWin3js.plainFunction(function(uuid){
			console.log('in panel-ui-treeview.js: toggle visibility in uuid', uuid)
			var object3d = InspectedWin3js.getObjectByUuid(uuid)
			object3d.visible = object3d.visible === true ? false : true
		}, [object3dUuid])
	}
	treeView.onExport = function(object3dUuid){
		PanelWin3js.plainFunction(function(uuid){
			var object3d = InspectedWin3js.getObjectByUuid(uuid)
			window.$object3d = object3d
			console.log('in panel-ui-treeview.js: Object3D exported as $object3d')
			console.dir($object3d)
		}, [object3dUuid])
	}

	//////////////////////////////////////////////////////////////////////////////////
	//		process updateObject3DTreeView
	//////////////////////////////////////////////////////////////////////////////////
	var treeViewObjects     = {}

	PanelWin3js.editor.signals.updateObject3DTreeView.add(function(dataJSON){
		// create treeViewObjects[] object if needed
		if( treeViewObjects[ dataJSON.uuid ] === undefined ){
		        console.log('in panel-ui-treeview.js: create a treeviewItem')
			// create the dom element
			treeViewObjects[ dataJSON.uuid ] = {
				uuid : dataJSON.uuid,
				parentUuid : dataJSON.parentUuid,
				data : {
					className : dataJSON.className,
					viewItem : new TreeViewItem( dataJSON.name, dataJSON.uuid )
				}
			}
		}

		var treeViewObject = treeViewObjects[ dataJSON.uuid ]
		console.assert( treeViewObject !== undefined )

		if( dataJSON.parentUuid ) {
		        console.log('in panel-ui-treeview.js: appendChild treeviewItem to parent', dataJSON.parentUuid)
			// add current object to the proper parent
			treeViewObjects[ dataJSON.parentUuid ].data.viewItem.appendChild( treeViewObject.data.viewItem );
			treeViewObject.parent = dataJSON.parentUuid;
		} else {
		        console.log('in panel-ui-treeview.js: appendChild treeviewItem to root')
			// if this object got no parent, add it at the root
			// if( !object.data.viewItem.parentItem ){
				treeView.getRoot().appendChild( treeViewObject.data.viewItem );
			// }
		}

	})

	return container
};
<commit_msg>better label for treeviewitem<commit_after>var PanelWin3js	= PanelWin3js	|| {}

//////////////////////////////////////////////////////////////////////////////////
//		Comments
//////////////////////////////////////////////////////////////////////////////////

/**
 * Handle panel for object3d
 *
 * @constructor
 */
PanelWin3js.PanelTreeView	= function(){

	var container	= UI.CollapsiblePanelHelper.createContainer('BROWSER', 'leftSidebarSceneBrowser', false)

	container.dom.addEventListener('click', function(){
		treeView.clearActive()
		PanelWin3js.plainFunction(function(){
			console.log('in panel-ui-treeview.js: unselecting item')
			InspectedWin3js.selectUuid(null)
		})
		console.log('click on empty panel', arguments)
	})

	//////////////////////////////////////////////////////////////////////////////////
	//		popupMenu
	//////////////////////////////////////////////////////////////////////////////////

	var popupMenu	= UI.PopupMenuHelper.createSelect({
		''			: '--- Options ---',
		'collapseAll'		: 'Collapse All',
		'expandAll'		: 'Expand All',
		'captureScene'		: 'Capture Scene',
	}, onPopupMenuChange)
	container.titleElement.add(popupMenu)
	container.dom.appendChild( document.createElement('br') )

	function onPopupMenuChange(value){
		if( value === 'collapseAll' ){
			treeView.getRoot().children.forEach(function(child){
				child.collapseAll()
			})
		}else if( value === 'expandAll' ){
			treeView.getRoot().children.forEach(function(child){
				child.expandAll()
			})
		}else if( value === 'captureScene' ){
		        console.log('in panel-ui-treeview.js: detecting click on a button capture_scene')
		        PanelWin3js.injectInspectedWinScripts()

			PanelWin3js.plainFunction(function(uuid){
		                InspectedWin3js.captureScene(scene)
			})
		}else{
			console.assert(false)
		}
	}

	//////////////////////////////////////////////////////////////////////////////////
	//		Comments
	//////////////////////////////////////////////////////////////////////////////////

	container.content.appendChild( document.createElement('br') )

	// create TreeView
	var treeView = new TreeView( container.content );
	treeView.onSelect = function( object3dUuid ){
		PanelWin3js.plainFunction(function(uuid){
			console.log('trying to select object3d uuid', uuid)
			InspectedWin3js.selectUuid(uuid)
		}, [object3dUuid])
	}
	treeView.onToggleVisibility = function(object3dUuid){
		PanelWin3js.plainFunction(function(uuid){
			console.log('in panel-ui-treeview.js: toggle visibility in uuid', uuid)
			var object3d = InspectedWin3js.getObjectByUuid(uuid)
			object3d.visible = object3d.visible === true ? false : true
		}, [object3dUuid])
	}
	treeView.onExport = function(object3dUuid){
		PanelWin3js.plainFunction(function(uuid){
			var object3d = InspectedWin3js.getObjectByUuid(uuid)
			window.$object3d = object3d
			console.log('in panel-ui-treeview.js: Object3D exported as $object3d')
			console.dir($object3d)
		}, [object3dUuid])
	}

	//////////////////////////////////////////////////////////////////////////////////
	//		process updateObject3DTreeView
	//////////////////////////////////////////////////////////////////////////////////
	var treeViewObjects     = {}

	PanelWin3js.editor.signals.updateObject3DTreeView.add(function(dataJSON){
		// create treeViewObjects[] object if needed
		if( treeViewObjects[ dataJSON.uuid ] === undefined ){
		        console.log('in panel-ui-treeview.js: create a treeviewItem')
			var label = (dataJSON.name ? dataJSON.name : 'no name') + ' - ' + dataJSON.className
			// create the dom element
			treeViewObjects[ dataJSON.uuid ] = {
				uuid : dataJSON.uuid,
				parentUuid : dataJSON.parentUuid,
				data : {
					className : dataJSON.className,
					viewItem : new TreeViewItem( label, dataJSON.uuid )
				}
			}
		}

		var treeViewObject = treeViewObjects[ dataJSON.uuid ]
		console.assert( treeViewObject !== undefined )

		if( dataJSON.parentUuid ) {
		        console.log('in panel-ui-treeview.js: appendChild treeviewItem to parent', dataJSON.parentUuid)
			// add current object to the proper parent
			treeViewObjects[ dataJSON.parentUuid ].data.viewItem.appendChild( treeViewObject.data.viewItem );
			treeViewObject.parent = dataJSON.parentUuid;
		} else {
		        console.log('in panel-ui-treeview.js: appendChild treeviewItem to root')
			// if this object got no parent, add it at the root
			// if( !object.data.viewItem.parentItem ){
				treeView.getRoot().appendChild( treeViewObject.data.viewItem );
			// }
		}

	})

	return container
};
<|endoftext|>


<commit_before>/* eslint-disable */
/**
 * @module ol/interaction/Select
 * @ignore
 */
import {getUid} from 'ol/util.js';
import CollectionEventType from 'ol/CollectionEventType.js';
import {extend, includes} from 'ol/array.js';
import Event from 'ol/events/Event.js';
import {singleClick, never, shiftKeyOnly, pointerMove} from 'ol/events/condition.js';
import {TRUE} from 'ol/functions.js';
import GeometryType from 'ol/geom/GeometryType.js';
import Interaction from 'ol/interaction/Interaction.js';
import VectorLayer from 'ol/layer/Vector.js';
import {clear} from 'ol/obj.js';
import VectorSource from 'ol/source/Vector.js';
import {createEditingStyle} from 'ol/style/Style.js';

/**
 * @enum {string}
 * @ignore
 */
const SelectEventType = {
  /**
   * Triggered when feature(s) has been (de)selected.
   * @event SelectEvent#select
   * @api
   * @ignore
   */
  SELECT: 'select'
};

/**
 * A function that takes an {ol/Feature} or
 * {ol/render/Feature} and an
 * {Layer} and returns `true` if the feature may be
 * selected or `false` otherwise.
 */

/**
 * @property {Condition} [addCondition] A function
 * that takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled.
 * By default, this is {ol/events/condition~never}. Use this if you
 * want to use different events for add and remove instead of `toggle`.
 * @property {Function} [condition] A function that
 * takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled. This is the event
 * for the selected features as a whole. By default, this is
 * {ol/events/condition~singleClick}. Clicking on a feature selects that
 * feature and removes any that were in the selection. Clicking outside any
 * feature removes all from the selection.
 * See `toggle`, `add`, `remove` options for adding/removing extra features to/
 * from the selection.
 * @property {Array<Layer>} [layers]
 * A list of layers from which features should be selected. Alternatively, a
 * filter function can be provided. The function will be called for each layer
 * in the map and should return `true` for layers that you want to be
 * selectable. If the option is absent, all visible layers will be considered
 * selectable.
 * @property {StyleLike} [style]
 * Style for the selected features. By default the default edit style is used
 * (see {ol/style}).
 * @property {Condition} [removeCondition] A function
 * that takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled.
 * By default, this is {ol/events/condition~never}. Use this if you
 * want to use different events for add and remove instead of `toggle`.
 * @property {Condition} [toggleCondition] A function
 * that takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled. This is in addition
 * to the `condition` event. By default,
 * {ol/events/condition~shiftKeyOnly}, i.e. pressing `shift` as
 * well as the `condition` event, adds that feature to the current selection if
 * it is not currently selected, and removes it if it is. See `add` and `remove`
 * if you want to use different events instead of a toggle.
 * @property {boolean} [multi=false] A boolean that determines if the default
 * behaviour should select only single features or all (overlapping) features at
 * the clicked map position. The default of `false` means single select.
 * @property {Collection<Feature>} [features]
 * Collection where the interaction will place selected features. Optional. If
 * not set the interaction will create a collection. In any case the collection
 * used by the interaction is returned by
 * {ol/interaction/Select~Select#getFeatures}.
 * @property {FilterFunction} [filter] A function
 * that takes an {ol/Feature} and an
 * {Layer} and returns `true` if the feature may be
 * selected or `false` otherwise.
 * @property {boolean} [wrapX=true] Wrap the world horizontally on the selection
 * overlay.
 * @property {number} [hitTolerance=0] Hit-detection tolerance. Pixels inside
 * the radius around the given position will be checked for features.
 */

/**
 * @classdesc
 * Events emitted by {ol/interaction/Select~Select} instances are instances of
 * this type.
 * @class
 * @ignore
 */
class SelectEvent extends Event {
  /**
   * @param {SelectEventType} type The event type.
   * @param {Array<Feature>} selected Selected features.
   * @param {Array<Feature>} deselected Deselected features.
   * @param {MapBrowserEvent} mapBrowserEvent Associated
   *     {ol/MapBrowserEvent}.
   * @ignore
   */
  constructor(type, selected, deselected, mapBrowserEvent) {
    super(type);

    /**
     * Selected features array.
     * @type {Array<Feature>}
     * @api
     */
    this.selected = selected;

    /**
     * Deselected features array.
     * @type {Array<Feature>}
     * @api
     */
    this.deselected = deselected;

    /**
     * Associated {ol/MapBrowserEvent}.
     * @type {MapBrowserEvent}
     * @api
     */
    this.mapBrowserEvent = mapBrowserEvent;

  }

}

/**
 * @classdesc
 * Interaction for selecting vector features. By default, selected features are
 * styled differently, so this interaction can be used for visual highlighting,
 * as well as selecting features for other actions, such as modification or
 * output. There are three ways of controlling which features are selected:
 * using the browser event as defined by the `condition` and optionally the
 * `toggle`, `add`/`remove`, and `multi` options; a `layers` filter; and a
 * further feature filter using the `filter` option.
 *
 * Selected features are added to an internal unmanaged layer.
 *
 * @fires SelectEvent
 * @api
 * @ignore
 */
class Select extends Interaction {
  /**
   * @param {Options=} opt_options Options.
   * @ignore
   */
  constructor(opt_options) {

    super({
      handleEvent: handleEvent
    });

    const options = opt_options ? opt_options : {};

    /**
     * @private
     * @type {Condition}
     */
    this.condition_ = options.condition ? options.condition : singleClick;

    /**
     * @private
     * @type {Condition}
     */
    this.addCondition_ = options.addCondition ? options.addCondition : never;

    /**
     * @private
     * @type {Condition}
     */
    this.removeCondition_ = options.removeCondition ? options.removeCondition : never;

    /**
     * @private
     * @type {Condition}
     */
    this.toggleCondition_ = options.toggleCondition ? options.toggleCondition : shiftKeyOnly;

    /**
     * @private
     * @type {boolean}
     */
    this.multi_ = options.multi ? options.multi : false;

    /**
     * @private
     * @type {FilterFunction}
     */
    this.filter_ = options.filter ? options.filter : TRUE;

    /**
     * @private
     * @type {number}
     */
    this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0;

    const featureOverlay = new VectorLayer({
      source: new VectorSource({
        useSpatialIndex: false,
        features: options.features,
        wrapX: options.wrapX
      }),
      style: options.style ? options.style :
        getDefaultStyleFunction(),
      updateWhileAnimating: true,
      updateWhileInteracting: true
    });

    /**
     * @private
     * @type {VectorLayer}
     */
    this.featureOverlay_ = featureOverlay;

    /** @type {ol.layer.Layer} */
    let layerFilter;
    if (options.layers) {
      if (typeof options.layers === 'function') {
        layerFilter = options.layers;
      } else {
        const layers = options.layers;
        layerFilter = function(layer) {
          return includes(layers, layer);
        };
      }
    } else {
      layerFilter = TRUE;
    }

    /**
     * @private
     * @type {ol.layer.Layer}
     */
    this.layerFilter_ = layerFilter;

    /**
     * An association between selected feature (key)
     * and layer (value)
     * @private
     * @type {Layer}
     */
    this.featureLayerAssociation_ = {};
  }

  /**
   * @param {FeatureLike} feature Feature.
   * @param {Layer} layer Layer.
   * @private
   */
  addFeatureLayerAssociation_(feature, layer) {
    this.featureLayerAssociation_[getUid(feature)] = layer;
  }

  /**
   * Get the selected features.
   * @return {Feature} Features collection.
   * @api
   */
  getFeatures() {
    return this.featureOverlay_.getSource().getFeaturesCollection();
  }

  /**
   * Returns the Hit-detection tolerance.
   * @returns {number} Hit tolerance in pixels.
   * @api
   */
  getHitTolerance() {
    return this.hitTolerance_;
  }

  /**
   * Returns the associated {ol/layer/Vector~Vector vectorlayer} of
   * the (last) selected feature. Note that this will not work with any
   * programmatic method like pushing features to
   * {ol/interaction/Select~Select#getFeatures collection}.
   * @param {FeatureLike} feature Feature
   * @return {VectorLayer} Layer.
   * @api
   */
  getLayer(feature) {
    return (
      /** @type {VectorLayer} */ (this.featureLayerAssociation_[getUid(feature)])
    );
  }

  /**
   * Get the overlay layer that this interaction renders selected features to.
   * @return {VectorLayer} Overlay layer.
   * @api
   */
  getOverlay() {
    return this.featureOverlay_;
  }

  /**
   * Hit-detection tolerance. Pixels inside the radius around the given position
   * will be checked for features.
   * @param {number} hitTolerance Hit tolerance in pixels.
   * @api
   */
  setHitTolerance(hitTolerance) {
    this.hitTolerance_ = hitTolerance;
  }

  /**
   * Remove the interaction from its current map, if any,  and attach it to a new
   * map, if any. Pass `null` to just remove the interaction from the current map.
   * @param {PluggableMap} map Map.
   * @override
   * @api
   */
  setMap(map) {
    super.setMap(map);
    this.featureOverlay_.setMap(map);
  }

  /**
   * @param {FeatureLike} feature Feature.
   * @private
   */
  removeFeatureLayerAssociation_(feature) {
    delete this.featureLayerAssociation_[getUid(feature)];
  }
}

/**
 * Handles the {ol/MapBrowserEvent map browser event} and may change the
 * selected state of features.
 * @param {MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {Select}
 */
function handleEvent(mapBrowserEvent) {
  if (!this.condition_(mapBrowserEvent)) {
    return true;
  }
  const add = this.addCondition_(mapBrowserEvent);
  const remove = this.removeCondition_(mapBrowserEvent);
  const toggle = this.toggleCondition_(mapBrowserEvent);
  const set = !add && !remove && !toggle;
  const map = mapBrowserEvent.map;
  const features = this.getFeatures();
  const deselected = [];
  const selected = [];
  if (set) {
    // Replace the currently selected feature(s) with the feature(s) at the
    // pixel, or clear the selected feature(s) if there is no feature at
    // the pixel.
    clear(this.featureLayerAssociation_);
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
      (
        /**
         * @param {FeatureLike} feature Feature.
         * @param {Layer} layer Layer.
         * @return {boolean|undefined} Continue to iterate over the features.
         */
        function(feature, layer) {
          /* If feature layer is null, omit the selection.
           * Prevents mistakenly selecting features from the overlay.
           */
          if (this.filter_(feature, layer) && layer) {
            selected.push(feature);
            this.addFeatureLayerAssociation_(feature, layer);
            return !this.multi_;
          }
        }).bind(this), {
        layerFilter: this.layerFilter_,
        hitTolerance: this.hitTolerance_
      });
    for (let i = features.getLength() - 1; i >= 0; --i) {
      const feature = features.item(i);
      const index = selected.indexOf(feature);
      if (index > -1) {
        // feature is already selected
        selected.splice(index, 1);
      } else {
        features.remove(feature);
        deselected.push(feature);
      }
    }
    if (selected.length !== 0) {
      features.extend(selected);
    }
  } else {
    // Modify the currently selected feature(s).
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
      (
        /**
         * @param {FeatureLike} feature Feature.
         * @param {Layer} layer Layer.
         * @return {boolean|undefined} Continue to iterate over the features.
         */
        function(feature, layer) {
          if (layer && this.filter_(feature, layer)) {
            if ((add || toggle) && !includes(features.getArray(), feature)) {
              selected.push(feature);
              this.addFeatureLayerAssociation_(feature, layer);
            } else if ((remove || toggle) && includes(features.getArray(), feature)) {
              deselected.push(feature);
              this.removeFeatureLayerAssociation_(feature);
            }
            return !this.multi_;
          }
        }).bind(this), {
        layerFilter: this.layerFilter_,
        hitTolerance: this.hitTolerance_
      });
    for (let j = deselected.length - 1; j >= 0; --j) {
      features.remove(deselected[j]);
    }
    features.extend(selected);
  }
  if (selected.length > 0 || deselected.length > 0) {
    this.dispatchEvent(
      new SelectEvent(SelectEventType.SELECT,
        selected, deselected, mapBrowserEvent));
  }
  return pointerMove(mapBrowserEvent);
}

/**
 * @return {StyleFunction} Styles.
 */
function getDefaultStyleFunction() {
  const styles = createEditingStyle();
  extend(styles[GeometryType.POLYGON], styles[GeometryType.LINE_STRING]);
  extend(styles[GeometryType.GEOMETRY_COLLECTION], styles[GeometryType.LINE_STRING]);

  return function(feature, resolution) {
    if (!feature.getGeometry()) {
      return null;
    }
    return styles[feature.getGeometry().getType()];
  };
}

export default Select;
<commit_msg>Update src/interaction/select.js<commit_after>/* eslint-disable */
/**
 * @module ol/interaction/Select
 * @ignore
 */
import {getUid} from 'ol/util.js';
import CollectionEventType from 'ol/CollectionEventType.js';
import {extend, includes} from 'ol/array.js';
import Event from 'ol/events/Event.js';
import {singleClick, never, shiftKeyOnly, pointerMove} from 'ol/events/condition.js';
import {TRUE} from 'ol/functions.js';
import GeometryType from 'ol/geom/GeometryType.js';
import Interaction from 'ol/interaction/Interaction.js';
import VectorLayer from 'ol/layer/Vector.js';
import {clear} from 'ol/obj.js';
import VectorSource from 'ol/source/Vector.js';
import {createEditingStyle} from 'ol/style/Style.js';

/**
 * @enum {string}
 * @ignore
 */
const SelectEventType = {
  /**
   * Triggered when feature(s) has been (de)selected.
   * @event SelectEvent#select
   * @api
   * @ignore
   */
  SELECT: 'select'
};

/**
 * A function that takes an {ol/Feature} or
 * {ol/render/Feature} and an
 * {Layer} and returns `true` if the feature may be
 * selected or `false` otherwise.
 */

/**
 * @property {Condition} [addCondition] A function
 * that takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled.
 * By default, this is {ol/events/condition~never}. Use this if you
 * want to use different events for add and remove instead of `toggle`.
 * @property {Function} [condition] A function that
 * takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled. This is the event
 * for the selected features as a whole. By default, this is
 * {ol/events/condition~singleClick}. Clicking on a feature selects that
 * feature and removes any that were in the selection. Clicking outside any
 * feature removes all from the selection.
 * See `toggle`, `add`, `remove` options for adding/removing extra features to/
 * from the selection.
 * @property {Array<Layer>} [layers]
 * A list of layers from which features should be selected. Alternatively, a
 * filter function can be provided. The function will be called for each layer
 * in the map and should return `true` for layers that you want to be
 * selectable. If the option is absent, all visible layers will be considered
 * selectable.
 * @property {StyleLike} [style]
 * Style for the selected features. By default the default edit style is used
 * (see {ol/style}).
 * @property {Condition} [removeCondition] A function
 * that takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled.
 * By default, this is {ol/events/condition~never}. Use this if you
 * want to use different events for add and remove instead of `toggle`.
 * @property {Condition} [toggleCondition] A function
 * that takes an {MapBrowserEvent} and returns a
 * boolean to indicate whether that event should be handled. This is in addition
 * to the `condition` event. By default,
 * {ol/events/condition~shiftKeyOnly}, i.e. pressing `shift` as
 * well as the `condition` event, adds that feature to the current selection if
 * it is not currently selected, and removes it if it is. See `add` and `remove`
 * if you want to use different events instead of a toggle.
 * @property {boolean} [multi=false] A boolean that determines if the default
 * behaviour should select only single features or all (overlapping) features at
 * the clicked map position. The default of `false` means single select.
 * @property {Collection<Feature>} [features]
 * Collection where the interaction will place selected features. Optional. If
 * not set the interaction will create a collection. In any case the collection
 * used by the interaction is returned by
 * {ol/interaction/Select~Select#getFeatures}.
 * @property {FilterFunction} [filter] A function
 * that takes an {ol/Feature} and an
 * {Layer} and returns `true` if the feature may be
 * selected or `false` otherwise.
 * @property {boolean} [wrapX=true] Wrap the world horizontally on the selection
 * overlay.
 * @property {number} [hitTolerance=0] Hit-detection tolerance. Pixels inside
 * the radius around the given position will be checked for features.
 */

/**
 * @classdesc
 * Events emitted by {ol/interaction/Select~Select} instances are instances of
 * this type.
 * @class
 * @ignore
 */
class SelectEvent extends Event {
  /**
   * @param {SelectEventType} type The event type.
   * @param {Array<Feature>} selected Selected features.
   * @param {Array<Feature>} deselected Deselected features.
   * @param {MapBrowserEvent} mapBrowserEvent Associated
   *     {ol/MapBrowserEvent}.
   * @ignore
   */
  constructor(type, selected, deselected, mapBrowserEvent) {
    super(type);

    /**
     * Selected features array.
     * @type {Array<Feature>}
     * @api
     */
    this.selected = selected;

    /**
     * Deselected features array.
     * @type {Array<Feature>}
     * @api
     */
    this.deselected = deselected;

    /**
     * Associated {ol/MapBrowserEvent}.
     * @type {MapBrowserEvent}
     * @api
     */
    this.mapBrowserEvent = mapBrowserEvent;

  }

}

/**
 * @classdesc
 * Interaction for selecting vector features. By default, selected features are
 * styled differently, so this interaction can be used for visual highlighting,
 * as well as selecting features for other actions, such as modification or
 * output. There are three ways of controlling which features are selected:
 * using the browser event as defined by the `condition` and optionally the
 * `toggle`, `add`/`remove`, and `multi` options; a `layers` filter; and a
 * further feature filter using the `filter` option.
 *
 * Selected features are added to an internal unmanaged layer.
 *
 * @fires SelectEvent
 * @api
 * @ignore
 */
class Select extends Interaction {
  /**
   * @param {Options=} opt_options Options.
   * @ignore
   */
  constructor(opt_options) {

    super({
      handleEvent: handleEvent
    });

    const options = opt_options ? opt_options : {};

    /**
     * @private
     * @type {Condition}
     */
    this.condition_ = options.condition ? options.condition : singleClick;

    /**
     * @private
     * @type {Condition}
     */
    this.addCondition_ = options.addCondition ? options.addCondition : never;

    /**
     * @private
     * @type {Condition}
     */
    this.removeCondition_ = options.removeCondition ? options.removeCondition : never;

    /**
     * @private
     * @type {Condition}
     */
    this.toggleCondition_ = options.toggleCondition ? options.toggleCondition : shiftKeyOnly;

    /**
     * @private
     * @type {boolean}
     */
    this.multi_ = options.multi ? options.multi : false;

    /**
     * @private
     * @type {FilterFunction}
     */
    this.filter_ = options.filter ? options.filter : TRUE;

    /**
     * @private
     * @type {number}
     */
    this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0;

    const featureOverlay = new VectorLayer({
      source: new VectorSource({
        useSpatialIndex: false,
        features: options.features,
        wrapX: options.wrapX
      }),
      style: options.style ? options.style :
        getDefaultStyleFunction(),
      updateWhileAnimating: true,
      updateWhileInteracting: true
    });

    /**
     * @private
     * @type {VectorLayer}
     */
    this.featureOverlay_ = featureOverlay;

    /** @type {ol.layer.Layer} */
    let layerFilter;
    if (options.layers) {
      if (typeof options.layers === 'function') {
        layerFilter = options.layers;
      } else {
        const layers = options.layers;
        layerFilter = function(layer) {
          return includes(layers, layer);
        };
      }
    } else {
      layerFilter = TRUE;
    }

    /**
     * @private
     * @type {ol.layer.Layer}
     */
    this.layerFilter_ = layerFilter;

    /**
     * An association between selected feature (key)
     * and layer (value)
     * @private
     * @type {Layer}
     */
    this.featureLayerAssociation_ = {};
  }

  /**
   * @param {FeatureLike} feature Feature.
   * @param {Layer} layer Layer.
   * @private
   */
  addFeatureLayerAssociation_(feature, layer) {
    this.featureLayerAssociation_[getUid(feature)] = layer;
  }

  /**
   * Get the selected features.
   * @return {Feature} Features collection.
   * @api
   */
  getFeatures() {
    return this.featureOverlay_.getSource().getFeaturesCollection();
  }

  /**
   * Returns the Hit-detection tolerance.
   * @returns {number} Hit tolerance in pixels.
   * @api
   */
  getHitTolerance() {
    return this.hitTolerance_;
  }

  /**
   * Returns the associated {ol/layer/Vector~Vector vectorlayer} of
   * the (last) selected feature. Note that this will not work with any
   * programmatic method like pushing features to
   * {ol/interaction/Select~Select#getFeatures collection}.
   * @param {FeatureLike} feature Feature
   * @return {VectorLayer} Layer.
   * @api
   */
  getLayer(feature) {
    return (
      /** @type {VectorLayer} */ (this.featureLayerAssociation_[getUid(feature)])
    );
  }

  /**
   * Get the overlay layer that this interaction renders selected features to.
   * @return {VectorLayer} Overlay layer.
   * @api
   */
  getOverlay() {
    return this.featureOverlay_;
  }

  /**
   * Hit-detection tolerance. Pixels inside the radius around the given position
   * will be checked for features.
   * @param {number} hitTolerance Hit tolerance in pixels.
   * @api
   */
  setHitTolerance(hitTolerance) {
    this.hitTolerance_ = hitTolerance;
  }

  /**
   * Remove the interaction from its current map, if any,  and attach it to a new
   * map, if any. Pass `null` to just remove the interaction from the current map.
   * @param {PluggableMap} map Map.
   * @override
   * @api
   */
  setMap(map) {
    super.setMap(map);
    this.featureOverlay_.setMap(map);
  }

  /**
   * @param {FeatureLike} feature Feature.
   * @private
   */
  removeFeatureLayerAssociation_(feature) {
    delete this.featureLayerAssociation_[getUid(feature)];
  }
}

/**
 * Handles the {ol/MapBrowserEvent map browser event} and may change the
 * selected state of features.
 * @param {MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {Select}
 */
function handleEvent(mapBrowserEvent) {
  if (!this.condition_(mapBrowserEvent)) {
    return true;
  }
  const add = this.addCondition_(mapBrowserEvent);
  const remove = this.removeCondition_(mapBrowserEvent);
  const toggle = this.toggleCondition_(mapBrowserEvent);
  const set = !add && !remove && !toggle;
  const map = mapBrowserEvent.map;
  const features = this.getFeatures();
  const deselected = [];
  const selected = [];
  if (set) {
    // Replace the currently selected feature(s) with the feature(s) at the
    // pixel, or clear the selected feature(s) if there is no feature at
    // the pixel.
    clear(this.featureLayerAssociation_);
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
      (
        /**
         * @param {FeatureLike} feature Feature.
         * @param {Layer} layer Layer.
         * @return {boolean|undefined} Continue to iterate over the features.
         */
        function(feature, layer) {
          /* If feature layer is null, omit the selection.
           * Prevents mistakenly selecting features from the overlay.
           */
          if (layer && this.filter_(feature, layer)) {
            selected.push(feature);
            this.addFeatureLayerAssociation_(feature, layer);
            return !this.multi_;
          }
        }).bind(this), {
        layerFilter: this.layerFilter_,
        hitTolerance: this.hitTolerance_
      });
    for (let i = features.getLength() - 1; i >= 0; --i) {
      const feature = features.item(i);
      const index = selected.indexOf(feature);
      if (index > -1) {
        // feature is already selected
        selected.splice(index, 1);
      } else {
        features.remove(feature);
        deselected.push(feature);
      }
    }
    if (selected.length !== 0) {
      features.extend(selected);
    }
  } else {
    // Modify the currently selected feature(s).
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
      (
        /**
         * @param {FeatureLike} feature Feature.
         * @param {Layer} layer Layer.
         * @return {boolean|undefined} Continue to iterate over the features.
         */
        function(feature, layer) {
          if (layer && this.filter_(feature, layer)) {
            if ((add || toggle) && !includes(features.getArray(), feature)) {
              selected.push(feature);
              this.addFeatureLayerAssociation_(feature, layer);
            } else if ((remove || toggle) && includes(features.getArray(), feature)) {
              deselected.push(feature);
              this.removeFeatureLayerAssociation_(feature);
            }
            return !this.multi_;
          }
        }).bind(this), {
        layerFilter: this.layerFilter_,
        hitTolerance: this.hitTolerance_
      });
    for (let j = deselected.length - 1; j >= 0; --j) {
      features.remove(deselected[j]);
    }
    features.extend(selected);
  }
  if (selected.length > 0 || deselected.length > 0) {
    this.dispatchEvent(
      new SelectEvent(SelectEventType.SELECT,
        selected, deselected, mapBrowserEvent));
  }
  return pointerMove(mapBrowserEvent);
}

/**
 * @return {StyleFunction} Styles.
 */
function getDefaultStyleFunction() {
  const styles = createEditingStyle();
  extend(styles[GeometryType.POLYGON], styles[GeometryType.LINE_STRING]);
  extend(styles[GeometryType.GEOMETRY_COLLECTION], styles[GeometryType.LINE_STRING]);

  return function(feature, resolution) {
    if (!feature.getGeometry()) {
      return null;
    }
    return styles[feature.getGeometry().getType()];
  };
}

export default Select;
<|endoftext|>