/*globals $, _, Power0, google, TweenLite, __scope__, jQuery */
(function(parentScope) {
'use strict';
/**
* Create a <code>StreetviewSequence</code> object.
* @constructor
* @param {string|jQuery} container The container to which the animation canvas will be appended.
* @param {Object} options
* @param {string} [options.domain] Scheme + host which to pass the query parameters. Useful when using a proxy to generate signed URLs for high resolution imagery.
* @param {number} [options.duration=1] Duration of the animation.
* @param {Ease} [options.easeHeading=Power0.easeIn] Greensock easing method used for heading skew.
* @param {Ease} [options.easePitch=Power0.easeIn] Greensock easing method used for pitch skew.
* @param {Ease} [options.easeRoute=Power0.easeIn] Greensock easing method used for route waypoint selection.
* @param {number} [options.headingSkewEnd=options.headingSkewStart] Heading at which to end the animation (horizontal).
* @param {number} [options.headingSkewStart=0] Heading at which to start the animation (horizontal).
* @param {number} [options.height=150] (Intrinsic) height of the animation canvas in pixels.
* @param {string} [options.key] Google API key.
* @param {google.maps.LatLng} [options.location] Location at which to place the stationary panorama.
* @param {boolean} [options.loop=false] Whether or not the animation should loop.
* @param {number} [options.pitchSkewEnd=options.pitchSkewStart] Pitch at which to end the animation (vertical).
* @param {number} [options.pitchSkewStart=0] Pitch at which to start the animation (vertical).
* @param {google.maps.google.maps.DirectionsResult} [options.route] Maps directions result for route stepping.
* @param {boolean} [options.sensor=false] Indicates whether or not the request came from a device using
* a location sensor (e.g. a GPS) to determine the location sent in this request. This value must be either true or false.
* @param {number} [options.totalFrames=75] Total number of frames to be used for animation.
* @param {number} [options.width=300] (Intrinsic) height of the animation canvas in pixels.
* @return {jQuery.Deferred.Promise}
*/
parentScope.StreetviewSequence = function (container, options) {
var $canvas;
var $container;
var canvas;
var ctx;
var defaults = {
duration: 1,
easeHeading: Power0.easeIn,
easePitch: Power0.easeIn,
easeRoute: Power0.easeIn,
headingSkewStart: 0,
height: 150,
loop: false,
pitchSkewStart: 0,
sensor: false,
totalFrames: 75,
width: 300
};
var headingCache = {};
var images;
var imagesLoadedCount;
var publicMethods;
var streetViewPanoramaDfd;
var streetViewService;
var tween;
function _init() {
_.defaults(options, defaults);
$canvas = $('<canvas />');
$container = (container instanceof jQuery) ? container : $(container);
$container.append($canvas);
canvas = $canvas.get(0);
ctx = canvas.getContext('2d');
images = [];
imagesLoadedCount = 0;
streetViewPanoramaDfd = $.Deferred();
streetViewService = new google.maps.StreetViewService();
canvas.height = options.height;
canvas.width = options.width;
if ('undefined' === typeof options.headingSkewEnd) {
options.headingSkewEnd = options.headingSkewStart;
}
if ('undefined' === typeof options.pitchSkewEnd) {
options.pitchSkewEnd = options.pitchSkewStart;
}
tween = TweenLite.to(
{ currentTime: 0 },
options.duration / 1000,
{
currentTime: options.duration,
onComplete: ended,
onReverseComplete: ended,
onUpdate: draw,
paused: true
}
);
loadImages();
}
/**
* Calculate the current value of an ease based on it's progress
* given a start and end point.
* @param {number} p Raw linear progress of ease
* @param {Ease} ease Ease function used for completion ratio calculation
* @param {number} start Starting eased value
* @param {number} end Ending eased value
* @return {number}
*/
function calcScalar(p, ease, start, end) {
var delta;
delta = end - start;
return start + delta * ease.getRatio(p);
}
/**
* Draw the current frame onto the animation canvas. Current frame is derived
* from the animation progress and the total amount of frames.
*/
function draw() {
var p = tween.progress();
var idx = Math.round(p * (options.totalFrames - 1));
images[idx].done(function (img) {
ctx.drawImage(img, 0, 0);
});
}
/**
* If <code>options.loop</code> is set to false, trigger an <code>ended</code>
* event. If true, the animation is then reversed, and thus, loops.
*/
function ended() {
if (!options.loop) {
$canvas.trigger('ended');
return;
}
if (tween.reversed()) {
tween.restart();
} else {
tween.reverse();
}
}
/**
* As of Google Maps Javascript API V3 3.16, a literal lat/lng object and a <code>google.maps.LatLng</code> object
* are accepted as location points. This facade makes this easier to manage.
* @see https://developers.google.com/maps/documentation/javascript/3.exp/reference#LatLngLiteral
* @param {Object|google.maps.LatLng} latLng google.maps.LatLng object or a literal lat/lng object.
* @return {number}
*/
function getLng(latLng) {
return ('function' === typeof latLng.lng) ? latLng.lng() : latLng.lng;
}
/**
* As of Google Maps Javascript API V3 3.16, a literal lat/lng object and a <code>google.maps.LatLng</code> object
* are accepted as location points. This facade makes this easier to manage.
* @see https://developers.google.com/maps/documentation/javascript/3.exp/reference#LatLngLiteral
* @param {Object|google.maps.LatLng} latLng google.maps.LatLng object or a literal lat/lng object.
* @return {number}
*/
function getLat(latLng) {
return ('function' === typeof latLng.lat) ? latLng.lat() : latLng.lat;
}
/**
* For the given <code>options.location</code>, retrieve the necessary information to
* build a Street View Image URL. Note that the only information used from the API request
* is a <code>heading</code> that faces the street.
* @return {jQuery.Deferred.Promise} Promise of a deferred object which eventually resolves with the necessary
* information to generate a Street View Image API URL.
*/
function getPanoramaData() {
var key;
key = options.location;
if ('undefined' === typeof headingCache[key]) {
headingCache[key] = getStreetHeading(options.location)
.then(function (heading) {
return {
location: options.location,
heading: heading,
pitch: 0
};
})
;
}
return headingCache[key];
}
/**
* Retrieve the Street View Panorama information for a given point on the route based on the given
* progress, <code>p</code>. From the Street View Panorama API response, we are given the original
* panorama location, the proper heading (facing down the street), and the proper pitch (vertically centered).
* @param {number} p Progress through the list of street view images.
* @return {jQuery.Deferred.Promise} Promise of a deferred object which eventually resolves with the necessary
* information to generate a Street View Image API URL.
*/
function getRouteData(p) {
var location;
var locationDfd;
var panoResponseHandler;
var pathIndex;
var path;
path = options.route.routes[0].overview_path;
locationDfd = $.Deferred();
pathIndex = calcScalar(p, options.easeRoute, 0, path.length - 1);
location = path[Math.round(pathIndex)];
panoResponseHandler = function (result, status) {
if (google.maps.StreetViewStatus.OK !== status) {
locationDfd.reject({
error: new Error('Unable to get location panorama'),
status: status
});
return;
}
locationDfd.resolve({
location: result.location.latLng,
heading: result.tiles.centerHeading,
pitch: result.tiles.originPitch
});
};
streetViewService.getPanoramaByLocation(location, 50, panoResponseHandler);
return locationDfd.promise();
}
/**
* Get a street-facing heading for a given location accompanied with whether or not the location
* is indoors.
* @param {google.maps.LatLng} location
* @return {jQuery.Deferred.Promise} Promise of a deferred object which eventually resolves with a heading and
* a boolean value indicated whether or not the location is indoor.
*/
function getStreetHeading(location) {
var dfd = new $.Deferred();
streetViewService.getPanoramaByLocation(location, 50, function (data, status) {
if (google.maps.StreetViewStatus.OK !== status) {
dfd.reject({
error: new Error('StreetViewStatus is not OK'),
status: status
});
return;
}
if (0 === data.links.length) {
dfd.reject({
error: new Error('Nearby panorama not found')
});
return;
}
dfd.resolve(data.links[0].heading, '' === data.links[0].description);
});
return dfd.promise();
}
/**
* Build a Street View Image API URL.
* @param {object} options
* @param {string} [options.domain=window.location.protocol + '//maps.googleapis.com'] Scheme + host which to pass the query parameters. Useful when using a proxy to generate signed URLs for high resolution imagery.
* @param {number} options.height Image height in pixels.
* @param {string} [options.key] Google Maps Javascript V3 API key.
* @param {boolean} options.sensor Indicates whether or not the request came from a device using a location sensor (e.g. a GPS) to determine the location sent in this request. This value must be either true or false.
* @param {number} options.width Image width in pixels.
* @return {string} Street View Image API URL
*/
function getStreetViewImageURL(options) {
var PATH = '/maps/api/streetview';
var domain;
var parameters = {};
var resource;
domain = ('undefined' !== typeof options.domain) ? options.domain : window.location.protocol + '//maps.googleapis.com';
if ('undefined' !== typeof options.key) {
parameters.key = options.key;
}
parameters.sensor = options.sensor;
parameters.size = options.width + 'x' + options.height;
parameters.location = getLat(options.location) + ',' + getLng(options.location);
if ('undefined' !== typeof options.heading) {
parameters.heading = options.heading;
}
if ('undefined' !== typeof options.pitch) {
parameters.pitch = options.pitch;
}
if ('undefined' !== typeof options.client) {
parameters.client = options.client;
}
resource = domain + PATH + '?' + $.param(parameters);
return resource;
}
/**
* Increment the total count of loaded images, and notify the returned deferred
* object of the total load progress. If the total loaded count is equal
* to the total number of frames, the deferred object is resolved.
* Every image used in the animation has its <code>onload</code>
* event bound to this function.
*/
function imageOnLoad() {
imagesLoadedCount += 1;
streetViewPanoramaDfd.notify(imagesLoadedCount / options.totalFrames);
if (imagesLoadedCount === options.totalFrames) {
streetViewPanoramaDfd.resolve(publicMethods);
}
}
/**
* Build the array of images used for animation.
*/
function loadImages() {
var i;
var locationDataRetriever;
var locationOnDataHandlerGenerator;
var locationOnFailHandlerGenerator;
var locationPromise;
var p;
locationOnDataHandlerGenerator = function (p) {
return function (locationData) {
var currentLocationData = _.clone(locationData);
var image;
currentLocationData.heading += calcScalar(p, options.easeHeading, options.headingSkewStart, options.headingSkewEnd);
currentLocationData.pitch += calcScalar(p, options.easePitch, options.pitchSkewStart, options.pitchSkewEnd);
currentLocationData.sensor = options.sensor;
currentLocationData.key = options.key;
currentLocationData.height = options.height;
currentLocationData.width = options.width;
currentLocationData.client = options.client;
currentLocationData.domain = options.domain;
image = new Image();
image.onload = imageOnLoad;
image.src = getStreetViewImageURL(currentLocationData);
return image;
};
};
locationOnFailHandlerGenerator = function () {
return function () {
imageOnLoad();
return new Image();
};
};
locationDataRetriever = ('undefined' === typeof options.route) ? getPanoramaData : getRouteData;
for (i = 0; i < options.totalFrames; i += 1) {
p = (i / (options.totalFrames - 1));
locationPromise = locationDataRetriever(p)
.then(locationOnDataHandlerGenerator(p), locationOnFailHandlerGenerator(p))
;
images.push(locationPromise);
}
}
/**
* Pause the animation.
*/
function pause() {
tween.pause();
}
/**
* Play the animation.
*/
function play() {
tween.resume();
}
_init();
//-- Expose:
publicMethods = {
getStreetHeading: getStreetHeading,
getStreetViewImageURL: getStreetViewImageURL,
pause: pause,
play: play
};
return streetViewPanoramaDfd.promise();
};
}('undefined' !== typeof __scope__ ? __scope__ : window));