<commit_before>/*
    Copyright 2013-2015 appPlant UG

    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

exports = require('de.appplant.cordova.plugin.local-notification.LocalNotification.Proxy').core;

var channel = require('cordova/channel');

/***********
 * MEMBERS *
 ***********/

// True if App is running, false if suspended
exports.isInBackground = true;

// Indicates if the device is ready (to receive events)
exports.isReady = false;

// Queues all events before deviceready
exports.eventQueue = [];

/********
 * UTIL *
 ********/

/**
 * The repeating interval in milliseconds.
 *
 * @param {String} interval
 *      A number or a placeholder like `minute`.
 *
 * @return {Number}
 *      Interval in milliseconds
 */
exports.getRepeatInterval = function (every) {

    if (!every)
        return 0;

    if (every == 'minute')
        return 60000;

    if (every == 'hour')
        return 360000;

    if (!NaN(every))
        return parseInt(every) * 60000;

    return 0;
};

/**
 * If the notification is repeating.
 *
 * @param {Object} notification
 *      Local notification object
 *
 * @return Boolean
 */
exports.isRepeating = function (notification) {
    return this.getRepeatInterval(notification.every) !== 0;
};

/**
 * Parses sound file path.
 *
 * @param {String} path
 *      Relative path to sound resource
 *
 * @return {String} XML Tag for Sound-File
 */
exports.parseSound = function (path) {
	if (!path.match(/^file/))
		return '';

	var uri = this.parseUri(path),
		audio = "<audio src=" + uri + " loop='false'/>";

	return audio;
};

/**
 * Parses image file path.
 *
 * @param {String} path
 *      Relative path to image resource
 *
 * @return {String} XML-Tag for Image-File
 */
exports.parseImage = function (path) {
    if (!path.match(/^file/))
        return '';

    var uri = this.parseUri(path),
        image = "<image id='1' src=" + uri + " />";

    return image;
};

/**
 * Parses file path to URI.
 *
 * @param {String} path
 *      Relative path to a resource
 *
 * @return {String} URI to File
 */
exports.parseUri = function (path) {
    var pkg = Windows.ApplicationModel.Package.current,
        pkgId = pkg.id,
        pkgName = pkgId.name;

	var uri = "'ms-appx://" + pkgName + "/www" + path.slice(6, path.length) + "'";

	return uri;
};

/**
 * Builds the xml payload for a local notification based on its options.
 *
 * @param {Object} options
 *      Local notification properties
 *
 * @return Windows.Data.Xml.Dom.XmlDocument
 */
exports.build = function (options) {
    var template = this.buildToastTemplate(options),
        notification = new Windows.Data.Xml.Dom.XmlDocument();

    try {
        notification.loadXml(template);
    } catch (e) {
        console.error(
            'LocalNotification#schedule',
            'Error loading the xml, check for invalid characters.');
    }

    // Launch Attribute to enable onClick event
    var launchAttr = notification.createAttribute('launch'),
        toastNode = notification.selectSingleNode('/toast');

    launchAttr.value = options.id.toString();
    toastNode.attributes.setNamedItem(launchAttr);

    return notification;
};

/**
 * Builds the toast template with the right style depend on the options.
 *
 * @param {Object} options
 *      Local notification properties
 *
 * @return String
 */
exports.buildToastTemplate = function (options) {
	var title = options.title,
		message = options.text || '',
		json = JSON.stringify(options),
		sound = '';

	if (options.sound && options.sound !== '') {
		sound = this.parseSound(options.sound);
	}

	var templateName = "ToastText",
		imageNode;
	if (options.icon && options.icon !== '') {
		imageNode = this.parseImage(options.icon);
		// template with Image
		if (imageNode !== '') {
			templateName = "ToastImageAndText";
		}
	} else {
		imageNode = "";
	}

	var bindingNode;
	if (title && title !== '') {
		bindingNode = "<binding template='" + templateName + "02'>" +
							imageNode +
							"<text id='1'>" + title + "</text>" +
							"<text id='2'>" + message + "</text>" +
						"</binding>";
	} else {
		bindingNode = "<binding template='" + templateName + "01'>" +
							imageNode +
							"<text id='1'>" + message + "</text>" +
						"</binding>";
	}
	return "<toast>" +
				"<visual>" +
						bindingNode +
				"</visual>" +
				sound +
				"<json>" + json + "</json>" +
			"</toast>";
};

/**
 * Short-hand method for the toast notification history.
 */
exports.getToastHistory = function () {
    return Windows.UI.Notifications.ToastNotificationManager.history;
};

/**
 * Gets a toast notifier instance.
 *
 * @return Object
 */
exports.getToastNotifier = function () {
    return Windows.UI.Notifications.ToastNotificationManager
            .createToastNotifier();
};

/**
 * List of all scheduled toast notifiers.
 *
 * @return Array
 */
exports.getScheduledToasts = function () {
    return this.getToastNotifier().getScheduledToastNotifications();
};

/**
 * Gets the Id from the toast notifier.
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return String
 */
exports.getToastId = function (toast) {
    var id = toast.id;

    if (id.match(/-2$/))
        return id.match(/^[^-]+/)[0];

    return id;
};

/**
 * Gets the notification life cycle type
 * (scheduled or triggered)
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return String
 */
exports.getToastType = function (toast) {
    return this.isToastTriggered(toast) ? 'triggered' : 'scheduled';
};

/**
 * If the toast is already scheduled.
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return Boolean
 */
exports.isToastScheduled = function (toast) {
    return !this.isToastTriggered(toast);
};

/**
 * If the toast is already triggered.
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return Boolean
 */
exports.isToastTriggered = function (toast) {
    var id = this.getToastId(toast),
        notification = this.getAll(id)[0];
        fireDate = new Date((notification.at) * 1000);

    if (this.isRepeating(notification))
        return false;

    return fireDate <= new Date();
};

/**
 * Finds the toast by it's ID.
 *
 * @param {String} id
 *      Local notification ID
 *
 * @param Object
 */
exports.findToastById = function (id) {
    var toasts = this.getScheduledToasts();

    for (var i = 0; i < toasts.length; i++) {
        var toast = toasts[i];

        if (this.getToastId(toast) == id)
            return toast;
    }

    return null;
};

/**
 * Sets trigger event for local notification.
 *
 * @param {Object} notification
 *      Local notification object
 * @param {Function} callback
 *      Callback function
 */
exports.callOnTrigger = function (notification, callback) {
    var triggerTime = new Date((notification.at * 1000)),
        interval = triggerTime - new Date();

    if (interval <= 0) {
        callback.call(this, notification);
        return;
    }

    WinJS.Promise.timeout(interval).then(function () {
        if (exports.isPresent(notification.id)) {
            callback.call(exports, notification);
        }
    });
};

/**
 * Sets trigger event for all scheduled local notification.
 *
 * @param {Function} callback
 *      Callback function
 */
exports.callOnTriggerForScheduled = function (callback) {
    var notifications = this.getScheduled();

    for (var i = 0; i < notifications.length; i++) {
        this.callOnTrigger(notifications[i], callback);
    }
};

/**
 * The application state - background or foreground.
 *
 * @return String
 */
exports.getApplicationState = function () {
    return this.isInBackground ? 'background' : 'foreground';
};

/**
 * Fires the event about a local notification.
 *
 * @param {String} event
 *      The event
 * @param {Object} notification
 *      The notification
 */
exports.fireEvent = function (event, notification) {
    var plugin = cordova.plugins.notification.local.core,
        state = this.getApplicationState(),
        args;

    if (notification) {
        args = [event, notification, state];
    } else {
        args = [event, state];
    }

    if (this.isReady && plugin) {
        plugin.fireEvent.apply(plugin, args);
    } else {
        this.eventQueue.push(args);
    }
};

/**************
 * LIFE CYCLE *
 **************/

// Called before 'deviceready' event
channel.onCordovaReady.subscribe(function () {
    // Register trigger handler for each scheduled notification
    exports.callOnTriggerForScheduled(function (notification) {
        this.updateBadge(notification.badge);
        this.fireEvent('trigger', notification);
    });
});

