// Class for managing a timelapse. // // Dependencies: // * org.gigapan.Util // * org.gigapan.timelapse.Videoset // * org.gigapan.timelapse.VideosetStats // * jQuery (http://jquery.com/) // // Copyright 2011 Carnegie Mellon University. All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other materials // provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY ''AS IS'' AND ANY EXPRESS OR IMPLIED // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND // FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // The views and conclusions contained in the software and documentation are those of the // authors and should not be interpreted as representing official policies, either expressed // or implied, of Carnegie Mellon University. // // Authors: // Chris Bartley (bartley@cmu.edu) // Paul Dille (pdille@andrew.cmu.edu) // Randy Sargent (randy.sargent@cs.cmu.edu) // // VERIFY NAMESPACE // // Create the global symbol "org" if it doesn't exist. Throw an error if it does exist but is not an object. "use strict"; var org; var availableTimelapses=[]; if (!org) { org = {}; } else { if (typeof org != "object") { var orgExistsMessage = "Error: failed to create org namespace: org already exists and is not an object"; alert(orgExistsMessage); throw new Error(orgExistsMessage); } } // Repeat the creation and type-checking for the next level if (!org.gigapan) { org.gigapan = {}; } else { if (typeof org.gigapan != "object") { var orgGigapanExistsMessage = "Error: failed to create org.gigapan namespace: org.gigapan already exists and is not an object"; alert(orgGigapanExistsMessage); throw new Error(orgGigapanExistsMessage); } } // Repeat the creation and type-checking for the next level if (!org.gigapan.timelapse) { org.gigapan.timelapse = {}; } else { if (typeof org.gigapan.timelapse != "object") { var orgGigapanTimelapseExistsMessage = "Error: failed to create org.gigapan.timelapse namespace: org.gigapan.timelapse already exists and is not an object"; alert(orgGigapanTimelapseExistsMessage); throw new Error(orgGigapanTimelapseExistsMessage); } } // // DEPENDECIES // if (!org.gigapan.Util) { var noUtilMsg = "The org.gigapan.Util library is required by org.gigapan.timelapse.Timelapse"; alert(noUtilMsg); throw new Error(noUtilMsg); } if (!org.gigapan.timelapse.Videoset) { var noVideosetMsg = "The org.gigapan.timelapse.Videoset library is required by org.gigapan.timelapse.Timelapse"; alert(noVideosetMsg); throw new Error(noVideosetMsg); } if (!org.gigapan.timelapse.VideosetStats) { var noVideosetStatsMsg = "The org.gigapan.timelapse.VideosetStats library is required by org.gigapan.timelapse.Timelapse"; alert(noVideosetStatsMsg); throw new Error(noVideosetStatsMsg); } if (!window['$']) { var nojQueryMsg = "The jQuery library is required by org.gigapan.timelapse.Timelapse"; alert(nojQueryMsg); throw new Error(nojQueryMsg); } // // CODE // (function () { var UTIL = org.gigapan.Util; org.gigapan.timelapse.loadTimeMachine = function (json) { var timelapseObj = availableTimelapses[availableTimelapses.length-1]; timelapseObj.loadTimelapseJSON(json); }; org.gigapan.timelapse.loadVideoset = function (json) { var timelapseObj = availableTimelapses[availableTimelapses.length-1]; if (timelapseObj.getDatasetJSON() == null) timelapseObj.loadInitialVideoSet(json); else timelapseObj.loadVideoSet(json); }; org.gigapan.timelapse.loadTours = function (json) { var timelapseObj = availableTimelapses[availableTimelapses.length-1]; timelapseObj.loadToursJSON(json); }; org.gigapan.timelapse.Timelapse = function (viewerDivId, settings) { availableTimelapses.push(this); settings["videosetStatsDivId"] = settings["videosetStatsDivId"] || "videoset_stats_container"; var videoset; var videosetStats; var videoDiv; var tiles = {}; var isSplitVideo = false; var framesPerFragment = 0; var secondsPerFragment = 0; var panoWidth = 0; var panoHeight = 0; var viewportWidth = 0; var viewportHeight = 0; var tileWidth = 0; var tileHeight = 0; var videoWidth = 0; var videoHeight = 0; var frames = 0; var maxLevel = 0; var levelInfo; var metadata = null; var view = null; var targetView = null; var currentIdx = null; var currentVideo = null; var animateInterval = null; var lastAnimationTime; var minTranslateSpeedPixelsPerSecond = 25.0; var translateFractionPerSecond = 3.0; // goes 300% toward goal in 1 sec var minZoomSpeedPerSecond = .25; // in log2 var zoomFractionPerSecond = 3.0; // in log2 var keyIntervals = []; var targetViewChangeListeners = []; var thisObj = this; var tmJSON; var datasetJSON = null; var videoDivId; //var hasLayers = settings["hasLayers"] || false; var loopPlayback = settings["loopPlayback"] || false; var customLoopPlaybackRates = settings["customLoopPlaybackRates"] || null; var playOnLoad = settings["playOnLoad"] || false; var playbackSpeed = settings["playbackSpeed"] && org.gigapan.Util.isNumber(settings["playbackSpeed"]) ? settings["playbackSpeed"] : 1; var datasetLayer = settings["layer"] && org.gigapan.Util.isNumber(settings["layer"]) ? settings["layer"] : 0; var playerSize; var datasetIndex; var initialTime = settings["initialTime"] && org.gigapan.Util.isNumber(settings["initialTime"]) ? settings["initialTime"] : 0; var initialView = settings["initialView"] || null var showShareBtn = (typeof(settings["showShareBtn"]) == "undefined") ? true : settings["showShareBtn"]; var datasetPath; var tileRootPath; var customPlaybackTimeout = null; var timelapseDurationInSeconds = 0.0; var timelapseCurrentTimeInSeconds = 0.0; var timelapseCurrentCaptureTimeIndex = 0; var captureTimes = new Array(); var snaplapse; var toursJSON = {}; // levelThreshold sets the quality of display by deciding what level of tile to show for a given level of zoom: // // 1.0: select a tile that's shown between 50% and 100% size (never supersample) // 0.5: select a tile that's shown between 71% and 141% size // 0.0: select a tile that's shown between 100% and 200% size (never subsample) // -0.5: select a tile that's shown between 141% and 242% size (always supersample) // -1.0: select a tile that's shown between 200% and 400% size (always supersample) var levelThreshold = 0.05; //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Public methods // this.getViewerDivId = function () { return viewerDivId; } this.getVideoDivId = function () { return videoDivId; } this.getSnaplapse = function () { return snaplapse; } this.changeDataset = function (data) { //datasetPath = gigapanUrl; UTIL.log("changeDataset(" + datasetPath + "): view is " + JSON.stringify(view)); // Reset currentIdx so that we'll load in the new tile with the different resolution. We don't null the // currentVideo here because 1) it will be assigned in the refresh() method when it compares the bestIdx // and the currentIdx; and 2) we want currentVideo to be non-null so that the VideosetStats can keep // track of what video replaced it. currentIdx = null; onPanoLoadSuccessCallback(data, view); }; var handleKeydownEvent = function (event) { // if we are focused on a text field or the slider handlers, do not run any player specific controls if ($(".timelineSlider .ui-slider-handle:focus").length || $(".zoomSlider .ui-slider-handle:focus").length || document.activeElement == "[object HTMLInputElement]" || document.activeElement == "[object HTMLTextAreaElement]") return; var translationSpeedConstant = 20; var moveFn; //console.log(event.which); switch (event.which) { case 37: moveFn = function () { targetView.x -= translationSpeedConstant / view.scale; setTargetView(targetView); }; break; // left arrow case 39: moveFn = function () { targetView.x += translationSpeedConstant / view.scale; setTargetView(targetView); }; break; // right arrow case 38: moveFn = function () { targetView.y -= translationSpeedConstant / view.scale; setTargetView(targetView); }; break; // up arrow case 40: moveFn = function () { targetView.y += translationSpeedConstant / view.scale; setTargetView(targetView); }; break; // down arrow case 189: moveFn = function () { targetView.scale *= .94; setTargetView(targetView); }; break; // minus case 187: moveFn = function () { targetView.scale /= .94; setTargetView(targetView); }; break; // plus case 80: // P if (_isPaused()) { _play(); } else { _pause(); } break; default: return; } // Install interval to run every 50 msec while key is down // Each arrow key and +/- has its own interval, so multiple can be down at once if (keyIntervals[event.which] == undefined) keyIntervals[event.which] = setInterval(moveFn, 50); // Don't propagate arrow events -- prevent scrolling of the document if (event.which <= 40) { event.preventDefault(); // depends on jQuery } } var handleKeyupEvent = function () { if (keyIntervals[event.which] != undefined) { clearInterval(keyIntervals[event.which]); keyIntervals[event.which] = undefined; } }; this.handleMousescrollEvent = function(event, delta) { event.preventDefault(); //UTIL.log('mousescroll delta ' + delta); if (delta > 0) { zoomAbout(1 / .9, event.pageX, event.pageY); } else if (delta < 0) { zoomAbout(.9, event.pageX, event.pageY); } }; var _warpTo = function (newView) { setTargetView(newView); view.x = targetView.x; view.y = targetView.y; view.scale = targetView.scale; refresh(); }; this.warpTo = _warpTo; var _homeView = function () { var ret = computeViewFit({ xmin: 0, ymin: 0, xmax: panoWidth, ymax: panoHeight }); return ret; }; this.homeView = _homeView; this.getBoundingBoxForCurrentView = function () { return computeBoundingBox(view); }; this.warpToBoundingBox = function (bbox) { this.warpTo(computeViewFit(bbox)); }; this.resetPerf = function () { videoset.resetPerf(); }; this.getPerf = function () { return videoset.getPerf(); }; this.getView = function () { // Clone current view return $.extend({}, view); }; this.getVideoset = function () { return videoset; }; var _addTargetViewChangeListener = function (listener) { targetViewChangeListeners.push(listener); }; this.addTargetViewChangeListener = _addTargetViewChangeListener; var _addVideoPauseListener = function (listener) { videoset.addEventListener('videoset-pause', listener); }; this.addVideoPauseListener = _addVideoPauseListener; var _addVideoPlayListener = function (listener) { videoset.addEventListener('videoset-play', listener); }; this.addVideoPlayListener = _addVideoPlayListener; var _getProjection = function (projectionType) { projectionType = typeof(projectionType) != 'undefined' ? projectionType : "mercator"; if (projectionType == "mercator") { var projectionBounds = tmJSON['projection-bounds']; return new org.gigapan.timelapse.MercatorProjection( projectionBounds["west"], projectionBounds["north"], projectionBounds["east"], projectionBounds["south"], panoWidth, panoHeight); } }; this.getProjection = _getProjection; var _getViewStrAsProjection = function () { var latlng = _getProjection().pointToLatlng(view); return Math.round(1e5 * latlng.lat) / 1e5 + "," + Math.round(1e5 * latlng.lng) / 1e5 + "," + Math.round(1e3 * Math.log(view.scale / _homeView().scale) / Math.log(2))/ 1e3; }; this.getViewStrAsProjection = _getViewStrAsProjection; var _getViewStrAsPoints = function () { return Math.round(1e5 * view.x) / 1e5 + "," + Math.round(1e5 * view.y) / 1e5 + "," + Math.round(1e3 * Math.log(view.scale / _homeView().scale) / Math.log(2))/ 1e3; }; this.getViewStrAsPoints = _getViewStrAsPoints; var _getViewStr = function () { if (typeof(tmJSON['projection-bounds']) != 'undefined') { return _getViewStrAsProjection(); } else { return _getViewStrAsPoints(); } } this.getViewStr = _getViewStr; var _setNewView = function (data) { var newView = view; if (data['center']) { // Center view if ((typeof(tmJSON['projection-bounds']) != 'undefined') && data['center']['lat'] && data['center']['lng'] && data['zoom']) { newView = computeViewLatLngCenter(data); } else if (data['center']['x'] && data['center']['y'] && data['zoom']) { newView = computeViewPointCenter(data); } } else if (data['bbox']) { // Bounding box view if ((typeof(tmJSON['projection-bounds']) != 'undefined') && data['bbox']['ne'] && data['bbox']['sw'] && data['bbox']['ne']['lat'] && data['bbox']['ne']['lng'] && data['bbox']['sw']['lat'] && data['bbox']['sw']['lng']) { newView = computeViewLatLngFit(data); } else if (data['bbox']['xmin'] && data['bbox']['xmax'] && data['bbox']['ymin'] && data['bbox']['ymax']) { newView = computeViewFit(data); } } setTargetView(newView); } this.setNewView = _setNewView; var _setViewFromProjectionViewStr = function (viewstr) { var a = viewstr.split(","); var view = _getProjection().latlngToPoint({"lat": a[0] - 0, "lng": a[1] - 0}); var zoom = a[2] - 0; view.scale = Math.pow(2, zoom) * _homeView().scale; setTargetView(view); }; this.setViewFromProjectionViewStr = _setViewFromProjectionViewStr; var _setViewFromPointsViewStr = function (viewstr) { var a = viewstr.split(","); var view = {'x': a[0], 'y': a[1]}; var zoom = a[2] - 0; view.scale = Math.pow(2, zoom) * _homeView().scale; setTargetView(view); }; this.setViewFromPointsViewStr = _setViewFromPointsViewStr; var _setViewFromViewStr = function (viewstr) { if (typeof(tmJSON['projection-bounds']) != 'undefined') { return _setViewFromProjectionViewStr(viewstr); } else { return _setViewFromPointsViewStr(viewstr); } } this.setViewFromViewStr = _setViewFromViewStr; var _shareView = function () { $("#" + viewerDivId + " .shareurl").val(window.location.href.split("#")[0] + '#v=' + _getViewStr() + '&t=' + timelapse.getCurrentTime().toFixed(2)); $("#" + viewerDivId + " .shareurl").focus(function(){$(this).select();}); $("#" + viewerDivId + " .shareurl").click(function(){$(this).select();}); $("#" + viewerDivId + " .shareurl").mouseup(function(e){e.preventDefault();}); $("#" + viewerDivId + " .shareView").dialog("open"); }; this.shareView = _shareView; /////////////////////////// // Timelapse video control // //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Public methods // var _isPaused = function () { return videoset.isPaused(); }; this.isPaused = _isPaused; var _pause = function () { window.clearTimeout(customPlaybackTimeout); videoset.pause(); }; this.pause = _pause; var _seek = function (t) { videoset.seek(Math.min(Math.max(0,t),timelapseDurationInSeconds)); }; this.seek = _seek; this.setPlaybackRate = function (rate) { videoset.setPlaybackRate(rate); }; this.getPlaybackRate = function () { return videoset.getPlaybackRate(); }; this.getVideoPosition = function () { return videoset.getVideoPosition(); }; function updateCustomPlayback() { /* Startup custom playback stuff if possible */ if(loopPlayback && customLoopPlaybackRates) { var nextSegment = null; /* next segment with custom playback */ for(var i in customLoopPlaybackRates) { var rateObj = customLoopPlaybackRates[i]; if(timelapseCurrentTimeInSeconds < rateObj.end) { if(timelapseCurrentTimeInSeconds >= rateObj.start) { nextSegment = rateObj; break; } else if(nextSegment === null || rateObj.start < nextSegment.start) { nextSegment = rateObj; } } } if(nextSegment === null) { /* Make sure playback rate matches selection */ thisObj.setPlaybackRate(playbackRate); } else { var difference = nextSegment.start - timelapseCurrentTimeInSeconds; if(difference > 0) customPlaybackTimeout = window.setTimeout(updateCustomPlayback, difference); else { thisObj.setPlaybackRate(nextSegment.rate); customPlaybackTimeout = window.setTimeout(updateCustomPlayback, difference); } } } } var _play = function () { updateCustomPlayback(); videoset.play(); }; this.play = _play; this.setStatusLoggingEnabled = function (enable) { videoset.setStatusLoggingEnabled(enable); }; this.setNativeVideoControlsEnabled = function (enable) { videoset.setNativeVideoControlsEnabled(enable); }; var _getNumFrames = function () { return frames; }; this.getNumFrames = _getNumFrames; var _getFps = function () { return videoset.getFps(); }; this.getFps = _getFps; this.getVideoWidth = function () { return videoWidth; }; this.getVideoHeight = function () { return videoHeight; }; this.getWidth = function () { return panoWidth; }; this.getHeight = function () { return panoHeight; }; this.getMetadata = function () { return metadata; } var _addTimeChangeListener = function (listener) { videoset.addEventListener('sync', listener); }; this.addTimeChangeListener = _addTimeChangeListener; var _removeTimeChangeListener = function (listener) { videoset.removeEventListener('sync', listener); }; this.removeTimeChangeListener = _removeTimeChangeListener; this.getCurrentTime = function () { return videoset.getCurrentTime(); }; this.setScale = function (val) { targetView.scale = val; setTargetView(targetView); }; this.setScaleFromSlider = function (val) { targetView.scale = _zoomSliderToViewScale(val); setTargetView(targetView); }; var _getMinScale = function () { return _homeView().scale * .5; }; this.getMinScale = _getMinScale; var _getMaxScale = function () { return 2; }; this.getMaxScale = _getMaxScale; this.getDefaultScale = function () { return _homeView().scale; }; this.updateDimensions = function () { readVideoDivSize(); }; var _viewScaleToZoomSlider = function (value) { var tmpValue = Math.sqrt((value - _getMinScale()) / (_getMaxScale() - _getMinScale())); return (1 / (Math.log(2))) * (Math.log(tmpValue + 1)); }; this.viewScaleToZoomSlider = _viewScaleToZoomSlider; var _zoomSliderToViewScale = function (value) { return _getMaxScale() * (Math.pow((Math.pow(2, value) - 1), 2)) - Math.pow(4, value) * _getMinScale() + 2 * Math.pow(2, value) * _getMinScale(); }; this.zoomSliderToViewScale = _zoomSliderToViewScale; var _getDatasetJSON = function () { return datasetJSON; } this.getDatasetJSON = _getDatasetJSON; //////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Private methods // var handleMousedownEvent = function (event) { if (event.which != 1) return; var mouseIsDown = true; var lastEvent = event; var saveMouseMove = document.onmousemove; var saveMouseUp = document.onmouseup; videoDiv.style.cursor = 'url("css/cursors/closedhand.png") 10 10, move'; document.onmousemove = function (event) { if (mouseIsDown) { targetView.x += (lastEvent.pageX - event.pageX) / view.scale; targetView.y += (lastEvent.pageY - event.pageY) / view.scale; setTargetView(targetView); lastEvent = event; } return false; }; $("body").bind("mouseup mouseleave", function(event){ mouseIsDown = false; videoDiv.style.cursor = 'url("css/cursors/openhand.png") 10 10, move'; document.onmousemove = saveMouseMove; document.onmouseup = saveMouseUp; }); return false; }; var zoomAbout = function (zoom, x, y) { var newScale = limitScale(targetView.scale * zoom); var actualZoom = newScale / targetView.scale; targetView.x += 1 * (1 - 1 / actualZoom) * (x - $(videoDiv).offset().left - viewportWidth * .5) / targetView.scale; //depends on jquery targetView.y += 1 * (1 - 1 / actualZoom) * (y - $(videoDiv).offset().top - viewportHeight * .5) / targetView.scale; //depends on jquery targetView.scale = newScale; setTargetView(targetView); }; var handleDoubleClickEvent = function (event) { zoomAbout(2.0, event.pageX, event.pageY); }; var limitScale = function (scale) { return Math.max(_getMinScale(), Math.min(_getMaxScale(), scale)); }; var view2string = function (view) { return "[view x:" + view.x + " y:" + view.y + " scale:" + view.scale + "]"; }; var setTargetView = function (newView) { var tempView = {}; tempView.scale = limitScale(newView.scale); tempView.x = Math.max(0, Math.min(panoWidth, newView.x)); tempView.y = Math.max(0, Math.min(panoHeight, newView.y)); targetView.x = tempView.x; targetView.y = tempView.y; targetView.scale = tempView.scale; if (animateInterval == null) { animateInterval = setInterval(animate, 80); // 12.5 hz lastAnimationTime = UTIL.getCurrentTimeInSecs(); } refresh(); for (var i = 0; i < targetViewChangeListeners.length; i++) targetViewChangeListeners[i](targetView); }; this.setTargetView = setTargetView; var point2mag = function (point) { return Math.sqrt(point.x * point.x + point.y * point.y); }; var point2sub = function (a, b) { return { x: a.x - b.x, y: a.y - b.y }; }; var point2scale = function (point, scale) { return { x: point.x * scale, y: point.y * scale }; }; var log2 = function (x) { return Math.log(x) / Math.log(2); }; var exp2 = function (x) { return Math.pow(2, x); }; var animate = function () { // Compute deltaT between this animation frame and last var now = UTIL.getCurrentTimeInSecs(); var deltaT = now - lastAnimationTime; if (deltaT < .001) deltaT = .001; if (deltaT > .2) deltaT = .2; lastAnimationTime = now; var viewChanged = false; // Animate translation var minTranslateSpeed = minTranslateSpeedPixelsPerSecond / view.scale; // convert to pano coords / sec var minTranslateDelta = minTranslateSpeed * deltaT; var translateFraction = Math.min(.5, translateFractionPerSecond * deltaT); var toGoal = point2sub(targetView, view); var toGoalMag = point2mag(toGoal); if (toGoalMag > 0) { var translateDelta; if (toGoalMag * translateFraction > minTranslateDelta) { translateDelta = point2scale(toGoal, translateFraction); //UTIL.log("translating by fraction " + translateFraction + ", mag " + point2mag(translateDelta)); } else if (toGoalMag > minTranslateDelta) { translateDelta = point2scale(toGoal, minTranslateDelta / toGoalMag); //UTIL.log("translating by min delta " + minTranslateDelta + ", mag " + point2mag(translateDelta)); } else { translateDelta = toGoal; //UTIL.log("translating full amount " + point2mag(translateDelta)); } view.x += translateDelta.x; view.y += translateDelta.y; viewChanged = true; } // Animate scale var minZoomSpeed = minZoomSpeedPerSecond; var minZoomDelta = minZoomSpeed * deltaT; var zoomFraction = zoomFractionPerSecond * deltaT; if (targetView.scale != view.scale) { var zoomDelta; var toGoal = log2(targetView.scale) - log2(view.scale); if (Math.abs(toGoal) * zoomFraction > minZoomDelta) { view.scale = exp2(log2(view.scale) + toGoal * translateFraction); } else if (Math.abs(toGoal) > minZoomDelta) { view.scale = exp2(log2(view.scale) + toGoal * minZoomDelta / Math.abs(toGoal)); } else { view.scale = targetView.scale; } viewChanged = true; } if (!viewChanged) { //UTIL.log("animation finished, clearing interval"); clearInterval(animateInterval); animateInterval = null; } else { refresh(); } }; //bounding box fit var computeViewFit = function (bbox) { if (typeof(bbox['bbox']) != 'undefined') bbox = bbox['bbox']; var scale = Math.min(viewportWidth / (bbox.xmax - bbox.xmin), viewportHeight / (bbox.ymax - bbox.ymin)); return { x: .5 * (bbox.xmin + bbox.xmax), y: .5 * (bbox.ymin + bbox.ymax), scale: scale }; }; //bounding box lat/lng fit var computeViewLatLngFit = function (view) { var projection = _getProjection(); var a = projection.latlngToPoint({"lat": view['bbox']['ne']['lng'], "lng": view['bbox']['sw']['lng']}); var b = projection.latlngToPoint({"lat": view['bbox']['ne']['lat'], "lng": view['bbox']['sw']['lat']}); var scale = Math.min(viewportWidth / (b.x - a.x), viewportHeight / (a.y - b.y)); return { x: .5 * (a.x + b.x), y: .5 * (a.y + b.y), scale: scale }; }; //point center var computeViewPointCenter = function (view) { var zoom = org.gigapan.Util.isNumber(view["zoom"]) ? view["zoom"] : 0; var scale = Math.pow(2, zoom) * _homeView().scale; return { x: view["center"].x, y: view["center"].y, scale: scale }; }; //latlng center var computeViewLatLngCenter = function (view) { var projection = _getProjection(); var zoom = org.gigapan.Util.isNumber(view["zoom"]) ? view["zoom"] : 0; var point = projection.latlngToPoint({"lat": view["center"]["lat"], "lng": view["center"]["lng"]}); var scale = Math.pow(2, zoom) * _homeView().scale; return { x: point.x, y: point.y, scale: scale }; }; var computeBoundingBox = function (theView) { var halfWidth = .5 * viewportWidth / theView.scale; var halfHeight = .5 * viewportHeight / theView.scale; return { xmin: theView.x - halfWidth, xmax: theView.x + halfWidth, ymin: theView.y - halfHeight, ymax: theView.y + halfHeight }; }; var onPanoLoadSuccessCallback = function (data, desiredView) { UTIL.log('onPanoLoadSuccessCallback(' + JSON.stringify(data) + ', ' + view + ', ' + ')'); isSplitVideo = 'frames_per_fragment' in data; framesPerFragment = isSplitVideo ? data['frames_per_fragment'] : data['frames']; secondsPerFragment = isSplitVideo ? framesPerFragment / data['fps'] : 1 / data['fps'] * (data['frames'] - 1); UTIL.log("isSplitVideo=[" + isSplitVideo + "], framesPerFragment=[" + framesPerFragment + "], secondsPerFragment=[" + secondsPerFragment + "]"); panoWidth = data['width']; panoHeight = data['height']; tileWidth = data['tile_width']; tileHeight = data['tile_height']; videoWidth = data['video_width']; videoHeight = data['video_height']; videoset.setFps(data['fps']); videoset.setDuration(1 / data['fps'] * (data['frames'] )); videoset.setLeader(data['leader'] / data['fps']); videoset.setIsSplitVideo(isSplitVideo); videoset.setSecondsPerFragment(secondsPerFragment); frames = data['frames']; maxLevel = data['nlevels'] - 1; levelInfo = data['level_info']; metadata = data; readVideoDivSize(); _warpTo(typeof desiredView != 'undefined' && desiredView ? desiredView : _homeView()); }; var readVideoDivSize = function () { viewportWidth = $(videoDiv).width(); // depends on jQuery viewportHeight = $(videoDiv).height(); // depends on jQuery }; var refresh = function () { if (!isFinite(view.scale)) return; var bestIdx = computeBestVideo(targetView); if (bestIdx != currentIdx) { currentVideo = addTileidx(bestIdx, currentVideo); currentIdx = bestIdx; } var activeVideos = videoset.getActiveVideos(); for (var key in activeVideos) { var video = activeVideos[key]; repositionVideo(video); } }; var needFirstAncestor = function (tileidx) { //UTIL.log("need ancestor for " + dumpTileidx(tileidx)); var a = tileidx; while (a) { a = getTileidxParent(a); //UTIL.log("checking " + dumpTileidx(a) + ": present=" + !!tiles[a] + ", ready=" + (tiles[a]?tiles[a].video.ready:"n/a")); if (tiles[a] && tiles[a].video.ready) { tiles[a].needed = true; //UTIL.log("need ancestor " + dumpTileidx(tileidx) + ": " + dumpTileidx(a)); return; } } //UTIL.log("need ancestor " + dumpTileidx(tileidx) + ": none found"); }; var findFirstNeededAncestor = function (tileidx) { var a = tileidx; while (a) { a = getTileidxParent(a); if (tiles[a] && tiles[a].needed) return a; } return false; }; var addTileidx = function (tileidx, videoToUnload) { var url = getTileidxUrl(tileidx); var geom = tileidxGeometry(tileidx); //UTIL.log("adding tile " + dumpTileidx(tileidx) + " from " + url + " and geom = (left:" + geom['left'] + " ,top:" + geom['top'] + ", width:" + geom['width'] + ", height:" + geom['height'] + ")"); var video = videoset.addVideo(url, geom); video.tileidx = tileidx; ////console.log("ADDTILEIDX"); ////console.log(video.tileidx); return video; }; var deleteTileidx = function (tileidx) { var tile = tiles[tileidx]; if (!tile) { UTIL.error('deleteTileidx(' + dumpTileidx(tileidx) + '): not loaded'); return; } UTIL.log("removing tile " + dumpTileidx(tileidx) + " ready=" + tile.video.ready); videoset.deleteVideo(tile.video); delete tiles[tileidx]; }; var getTileidxUrl = function (tileidx) { //var shardIndex = (getTileidxRow(tileidx) % 2) * 2 + (getTileidxColumn(tileidx) % 2); //var urlPrefix = url.replace("//", "//t" + shardIndex + "."); var fragmentSpecifier = isSplitVideo ? "_" + videoset.getFragment(videoset.getCurrentTime()) : ""; return datasetPath + getTileidxLevel(tileidx) + "/" + getTileidxRow(tileidx) + "/" + getTileidxColumn(tileidx) + fragmentSpecifier + ".mp4"; }; var computeBestVideo = function (theView) { //UTIL.log("computeBestVideo " + view2string(theView)); var level = scale2level(view.scale); var levelScale = Math.pow(2, maxLevel - level); var col = Math.round((theView.x - (videoWidth * levelScale * .5)) / (tileWidth * levelScale)); col = Math.max(col, 0); col = Math.min(col, levelInfo[level].cols - 1); var row = Math.round((theView.y - (videoHeight * levelScale * .5)) / (tileHeight * levelScale)); row = Math.max(row, 0); row = Math.min(row, levelInfo[level].rows - 1); //UTIL.log("computeBestVideo l=" + level + ", c=" + col + ", r=" + row); return tileidxCreate(level, col, row); }; var scale2level = function (scale) { // Minimum level is 0, which has one tile // Maximum level is maxLevel, which is displayed 1:1 at scale=1 var idealLevel = Math.log(scale) / Math.log(2) + maxLevel; var selectedLevel = Math.floor(idealLevel + levelThreshold); selectedLevel = Math.max(selectedLevel, 0); selectedLevel = Math.min(selectedLevel, maxLevel); //UTIL.log('scale2level('+scale+'): idealLevel='+idealLevel+', ret='+selectedLevel); return selectedLevel; }; var tileidxGeometry = function (tileidx) { var levelScale = Math.pow(2, maxLevel - getTileidxLevel(tileidx)); // Calculate left, right, top, bottom, rounding to nearest pixel; avoid gaps between tiles. var left = view.scale * (getTileidxColumn(tileidx) * tileWidth * levelScale - view.x) + viewportWidth * .5; var right = Math.round(left + view.scale * levelScale * videoWidth); left = Math.round(left); var top = view.scale * (getTileidxRow(tileidx) * tileHeight * levelScale - view.y) + viewportHeight * .5; var bottom = Math.round(top + view.scale * levelScale * videoHeight); top = Math.round(top); return { left : left, top: top, width: (right - left), height: (bottom - top) }; }; var repositionVideo = function (video) { videoset.repositionVideo(video, tileidxGeometry(video.tileidx)); }; this.writeStatusToLog = function () { videoset.writeStatusToLog(); }; this.getTiles = function () { return tiles; }; /////////////////////////// // Tile index // // Represent tile coord as a 31-bit integer so we can use it as an index // l:4 (0-15) r:13 (0-8191) c:14 (0-16383) // 31-bit representation // var tileidxCreate = function (l, c, r) { return (l << 27) + (r << 14) + c; }; var getTileidxLevel = function (t) { return t >> 27; }; var getTileidxRow = function (t) { return 8191 & (t >> 14); }; var getTileidxColumn = function (t) { return 16383 & t; }; var getTileidxParent = function (t) { return tileidxCreate(getTileidxLevel(t) - 1, getTileidxColumn(t) >> 1, getTileidxRow(t) >> 1); }; var dumpTileidx = function (t) { return "{l:" + getTileidxLevel(t) + ",c:" + getTileidxColumn(t) + ",r:" + getTileidxRow(t) + "}"; }; function createTimelineSlider(div) { if (tmJSON["capture-times"]) { captureTimes = tmJSON["capture-times"]; } else { for (var i = 0; i < _getNumFrames(); i++) { captureTimes.push("--") } } timelapseDurationInSeconds = (_getNumFrames() - 0.7) / _getFps(); $("#" + div + " .currentTime").html(org.gigapan.Util.formatTime(timelapseCurrentTimeInSeconds, true)); $("#" + div + " .totalTime").html(org.gigapan.Util.formatTime(timelapseDurationInSeconds, true)); $("#" + div + " .currentCaptureTime").html(org.gigapan.Util.htmlForTextWithEmbeddedNewlines(captureTimes[timelapseCurrentCaptureTimeIndex])); $("#" + div + " .timelineSlider").slider({ min: 0, max: _getNumFrames() - 1, // this way the time scrubber goes exactly to the end of timeline range: "min", step: 1, slide: function (e, ui) { // $(this).slider('value') --> previous value // ui.value --> current value // If we are manually using the slider and we are pulling it back to the start // we wont actually get to time 0 because of how we are snapping. // Manually seek to position 0 when this happens. if (($(this).slider('value') > ui.value) && ui.value == 0) _seek(0); else _seek((ui.value + 0.3) / _getFps()); } }); $("#" + div + " .timelineSlider .ui-slider-handle").attr("title", "Drag to go to a different point in time"); } function createPlaybackSpeedSlider(div, timelapseObj) { //populate playback speed dropdown populateSpeedPlaybackChoices(div, timelapseObj); var speedChoice; // set the playback speed dropdown $("#" + div + " .playbackSpeedChoices li a").each(function () { speedChoice = $(this); if (speedChoice.attr("data-speed") == playbackSpeed) return false; }); speedChoice.addClass("current"); $("#" + div + " .playbackSpeedText").text(speedChoice.text()); $("#" + div + " .playbackSpeedChoices li a").bind("click", function () { changePlaybackRate(this); }); } function changePlaybackRate(obj) { var rate = $(obj).attr("data-speed") - 0; //convert to number thisObj.setPlaybackRate(rate); playbackSpeed = rate; $("#" + viewerDivId + " li.playbackSpeedOptions").hide(); $("#" + viewerDivId + " li.playbackSpeedOptions a").removeClass("current"); $(obj).addClass("current"); $("#" + viewerDivId + " .playbackSpeedText").text($(obj).text()); } function populateSizes(viewerDivId) { // populate the player size dropdown var html = ""; var numSizes = tmJSON["sizes"].length; for (var i = 0; i < numSizes; i++) { html += '