// Handle onclick event
WinJS.Application.addEventListener('activated', function (args) {
    var id = args.detail.arguments,
        notification = exports.getAll([id])[0];

    if (!notification)
        return;

    exports.clearLocalNotification(id);

    var repeating = exports.isRepeating(notification);

    exports.fireEvent('click', notification);
    exports.fireEvent(repeating ? 'clear' : 'cancel', notification);
}, false);

// App is running in background
document.addEventListener('pause', function () {
    exports.isInBackground = true;
}, false);

// App is running in foreground
document.addEventListener('resume', function () {
    exports.isInBackground = false;
}, false);

// App is running in foreground
document.addEventListener('deviceready', function () {
    exports.isInBackground = false;
}, false);
<commit_msg>Fix crash when clicking on notification (WP8.1)<commit_after>/*
    Copyright 2013-2015 appPlant UG

    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

exports = require('de.appplant.cordova.plugin.local-notification.LocalNotification.Proxy').core;

var channel = require('cordova/channel');

/***********
 * MEMBERS *
 ***********/

// True if App is running, false if suspended
exports.isInBackground = true;

// Indicates if the device is ready (to receive events)
exports.isReady = false;

// Queues all events before deviceready
exports.eventQueue = [];

/********
 * UTIL *
 ********/

/**
 * The repeating interval in milliseconds.
 *
 * @param {String} interval
 *      A number or a placeholder like `minute`.
 *
 * @return {Number}
 *      Interval in milliseconds
 */
exports.getRepeatInterval = function (every) {

    if (!every)
        return 0;

    if (every == 'minute')
        return 60000;

    if (every == 'hour')
        return 360000;

    if (!NaN(every))
        return parseInt(every) * 60000;

    return 0;
};

/**
 * If the notification is repeating.
 *
 * @param {Object} notification
 *      Local notification object
 *
 * @return Boolean
 */
exports.isRepeating = function (notification) {
    return this.getRepeatInterval(notification.every) !== 0;
};

/**
 * Parses sound file path.
 *
 * @param {String} path
 *      Relative path to sound resource
 *
 * @return {String} XML Tag for Sound-File
 */
exports.parseSound = function (path) {
	if (!path.match(/^file/))
		return '';

	var uri = this.parseUri(path),
		audio = "<audio src=" + uri + " loop='false'/>";

	return audio;
};

/**
 * Parses image file path.
 *
 * @param {String} path
 *      Relative path to image resource
 *
 * @return {String} XML-Tag for Image-File
 */
exports.parseImage = function (path) {
    if (!path.match(/^file/))
        return '';

    var uri = this.parseUri(path),
        image = "<image id='1' src=" + uri + " />";

    return image;
};

/**
 * Parses file path to URI.
 *
 * @param {String} path
 *      Relative path to a resource
 *
 * @return {String} URI to File
 */
exports.parseUri = function (path) {
    var pkg = Windows.ApplicationModel.Package.current,
        pkgId = pkg.id,
        pkgName = pkgId.name;

	var uri = "'ms-appx://" + pkgName + "/www" + path.slice(6, path.length) + "'";

	return uri;
};

/**
 * Builds the xml payload for a local notification based on its options.
 *
 * @param {Object} options
 *      Local notification properties
 *
 * @return Windows.Data.Xml.Dom.XmlDocument
 */
exports.build = function (options) {
    var template = this.buildToastTemplate(options),
        notification = new Windows.Data.Xml.Dom.XmlDocument();

    try {
        notification.loadXml(template);
    } catch (e) {
        console.error(
            'LocalNotification#schedule',
            'Error loading the xml, check for invalid characters.');
    }

    // Launch Attribute to enable onClick event
    var launchAttr = notification.createAttribute('launch'),
        toastNode = notification.selectSingleNode('/toast');

    launchAttr.value = options.id.toString();
    toastNode.attributes.setNamedItem(launchAttr);

    return notification;
};

/**
 * Builds the toast template with the right style depend on the options.
 *
 * @param {Object} options
 *      Local notification properties
 *
 * @return String
 */
exports.buildToastTemplate = function (options) {
	var title = options.title,
		message = options.text || '',
		json = JSON.stringify(options),
		sound = '';

	if (options.sound && options.sound !== '') {
		sound = this.parseSound(options.sound);
	}

	var templateName = "ToastText",
		imageNode;
	if (options.icon && options.icon !== '') {
		imageNode = this.parseImage(options.icon);
		// template with Image
		if (imageNode !== '') {
			templateName = "ToastImageAndText";
		}
	} else {
		imageNode = "";
	}

	var bindingNode;
	if (title && title !== '') {
		bindingNode = "<binding template='" + templateName + "02'>" +
							imageNode +
							"<text id='1'>" + title + "</text>" +
							"<text id='2'>" + message + "</text>" +
						"</binding>";
	} else {
		bindingNode = "<binding template='" + templateName + "01'>" +
							imageNode +
							"<text id='1'>" + message + "</text>" +
						"</binding>";
	}
	return "<toast>" +
				"<visual>" +
						bindingNode +
				"</visual>" +
				sound +
				"<json>" + json + "</json>" +
			"</toast>";
};

/**
 * Short-hand method for the toast notification history.
 */
exports.getToastHistory = function () {
    return Windows.UI.Notifications.ToastNotificationManager.history;
};

/**
 * Gets a toast notifier instance.
 *
 * @return Object
 */
exports.getToastNotifier = function () {
    return Windows.UI.Notifications.ToastNotificationManager
            .createToastNotifier();
};

/**
 * List of all scheduled toast notifiers.
 *
 * @return Array
 */
exports.getScheduledToasts = function () {
    return this.getToastNotifier().getScheduledToastNotifications();
};

/**
 * Gets the Id from the toast notifier.
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return String
 */
exports.getToastId = function (toast) {
    var id = toast.id;

    if (id.match(/-2$/))
        return id.match(/^[^-]+/)[0];

    return id;
};

/**
 * Gets the notification life cycle type
 * (scheduled or triggered)
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return String
 */
exports.getToastType = function (toast) {
    return this.isToastTriggered(toast) ? 'triggered' : 'scheduled';
};

/**
 * If the toast is already scheduled.
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return Boolean
 */
exports.isToastScheduled = function (toast) {
    return !this.isToastTriggered(toast);
};

/**
 * If the toast is already triggered.
 *
 * @param {Object} toast
 *      A toast notifier object
 *
 * @return Boolean
 */
exports.isToastTriggered = function (toast) {
    var id = this.getToastId(toast),
        notification = this.getAll([id])[0],
        fireDate = new Date((notification.at) * 1000);

    if (this.isRepeating(notification))
        return false;

    return fireDate <= new Date();
};

/**
 * Finds the toast by it's ID.
 *
 * @param {String} id
 *      Local notification ID
 *
 * @param Object
 */
exports.findToastById = function (id) {
    var toasts = this.getScheduledToasts();

    for (var i = 0; i < toasts.length; i++) {
        var toast = toasts[i];

        if (this.getToastId(toast) == id)
            return toast;
    }

    return null;
};

/**
 * Sets trigger event for local notification.
 *
 * @param {Object} notification
 *      Local notification object
 * @param {Function} callback
 *      Callback function
 */
exports.callOnTrigger = function (notification, callback) {
    var triggerTime = new Date((notification.at * 1000)),
        interval = triggerTime - new Date();

    if (interval <= 0) {
        callback.call(this, notification);
        return;
    }

    WinJS.Promise.timeout(interval).then(function () {
        if (exports.isPresent(notification.id)) {
            callback.call(exports, notification);
        }
    });
};

/**
 * Sets trigger event for all scheduled local notification.
 *
 * @param {Function} callback
 *      Callback function
 */
exports.callOnTriggerForScheduled = function (callback) {
    var notifications = this.getScheduled();

    for (var i = 0; i < notifications.length; i++) {
        this.callOnTrigger(notifications[i], callback);
    }
};

/**
 * The application state - background or foreground.
 *
 * @return String
 */
exports.getApplicationState = function () {
    return this.isInBackground ? 'background' : 'foreground';
};

/**
 * Fires the event about a local notification.
 *
 * @param {String} event
 *      The event
 * @param {Object} notification
 *      The notification
 */
exports.fireEvent = function (event, notification) {
    var plugin = cordova.plugins.notification.local.core,
        state = this.getApplicationState(),
        args;

    if (notification) {
        args = [event, notification, state];
    } else {
        args = [event, state];
    }

    if (this.isReady && plugin) {
        plugin.fireEvent.apply(plugin, args);
    } else {
        this.eventQueue.push(args);
    }
};

/**************
 * LIFE CYCLE *
 **************/

// Called before 'deviceready' event
channel.onCordovaReady.subscribe(function () {
    // Register trigger handler for each scheduled notification
    exports.callOnTriggerForScheduled(function (notification) {
        this.updateBadge(notification.badge);
        this.fireEvent('trigger', notification);
    });
});

// Handle onclick event
WinJS.Application.addEventListener('activated', function (args) {
    var id = args.detail.arguments,
        notification = exports.getAll([id])[0];

    if (!notification)
        return;

    exports.clearLocalNotification(id);

    var repeating = exports.isRepeating(notification);

    exports.fireEvent('click', notification);
    exports.fireEvent(repeating ? 'clear' : 'cancel', notification);
}, false);

// App is running in background
document.addEventListener('pause', function () {
    exports.isInBackground = true;
}, false);

// App is running in foreground
document.addEventListener('resume', function () {
    exports.isInBackground = false;
}, false);

// App is running in foreground
document.addEventListener('deviceready', function () {
    exports.isInBackground = false;
}, false);
<|endoftext|>


<commit_before>/**
 * @private
 */
IonicModule

.controller('$ionicScroll', [
  '$scope',
  'scrollViewOptions',
  '$timeout',
  '$window',
  '$location',
  '$document',
  '$ionicScrollDelegate',
  '$ionicHistory',
function($scope, scrollViewOptions, $timeout, $window, $location, $document, $ionicScrollDelegate, $ionicHistory) {

  var self = this;
  // for testing
  self.__timeout = $timeout;

  self._scrollViewOptions = scrollViewOptions; //for testing

  var element = self.element = scrollViewOptions.el;
  var $element = self.$element = jqLite(element);
  var scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);

  //Attach self to element as a controller so other directives can require this controller
  //through `require: '$ionicScroll'
  //Also attach to parent so that sibling elements can require this
  ($element.parent().length ? $element.parent() : $element)
    .data('$$ionicScrollController', self);

  var deregisterInstance = $ionicScrollDelegate._registerInstance(
    self, scrollViewOptions.delegateHandle, function() {
      if ($scope.$$disconnected) {
        return false;
      }

      var currentHistoryId = $ionicHistory.currentHistoryId();
      if (currentHistoryId) {
        return currentHistoryId == (isDefined($scope.$historyId) ? $scope.$historyId : 'root');
      }

      return true;
    }
  );

  if (!angular.isDefined(scrollViewOptions.bouncing)) {
    ionic.Platform.ready(function() {
      scrollView.options.bouncing = true;

      if (ionic.Platform.isAndroid()) {
        // No bouncing by default on Android
        scrollView.options.bouncing = false;
        // Faster scroll decel
        scrollView.options.deceleration = 0.95;
      }
    });
  }

  var resize = angular.bind(scrollView, scrollView.resize);
  ionic.on('resize', resize, $window);

  var scrollFunc = function(e) {
    var detail = (e.originalEvent || e).detail || {};
    $scope.$onScroll && $scope.$onScroll({
      event: e,
      scrollTop: detail.scrollTop || 0,
      scrollLeft: detail.scrollLeft || 0
    });
  };

  $element.on('scroll', scrollFunc);

  $scope.$on('$destroy', function() {
    deregisterInstance();
    scrollView.__cleanup();
    ionic.off('resize', resize, $window);
    $window.removeEventListener('resize', resize);
    scrollViewOptions = null;
    self._scrollViewOptions.el = null;
    self._scrollViewOptions = null;
    $element.off('scroll', scrollFunc);
    $element = null;
    self.$element = null;
    element = null;
    self.element = null;
    self.scrollView = null;
    scrollView = null;
  });

  $timeout(function() {
    scrollView && scrollView.run && scrollView.run();
  });

  self.getScrollView = function() {
    return self.scrollView;
  };

  self.getScrollPosition = function() {
    return self.scrollView.getValues();
  };

  self.resize = function() {
    return $timeout(resize).then(function() {
      $element && $element.triggerHandler('scroll.resize');
    });
  };

  self.scrollTop = function(shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.scrollTo(0, 0, !!shouldAnimate);
    });
  };

  self.scrollBottom = function(shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      var max = scrollView.getScrollMax();
      scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
    });
  };

  self.scrollTo = function(left, top, shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.scrollTo(left, top, !!shouldAnimate);
    });
  };

  self.zoomTo = function(zoom, shouldAnimate, originLeft, originTop) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
    });
  };

  self.zoomBy = function(zoom, shouldAnimate, originLeft, originTop) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
    });
  };

  self.scrollBy = function(left, top, shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.scrollBy(left, top, !!shouldAnimate);
    });
  };

  self.anchorScroll = function(shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      var hash = $location.hash();
      var elm = hash && $document[0].getElementById(hash);
      if (!(hash && elm)) {
        scrollView.scrollTo(0,0, !!shouldAnimate);
        return;
      }
      var curElm = elm;
      var scrollLeft = 0, scrollTop = 0, levelsClimbed = 0;
      do {
        if (curElm !== null) scrollLeft += curElm.offsetLeft;
        if (curElm !== null) scrollTop += curElm.offsetTop;
        curElm = curElm.offsetParent;
        levelsClimbed++;
      } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
      scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
    });
  };

  /**
   * @private
   */
  self._setRefresher = function(refresherScope, refresherElement) {
    var refresher = self.refresher = refresherElement;
    var refresherHeight = self.refresher.clientHeight || 60;
    scrollView.activatePullToRefresh(refresherHeight, function() {
      // activateCallback
      refresher.classList.add('active');
      refresherScope.$onPulling();
    }, function() {
      // deactivateCallback
        refresher.classList.remove('active', 'refreshing', 'refreshing-tail');
    }, function() {
      // startCallback
      refresher.classList.add('refreshing');
      refresherScope.$onRefresh();
    }, function() {
      // showCallback
      refresher.classList.remove('invisible');
    }, function() {
      // hideCallback
      refresher.classList.add('invisible');
    }, function() {
      // tailCallback
      refresher.classList.add('refreshing-tail');
    });
  };
}]);
<commit_msg>fix(scroll): prevent 'cannot read' exception<commit_after>/**
 * @private
 */
IonicModule

.controller('$ionicScroll', [
  '$scope',
  'scrollViewOptions',
  '$timeout',
  '$window',
  '$location',
  '$document',
  '$ionicScrollDelegate',
  '$ionicHistory',
function($scope, scrollViewOptions, $timeout, $window, $location, $document, $ionicScrollDelegate, $ionicHistory) {

  var self = this;
  // for testing
  self.__timeout = $timeout;

  self._scrollViewOptions = scrollViewOptions; //for testing

  var element = self.element = scrollViewOptions.el;
  var $element = self.$element = jqLite(element);
  var scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);

  //Attach self to element as a controller so other directives can require this controller
  //through `require: '$ionicScroll'
  //Also attach to parent so that sibling elements can require this
  ($element.parent().length ? $element.parent() : $element)
    .data('$$ionicScrollController', self);

  var deregisterInstance = $ionicScrollDelegate._registerInstance(
    self, scrollViewOptions.delegateHandle, function() {
      if ($scope.$$disconnected) {
        return false;
      }

      var currentHistoryId = $ionicHistory.currentHistoryId();
      if (currentHistoryId) {
        return currentHistoryId == (isDefined($scope.$historyId) ? $scope.$historyId : 'root');
      }

      return true;
    }
  );

  if (!angular.isDefined(scrollViewOptions.bouncing)) {
    ionic.Platform.ready(function() {
      if (scrollView.options) {
        scrollView.options.bouncing = true;
        if (ionic.Platform.isAndroid()) {
          // No bouncing by default on Android
          scrollView.options.bouncing = false;
          // Faster scroll decel
          scrollView.options.deceleration = 0.95;
        }
      }
    });
  }

  var resize = angular.bind(scrollView, scrollView.resize);
  ionic.on('resize', resize, $window);

  var scrollFunc = function(e) {
    var detail = (e.originalEvent || e).detail || {};
    $scope.$onScroll && $scope.$onScroll({
      event: e,
      scrollTop: detail.scrollTop || 0,
      scrollLeft: detail.scrollLeft || 0
    });
  };

  $element.on('scroll', scrollFunc);

  $scope.$on('$destroy', function() {
    deregisterInstance();
    scrollView.__cleanup();
    ionic.off('resize', resize, $window);
    $window.removeEventListener('resize', resize);
    scrollViewOptions = null;
    self._scrollViewOptions.el = null;
    self._scrollViewOptions = null;
    $element.off('scroll', scrollFunc);
    $element = null;
    self.$element = null;
    element = null;
    self.element = null;
    self.scrollView = null;
    scrollView = null;
  });

  $timeout(function() {
    scrollView && scrollView.run && scrollView.run();
  });

  self.getScrollView = function() {
    return self.scrollView;
  };

  self.getScrollPosition = function() {
    return self.scrollView.getValues();
  };

  self.resize = function() {
    return $timeout(resize).then(function() {
      $element && $element.triggerHandler('scroll.resize');
    });
  };

  self.scrollTop = function(shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.scrollTo(0, 0, !!shouldAnimate);
    });
  };

  self.scrollBottom = function(shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      var max = scrollView.getScrollMax();
      scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
    });
  };

  self.scrollTo = function(left, top, shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.scrollTo(left, top, !!shouldAnimate);
    });
  };

  self.zoomTo = function(zoom, shouldAnimate, originLeft, originTop) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
    });
  };

  self.zoomBy = function(zoom, shouldAnimate, originLeft, originTop) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
    });
  };

  self.scrollBy = function(left, top, shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      scrollView.scrollBy(left, top, !!shouldAnimate);
    });
  };

  self.anchorScroll = function(shouldAnimate) {
    ionic.DomUtil.blurAll();
    self.resize().then(function() {
      var hash = $location.hash();
      var elm = hash && $document[0].getElementById(hash);
      if (!(hash && elm)) {
        scrollView.scrollTo(0,0, !!shouldAnimate);
        return;
      }
      var curElm = elm;
      var scrollLeft = 0, scrollTop = 0, levelsClimbed = 0;
      do {
        if (curElm !== null) scrollLeft += curElm.offsetLeft;
        if (curElm !== null) scrollTop += curElm.offsetTop;
        curElm = curElm.offsetParent;
        levelsClimbed++;
      } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
      scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
    });
  };

  /**
   * @private
   */
  self._setRefresher = function(refresherScope, refresherElement) {
    var refresher = self.refresher = refresherElement;
    var refresherHeight = self.refresher.clientHeight || 60;
    scrollView.activatePullToRefresh(refresherHeight, function() {
      // activateCallback
      refresher.classList.add('active');
      refresherScope.$onPulling();
    }, function() {
      // deactivateCallback
        refresher.classList.remove('active', 'refreshing', 'refreshing-tail');
    }, function() {
      // startCallback
      refresher.classList.add('refreshing');
      refresherScope.$onRefresh();
    }, function() {
      // showCallback
      refresher.classList.remove('invisible');
    }, function() {
      // hideCallback
      refresher.classList.add('invisible');
    }, function() {
      // tailCallback
      refresher.classList.add('refreshing-tail');
    });
  };
}]);
<|endoftext|>


<commit_before>import Component from 'flarum/Component';
import DiscussionListItem from 'flarum/components/DiscussionListItem';
import Button from 'flarum/components/Button';
import LoadingIndicator from 'flarum/components/LoadingIndicator';

/**
 * The `DiscussionList` component displays a list of discussions.
 *
 * ### Props
 *
 * - `params` A map of parameters used to construct a refined parameter object
 *   to send along in the API request to get discussion results.
 */
export default class DiscussionList extends Component {
  constructor(...args) {
    super(...args);

    /**
     * Whether or not discussion results are loading.
     *
     * @type {Boolean}
     */
    this.loading = true;

    /**
     * Whether or not there are more results that can be loaded.
     *
     * @type {Boolean}
     */
    this.moreResults = false;

    /**
     * The discussions in the discussion list.
     *
     * @type {Discussion[]}
     */
    this.discussions = [];

    this.refresh();

    app.session.on('loggedIn', this.loggedInHandler = this.refresh.bind(this));
  }

  onunload() {
    app.session.off('loggedIn', this.loggedInHandler);
  }

  view() {
    const params = this.props.params;
    let loading;

    if (this.loading) {
      loading = LoadingIndicator.component();
    } else if (this.moreResults) {
      loading = Button.component({
        children: app.trans('core.load_more'),
        className: 'Button',
        onclick: this.loadMore.bind(this)
      });
    }

    return (

          {this.discussions.map(discussion => {
            return (

                {DiscussionListItem.component({discussion, params})}

            );
          })}

          {loading}

    );
  }

  /**
   * Get the parameters that should be passed in the API request to get
   * discussion results.
   *
   * @return {Object}
   * @api
   */
  requestParams() {
    const params = {include: ['startUser', 'lastUser'], filter: {}};

    params.sort = this.sortMap()[this.props.params.sort];

    if (this.props.params.q) {
      params.filter.q = this.props.params.q;

      params.include.push('relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user');
    }

    return params;
  }

  /**
   * Get a map of sort keys (which appear in the URL, and are used for
   * translation) to the API sort value that they represent.
   *
   * @return {Object}
   */
  sortMap() {
    const map = {};

    if (this.props.params.q) {
      map.relevance = '';
    }
    map.latest = '-lastTime';
    map.top = '-commentsCount';
    map.newest = '-startTime';
    map.oldest = '+startTime';

    return map;
  }

  /**
   * Clear and reload the discussion list.
   *
   * @public
   */
  refresh(clear = true) {
    if (clear) {
      this.loading = true;
      this.discussions = [];
    }

    return this.loadResults().then(
      results => {
        this.discussions = [];
        this.parseResults(results);
      },
      () => {
        this.loading = false;
        m.redraw();
      }
    );
  }

  /**
   * Load a new page of discussion results.
   *
   * @param {Integer} offset The index to start the page at.
   * @return {Promise}
   */
  loadResults(offset) {
    const preloadedDiscussions = app.preloadedDocument();

    if (preloadedDiscussions) {
      return m.deferred().resolve(preloadedDiscussions).promise;
    }

    const params = this.requestParams();
    params.page = {offset};
    params.include = params.include.join(',');

    return app.store.find('discussions', params);
  }

  /**
   * Load the next page of discussion results.
   *
   * @public
   */
  loadMore() {
    this.loading = true;

    this.loadResults(this.discussions.length)
      .then(this.parseResults.bind(this));
  }

  /**
   * Parse results and append them to the discussion list.
   *
   * @param {Discussion[]} results
   * @return {Discussion[]}
   */
  parseResults(results) {
    [].push.apply(this.discussions, results);

    this.loading = false;
    this.moreResults = !!results.payload.links.next;

    // Since this may be called during the component's constructor, i.e. in the
    // middle of a redraw, forcing another redraw would not bode well. Instead
    // we start/end a computation so Mithril will only redraw if it isn't
    // already doing so.
    m.startComputation();
    m.endComputation();

    return results;
  }

  /**
   * Remove a discussion from the list if it is present.
   *
   * @param {Discussion} discussion
   * @public
   */
  removeDiscussion(discussion) {
    const index = this.discussions.indexOf(discussion);

    if (index !== -1) {
      this.discussions.splice(index, 1);
    }
  }

  /**
   * Add a discussion to the top of the list.
   *
   * @param {Discussion} discussion
   * @public
   */
  addDiscussion(discussion) {
    this.discussions.unshift(discussion);
  }
}
<commit_msg>Remove session event handler<commit_after>import Component from 'flarum/Component';
import DiscussionListItem from 'flarum/components/DiscussionListItem';
import Button from 'flarum/components/Button';
import LoadingIndicator from 'flarum/components/LoadingIndicator';

/**
 * The `DiscussionList` component displays a list of discussions.
 *
 * ### Props
 *
 * - `params` A map of parameters used to construct a refined parameter object
 *   to send along in the API request to get discussion results.
 */
export default class DiscussionList extends Component {
  constructor(...args) {
    super(...args);

    /**
     * Whether or not discussion results are loading.
     *
     * @type {Boolean}
     */
    this.loading = true;

    /**
     * Whether or not there are more results that can be loaded.
     *
     * @type {Boolean}
     */
    this.moreResults = false;

    /**
     * The discussions in the discussion list.
     *
     * @type {Discussion[]}
     */
    this.discussions = [];

    this.refresh();
  }

  view() {
    const params = this.props.params;
    let loading;

    if (this.loading) {
      loading = LoadingIndicator.component();
    } else if (this.moreResults) {
      loading = Button.component({
        children: app.trans('core.load_more'),
        className: 'Button',
        onclick: this.loadMore.bind(this)
      });
    }

    return (

          {this.discussions.map(discussion => {
            return (

                {DiscussionListItem.component({discussion, params})}

            );
          })}

          {loading}

    );
  }

  /**
   * Get the parameters that should be passed in the API request to get
   * discussion results.
   *
   * @return {Object}
   * @api
   */
  requestParams() {
    const params = {include: ['startUser', 'lastUser'], filter: {}};

    params.sort = this.sortMap()[this.props.params.sort];

    if (this.props.params.q) {
      params.filter.q = this.props.params.q;

      params.include.push('relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user');
    }

    return params;
  }

  /**
   * Get a map of sort keys (which appear in the URL, and are used for
   * translation) to the API sort value that they represent.
   *
   * @return {Object}
   */
  sortMap() {
    const map = {};

    if (this.props.params.q) {
      map.relevance = '';
    }
    map.latest = '-lastTime';
    map.top = '-commentsCount';
    map.newest = '-startTime';
    map.oldest = '+startTime';

    return map;
  }

  /**
   * Clear and reload the discussion list.
   *
   * @public
   */
  refresh(clear = true) {
    if (clear) {
      this.loading = true;
      this.discussions = [];
    }

    return this.loadResults().then(
      results => {
        this.discussions = [];
        this.parseResults(results);
      },
      () => {
        this.loading = false;
        m.redraw();
      }
    );
  }

  /**
   * Load a new page of discussion results.
   *
   * @param {Integer} offset The index to start the page at.
   * @return {Promise}
   */
  loadResults(offset) {
    const preloadedDiscussions = app.preloadedDocument();

    if (preloadedDiscussions) {
      return m.deferred().resolve(preloadedDiscussions).promise;
    }

    const params = this.requestParams();
    params.page = {offset};
    params.include = params.include.join(',');

    return app.store.find('discussions', params);
  }

  /**
   * Load the next page of discussion results.
   *
   * @public
   */
  loadMore() {
    this.loading = true;

    this.loadResults(this.discussions.length)
      .then(this.parseResults.bind(this));
  }

  /**
   * Parse results and append them to the discussion list.
   *
   * @param {Discussion[]} results
   * @return {Discussion[]}
   */
  parseResults(results) {
    [].push.apply(this.discussions, results);

    this.loading = false;
    this.moreResults = !!results.payload.links.next;

    // Since this may be called during the component's constructor, i.e. in the
    // middle of a redraw, forcing another redraw would not bode well. Instead
    // we start/end a computation so Mithril will only redraw if it isn't
    // already doing so.
    m.startComputation();
    m.endComputation();

    return results;
  }

  /**
   * Remove a discussion from the list if it is present.
   *
   * @param {Discussion} discussion
   * @public
   */
  removeDiscussion(discussion) {
    const index = this.discussions.indexOf(discussion);

    if (index !== -1) {
      this.discussions.splice(index, 1);
    }
  }

  /**
   * Add a discussion to the top of the list.
   *
   * @param {Discussion} discussion
   * @public
   */
  addDiscussion(discussion) {
    this.discussions.unshift(discussion);
  }
}
<|endoftext|>


<commit_before>/*
 * freepano - WebGL panorama viewer
 *
 * Copyright (c) 2015 FOXEL SA - http://foxel.ch
 * Please read <http://foxel.ch/license> for more information.
 *
 *
 * Author(s):
 *
 *      Luc Deschenaux <l.deschenaux@foxel.ch>
 *
 * This file is part of the FOXEL project <http://foxel.ch>.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Additional Terms:
 *
 *      You are required to preserve legal notices and author attributions in
 *      that material or in the Appropriate Legal Notices displayed by works
 *      containing it.
 *
 *      You are required to attribute the work as explained in the "Usage and
 *      Attribution" section of <http://foxel.ch/license>.
 */

(function(root){

var $=root.jQuery;
var THREE=root.THREE;
var Panorama=root.Panorama;
var PointCloud=root.PointCloud;
var setupEventDispatcher=root.setupEventDispatcher;

function ParticleSequence(options){

  if (!(this instanceof ParticleSequence)) {
    return new ParticleSequence(options);
  }

  $.extend(true,this,this.defaults,options);

  this.init();

} // ParticleSequence

$.extend(true,ParticleSequence.prototype,{

    defaults: {

      mode: {},

      // point cloud particle list
      particle_list: [],

      // line segments
      line: {
        scene: null,
        instance: {},
        material: {
          options: {
            color: 'black',
            lineWidth: 2
          }
        },
        geometry: {
          maxPoints: 500,
          options: {}
        },
        type: THREE.LineStrip
      },

      // line segment labels
      label: {
        scene: null,
        object3D_list: []
      }
    },

    // Init particle sequence with its scene, line geometry and material, and label scene
    // If particle_list is not empty, initialize particle sequence.
    init: function particleSequence_init(){

      var seq=this;
      var line=seq.line;

      // if THREE.Line has to be instantiated
      if (!(line.instance instanceof THREE.Line)) {

        // if line geometry has to be instantiated
        if (!(line.geometry.instance instanceof THREE.Geometry)){

          // instantiate line geometry
          var geometry=line.geometry.instance=new THREE.Geometry(line.geometry.options);

          // create empty buffer vertices
          var maxPoints=line.geometry.maxPoints;
          var v=new THREE.Vector3();
          for (var i=0; i<maxPoints; ++i) {
            geometry.vertices.push(v);
          }
        }

        // instantiate line material
        if (!(line.material.instance instanceof THREE.LineBasicMaterial)) {
          line.material.instance=new THREE.LineBasicMaterial(line.material.options);
        }

        // instantiate THREE.Line
        line.instance=new THREE.Line(line.geometry.instance,line.material.instance,line.type);
        line.instance.dynamic=true;

        // instantiate line.scene
        if (!(line.scene instanceof THREE.Scene)) {
          line.scene=new THREE.Scene();
        }

        if (!(seq.label.scene instanceof THREE.Scene)) {
          seq.label.scene=new THREE.Scene();
        }

        line.scene.add(line.instance);

      }

      // add particles to sequence, if any specified
      if (seq.particle_list.length){
        var particles_to_be_added=seq.particle_list;
        seq.particle_list=[];
        $.each(particles_to_be_added,function(){
          var particle_index=this;
          seq.add(particle_index);
        });
      }

    }, // particleSequence_init

    // add a particle to the sequence, update line geometry and segment label list accordingly
    add: function particleSequence_add(particle_index,flags) {

      var seq=this;
      var line=seq.line;
      var pointCloud=seq.pointCloud;
      var panorama=pointCloud.panorama;

      var prevcount=seq.particle_list.length;
      if (prevcount>=line.geometry.maxPoints) {
        $.notify('Maximum number of segments reached');
        return;
      }

      // dont add the same particle twice in a row
      if (prevcount && seq.particle_list[prevcount-1].index==particle_index) {

        // detect double click
        if (flags && flags.isclick) {

          if (seq.doubleClick && seq.doubleClick.index==particle_index) {
             seq.doubleClick={
               bool: true,
               index: particle_index,
               t: Date.now()-seq.doubleClick.t
             }

          } else {
            seq.doubleClick={
              bool: false,
              index: particle_index,
              t: Date.now()
            };
          }
        }

        return;

      } else {

        if (flags && flags.isclick) {
          seq.doubleClick={
            bool: false,
            index: particle_index,
            t: Date.now()
          };
        }

      }

      var particle={index:particle_index};

      // register particle index
      seq.particle_list.push(particle);

      // get particle world coordinates
      var vw=new THREE.Vector3().copy(pointCloud.getParticlePosition(particle_index));

      // convert to sphere coordinates
      var vs=new THREE.Vector3().copy(vw).normalize().multiplyScalar(panorama.sphere.radius);

      // add particle to line geometry
      var geometry=line.instance.geometry;
      if (prevcount) {
        geometry.vertices.shift();
      }

      geometry.vertices[line.geometry.maxPoints-1]=vs;
      geometry.verticesNeedUpdate=true;
      geometry.computeLineDistances();

      // add new segment label ?
      if (prevcount) {

        // get particles world coordinates
        var v1=new THREE.Vector3().copy(pointCloud.getParticlePosition(seq.particle_list[prevcount-1].index));
        var v2=vw;

        // compute segment length, rounded to cm
        var distance=Math.round(v1.distanceTo(v2)*100)/100;

        // instantiate segment label text sprite
        var canvas=seq.labelText2Canvas(distance+'m');
        var texture=new THREE.Texture(canvas);
        texture.needsUpdate=true;
        var sprite=new THREE.Sprite(new THREE.SpriteMaterial({
           map: texture,
           transparent: true,
           depthTest: false,
           depthWrite: false
        }));
        sprite.scale.set(canvas.width/20,canvas.height/20,1.0);

        var object3D=new THREE.Object3D();
        object3D.add(sprite);

        // get particles sphere coordinates
        v1=geometry.vertices[line.geometry.maxPoints-2];
        v2=vs;

        // set segment label position (midpoint)
        object3D.position.set((v1.x+v2.x)/2,(v1.y+v2.y)/2,(v1.z+v2.z)/2);

        // add segment label to list
        seq.label.object3D_list.push(object3D);
        seq.label.scene.add(object3D);
      }

      seq.dispatch('add',particle);

    }, // particleSequence_add

    // remove last particle from sequence
    pop: function particleSequence_pop(particle) {

      var seq=this;
      var line=seq.line;
      var pointCloud=seq.pointCloud;
      var panorama=pointCloud.panorama;
      var prevcount=seq.particle_list.length;

      if (!prevcount) {
        return;
      }

      if (seq.particle_list[prevcount-1].index!=particle.index) {
        return;
      }

      // remove particle from list
      particle=seq.particle_list.pop();

      // remove particle from Line
      var geometry=line.instance.geometry;
      geometry.vertices.unshift(new THREE.Vector3());
      geometry.verticesNeedUpdate=true;
      geometry.computeLineDistances();

      // remove label
      var labelIndex=seq.label.object3D_list.length-1;
      if (labelIndex>=0) {
        seq.label.scene.remove(seq.label.object3D_list[labelIndex]);
        seq.label.object3D_list.pop();
      }

      seq.dispatch('pop',particle);

    }, // particleSequence_pop

    labelText2Canvas: function particleSequence_labelText2Canvas(text,options){

      function roundRect(ctx, x, y, w, h, r) {
          ctx.beginPath();
          ctx.moveTo(x+r, y);
          ctx.lineTo(x+w-r, y);
          ctx.quadraticCurveTo(x+w, y, x+w, y+r);
          ctx.lineTo(x+w, y+h-r);
          ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
          ctx.lineTo(x+r, y+h);
          ctx.quadraticCurveTo(x, y+h, x, y+h-r);
          ctx.lineTo(x, y+r);
          ctx.quadraticCurveTo(x, y, x+r, y);
          ctx.closePath();
          ctx.fill();
          ctx.stroke();
      }

      var options=$.extend(true,{
         fontSize: 48,
         padding: 16,
      }, options);

      var canvas=document.createElement('canvas');
      var ctx=canvas.getContext('2d');

      // measure text
      ctx.font=options.font||" "+options.fontSize+"px helvetica";
      ctx.align='left';
      ctx.textBaseline='middle';
      var size=ctx.measureText(text);

      // set canvas size according to text width
      canvas.width=size.width+options.padding;
      canvas.height=options.padding*2+options.fontSize;
      ctx=canvas.getContext('2d');

      // draw rect
      ctx.lineWidth=2;
      var border=ctx.lineWidth;
      ctx.fillStyle="rgba(236,177,0,0.5)";
      ctx.strokeStyle="rgba(0,0,0,0.5)";
      roundRect(ctx, border/2, border/2, canvas.width-8, canvas.height-8, 8);

      // draw text
      ctx.beginPath();
      ctx.lineWidth=1;
      ctx.font=options.font||" "+options.fontSize+"px helvetica";
      ctx.fillStyle=options.fillStyle||"rgba(0,0,0,1)";
      ctx.strokeStyle=options.strokeStyle||"rgba(0,0,0,1)";
      ctx.align='left';
      ctx.textBaseline='middle';
      ctx.fillText(text,options.padding,canvas.height/2,(canvas.width-options.padding*2));
//      ctx.strokeText(text,options.padding,canvas.height/2,(canvas.width-options.padding*2));

      return canvas;

    }, // particleSequence_labelText2Canvas

    // render lines and labels
    on_pointcloud_render: function particleSequence_on_pointcloud_render(e) {
      var pointCloud=this;
      var seq_list=pointCloud.sequence;

      if (!seq_list) {
          return;
      }

      var panorama=pointCloud.panorama;

      $.each(seq_list,function(){

        var seq=this;

        if (!seq || seq.particle_list.length<2) {
          return;
        }

        // render line segments
        panorama.renderer.clearDepth();
        panorama.renderer.render(seq.line.scene,panorama.camera.instance);

        // render segment labels
        panorama.renderer.clearDepth();
        panorama.renderer.render(seq.label.scene,panorama.camera.instance);

      });

    }, // particleSequence_on_pointcloud_render

    // when no particle hovered, trigger particleclick for last mouseout index if any
    on_panorama_click: function particleSequence_on_panorama_click(e) {
        var panorama=this;
        if (!panorama.pointCloud || !panorama.pointCloud.instance || !panorama.pointCloud.instance.sequence) {
            return;
        }

        var seq=panorama.pointCloud.instance.sequence[panorama.pointCloud.instance.sequence.length-1];
        if (seq) {
          if (seq.lastmouseout!==undefined) {
              seq.pointCloud.dispatch({
                  type: 'particleclick',
                  target: seq.lastmouseout,
                  originalEvent: e
              });
          }
        }
    },

    // add particle to sequence on click
    on_pointcloud_particleclick: function particleSequence_on_pointcloud_particleclick(e) {

      var pointCloud=this;

      if (e.originalEvent.particleSequence_on_pointcloud_particleclick_was_here) {
          return;
      }
      e.originalEvent.particleSequence_on_pointcloud_particleclick_was_here=true;

      if (!pointCloud.sequence || !pointCloud.sequence.length) {
          return;
      }

      var seq=pointCloud.sequence[pointCloud.sequence.length-1];
      var panorama=pointCloud.panorama;

      if (!seq) {
        return;
      }

      if (!seq.mode.add) {
        return;
      }

      seq.add(e.target,{isclick:true});
      seq.lastclicked=e.target;
      seq.lastmouseout=undefined;

      panorama.drawScene();

    }, // particleSequence_on_pointcloud_particleclick

    // add particle onmousein if it's not the first one and 'wheredowegofromhere' mode is enabled
    on_pointcloud_particlemousein: function particleSequence_on_pointcloud_particlemousein(e) {
      var pointCloud=this;

      if (!pointCloud.sequence || !pointCloud.sequence.length) {
          return;
      }

      var seq=pointCloud.sequence[pointCloud.sequence.length-1];
      var panorama=pointCloud.panorama;

      if (!seq) {
        return;
      }

      if (!seq.mode.wheredowegofromhere) {
        return;
      }

      if (!seq.particle_list.length) {
        return;
      }

      var particle=seq.particle_list[seq.particle_list.length-1];

      if (seq.lastmouseout==particle.index) {
          seq.pop(particle);
          seq.lastmouseout=undefined;
      }

      seq.add(e.target);
      panorama.drawScene();

    }, // on_pointcloud_particlemousein

    // remove last particle added onmousein
    on_pointcloud_particlemouseout: function particleSequence_on_pointcloud_particlemouseout(e) {
      var pointCloud=this;

      if (!pointCloud.sequence || !pointCloud.sequence.length) {
          return;
      }

      var seq=pointCloud.sequence[pointCloud.sequence.length-1];
      var panorama=pointCloud.panorama;

      if (!seq) {
        return;
      }

      if (!seq.mode.wheredowegofromhere) {
        return;
      }

      if (seq.particle_list.length<2) {
        return;
      }

      // dont remove the particle we just added on click
      if  (seq.mode.add && seq.lastclicked==e.target) {
        return;
      }

      seq.lastmouseout=e.target;

    }, // on_pointcloud_particlemouseout

    on_pointcloud_ready: function particleSequence_on_pointcloud_ready(e) {
      var pointCloud=this;

      pointCloud.sequence=[new ParticleSequence({
        pointCloud: pointCloud
      })];

    }, // particleSequence_on_pointcloud_ready

    on_pointcloud_dispose: function particleSequence_on_pointcloud_dispose(e) {
    }, // particleSequence_on_pointcloud_dispose

    /** ParticleSequence.ondispose()
    * @todo
    */
    ondispose: function particleSequence_ondispose() {
        this.dispose();
    }, // particleSequence_ondispose

    /** ParticleSequence.ondispose()
    * @todo
    */
    dispose: function particleSequence_dispose() {
    }

}); // extend ParticleSequence prototype

// define PoinCloud.Sequence
PointCloud.prototype.Sequence=ParticleSequence;

// register to Panorama events
Panorama.prototype.dispatchEventsTo(ParticleSequence.prototype);

// register to PointCloud events
PointCloud.prototype.dispatchEventsTo(ParticleSequence.prototype);

// allow ParticleSequence instances to dispatch events
setupEventDispatcher(ParticleSequence.prototype);

})(window);

<commit_msg>add ParticleSequence.fixedLabelScale property<commit_after>/*
 * freepano - WebGL panorama viewer
 *
 * Copyright (c) 2015 FOXEL SA - http://foxel.ch
 * Please read <http://foxel.ch/license> for more information.
 *
 *
 * Author(s):
 *
 *      Luc Deschenaux <l.deschenaux@foxel.ch>
 *
 * This file is part of the FOXEL project <http://foxel.ch>.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Additional Terms:
 *
 *      You are required to preserve legal notices and author attributions in
 *      that material or in the Appropriate Legal Notices displayed by works
 *      containing it.
 *
 *      You are required to attribute the work as explained in the "Usage and
 *      Attribution" section of <http://foxel.ch/license>.
 */

(function(root){

var $=root.jQuery;
var THREE=root.THREE;
var Panorama=root.Panorama;
var PointCloud=root.PointCloud;
var setupEventDispatcher=root.setupEventDispatcher;

function ParticleSequence(options){

  if (!(this instanceof ParticleSequence)) {
    return new ParticleSequence(options);
  }

  $.extend(true,this,this.defaults,options);

  this.init();

} // ParticleSequence

$.extend(true,ParticleSequence.prototype,{

    defaults: {

      mode: {},

      // point cloud particle list
      particle_list: [],

      // line segments
      line: {
        scene: null,
        instance: {},
        material: {
          options: {
            color: 'black',
            lineWidth: 2
          }
        },
        geometry: {
          maxPoints: 500,
          options: {}
        },
        type: THREE.LineStrip
      },

      // line segment labels
      label: {
        scene: null,
        object3D_list: []
      }
    },

    // Init particle sequence with its scene, line geometry and material, and label scene
    // If particle_list is not empty, initialize particle sequence.
    init: function particleSequence_init(){

      var seq=this;
      var line=seq.line;

      // if THREE.Line has to be instantiated
      if (!(line.instance instanceof THREE.Line)) {

        // if line geometry has to be instantiated
        if (!(line.geometry.instance instanceof THREE.Geometry)){

          // instantiate line geometry
          var geometry=line.geometry.instance=new THREE.Geometry(line.geometry.options);

          // create empty buffer vertices
          var maxPoints=line.geometry.maxPoints;
          var v=new THREE.Vector3();
          for (var i=0; i<maxPoints; ++i) {
            geometry.vertices.push(v);
          }
        }

        // instantiate line material
        if (!(line.material.instance instanceof THREE.LineBasicMaterial)) {
          line.material.instance=new THREE.LineBasicMaterial(line.material.options);
        }

        // instantiate THREE.Line
        line.instance=new THREE.Line(line.geometry.instance,line.material.instance,line.type);
        line.instance.dynamic=true;

        // instantiate line.scene
        if (!(line.scene instanceof THREE.Scene)) {
          line.scene=new THREE.Scene();
        }

        if (!(seq.label.scene instanceof THREE.Scene)) {
          seq.label.scene=new THREE.Scene();
        }

        line.scene.add(line.instance);

      }

      // add particles to sequence, if any specified
      if (seq.particle_list.length){
        var particles_to_be_added=seq.particle_list;
        seq.particle_list=[];
        $.each(particles_to_be_added,function(){
          var particle_index=this;
          seq.add(particle_index);
        });
      }

    }, // particleSequence_init

    // add a particle to the sequence, update line geometry and segment label list accordingly
    add: function particleSequence_add(particle_index,flags) {

      var seq=this;
      var line=seq.line;
      var pointCloud=seq.pointCloud;
      var panorama=pointCloud.panorama;

      var prevcount=seq.particle_list.length;
      if (prevcount>=line.geometry.maxPoints) {
        $.notify('Maximum number of segments reached');
        return;
      }

      // dont add the same particle twice in a row
      if (prevcount && seq.particle_list[prevcount-1].index==particle_index) {

        // detect double click
        if (flags && flags.isclick) {

          if (seq.doubleClick && seq.doubleClick.index==particle_index) {
             seq.doubleClick={
               bool: true,
               index: particle_index,
               t: Date.now()-seq.doubleClick.t
             }

          } else {
            seq.doubleClick={
              bool: false,
              index: particle_index,
              t: Date.now()
            };
          }
        }

        return;

      } else {

        if (flags && flags.isclick) {
          seq.doubleClick={
            bool: false,
            index: particle_index,
            t: Date.now()
          };
        }

      }

      var particle={index:particle_index};

      // register particle
      seq.particle_list.push(particle);

      // get particle world coordinates
      var vw=new THREE.Vector3().copy(pointCloud.getParticlePosition(particle_index));

      // convert to sphere coordinates
      var vs=new THREE.Vector3().copy(vw).normalize().multiplyScalar(panorama.sphere.radius);

      // add particle to line geometry
      var geometry=line.instance.geometry;
      if (prevcount) {
        geometry.vertices.shift();
      }

      geometry.vertices[line.geometry.maxPoints-1]=vs;
      geometry.verticesNeedUpdate=true;
      geometry.computeLineDistances();

      // add new segment label ?
      if (prevcount) {

        // get particles world coordinates
        var v1=new THREE.Vector3().copy(pointCloud.getParticlePosition(seq.particle_list[prevcount-1].index));
        var v2=vw;

        // compute segment length, rounded to cm
        var distance=Math.round(v1.distanceTo(v2)*100)/100;

        // instantiate segment label text sprite
        var canvas=seq.labelText2Canvas(distance+'m');
        var texture=new THREE.Texture(canvas);
        texture.needsUpdate=true;
        var sprite=new THREE.Sprite(new THREE.SpriteMaterial({
           map: texture,
           transparent: true,
           depthTest: false,
           depthWrite: false
        }));

        // set label scale factor
        if (seq.fixedLabelScale==true) {
            // fixed label scale
            sprite._scaleFactor=1/20;

        } else {
            // distance between line midpoint in world coords and camera
            var labelDistance=new THREE.Vector3((v1.x+v2.x)/2,(v1.y+v2.y)/2,(v1.z+v2.z)/2).length();
            sprite._scaleFactor=1/(10+labelDistance);
        }

        sprite.scale.set(canvas.width*sprite._scaleFactor,canvas.height*sprite._scaleFactor,1.0);

        var object3D=new THREE.Object3D();
        object3D.add(sprite);

        // get particles sphere coordinates
        v1=geometry.vertices[line.geometry.maxPoints-2];
        v2=vs;

        // set segment label position (midpoint)
        object3D.position.set((v1.x+v2.x)/2,(v1.y+v2.y)/2,(v1.z+v2.z)/2);

        // add segment label to list
        seq.label.object3D_list.push(object3D);
        seq.label.scene.add(object3D);
      }

      seq.dispatch('add',particle);

    }, // particleSequence_add

    // remove last particle from sequence
    pop: function particleSequence_pop(particle) {

      var seq=this;
      var line=seq.line;
      var pointCloud=seq.pointCloud;
      var panorama=pointCloud.panorama;
      var prevcount=seq.particle_list.length;

      if (!prevcount) {
        return;
      }

      if (seq.particle_list[prevcount-1].index!=particle.index) {
        return;
      }

      // remove particle from list
      particle=seq.particle_list.pop();

      // remove particle from Line
      var geometry=line.instance.geometry;
      geometry.vertices.unshift(new THREE.Vector3());
      geometry.verticesNeedUpdate=true;
      geometry.computeLineDistances();

      // remove label
      var labelIndex=seq.label.object3D_list.length-1;
      if (labelIndex>=0) {
        seq.label.scene.remove(seq.label.object3D_list[labelIndex]);
        seq.label.object3D_list.pop();
      }

      seq.dispatch('pop',particle);

    }, // particleSequence_pop

    labelText2Canvas: function particleSequence_labelText2Canvas(text,options){

      function roundRect(ctx, x, y, w, h, r) {
          ctx.beginPath();
          ctx.moveTo(x+r, y);
          ctx.lineTo(x+w-r, y);
          ctx.quadraticCurveTo(x+w, y, x+w, y+r);
          ctx.lineTo(x+w, y+h-r);
          ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
          ctx.lineTo(x+r, y+h);
          ctx.quadraticCurveTo(x, y+h, x, y+h-r);
          ctx.lineTo(x, y+r);
          ctx.quadraticCurveTo(x, y, x+r, y);
          ctx.closePath();
          ctx.fill();
          ctx.stroke();
      }

      var options=$.extend(true,{
         fontSize: 48,
         padding: 16,
      }, options);

      var canvas=document.createElement('canvas');
      var ctx=canvas.getContext('2d');

      // measure text
      ctx.font=options.font||" "+options.fontSize+"px helvetica";
      ctx.align='left';
      ctx.textBaseline='middle';
      var size=ctx.measureText(text);

      // set canvas size according to text width
      canvas.width=size.width+options.padding;
      canvas.height=options.padding*2+options.fontSize;
      ctx=canvas.getContext('2d');

      // draw rect
      ctx.lineWidth=2;
      var border=ctx.lineWidth;
      ctx.fillStyle="rgba(236,177,0,0.5)";
      ctx.strokeStyle="rgba(0,0,0,0.5)";
      roundRect(ctx, border/2, border/2, canvas.width-8, canvas.height-8, 8);

      // draw text
      ctx.beginPath();
      ctx.lineWidth=1;
      ctx.font=options.font||" "+options.fontSize+"px helvetica";
      ctx.fillStyle=options.fillStyle||"rgba(0,0,0,1)";
      ctx.strokeStyle=options.strokeStyle||"rgba(0,0,0,1)";
      ctx.align='left';
      ctx.textBaseline='middle';
      ctx.fillText(text,options.padding,canvas.height/2,(canvas.width-options.padding*2));
//      ctx.strokeText(text,options.padding,canvas.height/2,(canvas.width-options.padding*2));

      return canvas;

    }, // particleSequence_labelText2Canvas

    // render lines and labels
    on_pointcloud_render: function particleSequence_on_pointcloud_render(e) {
      var pointCloud=this;
      var seq_list=pointCloud.sequence;

      if (!seq_list) {
          return;
      }

      var panorama=pointCloud.panorama;

      $.each(seq_list,function(){

        var seq=this;

        if (!seq || seq.particle_list.length<2) {
          return;
        }

        // render line segments
        panorama.renderer.clearDepth();
        panorama.renderer.render(seq.line.scene,panorama.camera.instance);

        // render segment labels
        panorama.renderer.clearDepth();
        panorama.renderer.render(seq.label.scene,panorama.camera.instance);

      });

    }, // particleSequence_on_pointcloud_render

    // when no particle hovered, trigger particleclick for last mouseout index if any
    on_panorama_click: function particleSequence_on_panorama_click(e) {
        var panorama=this;
        if (!panorama.pointCloud || !panorama.pointCloud.instance || !panorama.pointCloud.instance.sequence) {
            return;
        }

        var seq=panorama.pointCloud.instance.sequence[panorama.pointCloud.instance.sequence.length-1];
        if (seq) {
          if (seq.lastmouseout!==undefined) {
              seq.pointCloud.dispatch({
                  type: 'particleclick',
                  target: seq.lastmouseout,
                  originalEvent: e
              });
          }
        }
    },

    // add particle to sequence on click
    on_pointcloud_particleclick: function particleSequence_on_pointcloud_particleclick(e) {

      var pointCloud=this;

      if (e.originalEvent.particleSequence_on_pointcloud_particleclick_was_here) {
          return;
      }
      e.originalEvent.particleSequence_on_pointcloud_particleclick_was_here=true;

      if (!pointCloud.sequence || !pointCloud.sequence.length) {
          return;
      }

      var seq=pointCloud.sequence[pointCloud.sequence.length-1];
      var panorama=pointCloud.panorama;

      if (!seq) {
        return;
      }

      if (!seq.mode.add) {
        return;
      }

      seq.add(e.target,{isclick:true});
      seq.lastclicked=e.target;
      seq.lastmouseout=undefined;

      panorama.drawScene();

    }, // particleSequence_on_pointcloud_particleclick

    // add particle onmousein if it's not the first one and 'wheredowegofromhere' mode is enabled
    on_pointcloud_particlemousein: function particleSequence_on_pointcloud_particlemousein(e) {
      var pointCloud=this;

      if (!pointCloud.sequence || !pointCloud.sequence.length) {
          return;
      }

      var seq=pointCloud.sequence[pointCloud.sequence.length-1];
      var panorama=pointCloud.panorama;

      if (!seq) {
        return;
      }

      if (!seq.mode.wheredowegofromhere) {
        return;
      }

      if (!seq.particle_list.length) {
        return;
      }

      var particle=seq.particle_list[seq.particle_list.length-1];

      if (seq.lastmouseout==particle.index) {
          seq.pop(particle);
          seq.lastmouseout=undefined;
      }

      seq.add(e.target);
      panorama.drawScene();

    }, // on_pointcloud_particlemousein

    // remove last particle added onmousein
    on_pointcloud_particlemouseout: function particleSequence_on_pointcloud_particlemouseout(e) {
      var pointCloud=this;

      if (!pointCloud.sequence || !pointCloud.sequence.length) {
          return;
      }

      var seq=pointCloud.sequence[pointCloud.sequence.length-1];
      var panorama=pointCloud.panorama;

      if (!seq) {
        return;
      }

      if (!seq.mode.wheredowegofromhere) {
        return;
      }

      if (seq.particle_list.length<2) {
        return;
      }

      // dont remove the particle we just added on click
      if  (seq.mode.add && seq.lastclicked==e.target) {
        return;
      }

      seq.lastmouseout=e.target;

    }, // on_pointcloud_particlemouseout

    on_pointcloud_ready: function particleSequence_on_pointcloud_ready(e) {
      var pointCloud=this;

      pointCloud.sequence=[new ParticleSequence({
        pointCloud: pointCloud
      })];

    }, // particleSequence_on_pointcloud_ready

    on_pointcloud_dispose: function particleSequence_on_pointcloud_dispose(e) {
    }, // particleSequence_on_pointcloud_dispose

    /** ParticleSequence.ondispose()
    * @todo
    */
    ondispose: function particleSequence_ondispose() {
        this.dispose();
    }, // particleSequence_ondispose

    /** ParticleSequence.ondispose()
    * @todo
    */
    dispose: function particleSequence_dispose() {
    }

}); // extend ParticleSequence prototype

// define PoinCloud.Sequence
PointCloud.prototype.Sequence=ParticleSequence;

// register to Panorama events
Panorama.prototype.dispatchEventsTo(ParticleSequence.prototype);

// register to PointCloud events
PointCloud.prototype.dispatchEventsTo(ParticleSequence.prototype);

// allow ParticleSequence instances to dispatch events
setupEventDispatcher(ParticleSequence.prototype);

})(window);

<|endoftext|>