diff --git a/dist/LeafletPlayback.js b/dist/LeafletPlayback.js index 1d6b9ae..1205e43 100644 --- a/dist/LeafletPlayback.js +++ b/dist/LeafletPlayback.js @@ -198,135 +198,31 @@ L.Playback.MoveableMarker = L.Marker.extend({ }); L.Playback = L.Playback || {}; - - - +function search(o, v, i){ + var h = o.length, l = -1, m; + while(h - l > 1) + if(o[m = h + l >> 1] < v) l = m; + else h = m; + return o[h] != v ? i ? h : -1 : h; +} L.Playback.Track = L.Class.extend({ initialize : function (geoJSON, options) { options = options || {}; var tickLen = options.tickLen || 250; + var sampleTimes = geoJSON.properties.time; + var samples = geoJSON.geometry.coordinates; this._staleTime = options.staleTime || 60*60*1000; this._fadeMarkersWhenStale = options.fadeMarkersWhenStale || false; this._geoJSON = geoJSON; this._tickLen = tickLen; - this._ticks = []; + this._ticks = samples; + this._times = sampleTimes; this._marker = null; - this._orientations = []; - - var sampleTimes = geoJSON.properties.time; - this._orientIcon = options.orientIcons; - var previousOrientation; - - var samples = geoJSON.geometry.coordinates; - var currSample = samples[0]; - var nextSample = samples[1]; - - var currSampleTime = sampleTimes[0]; - var t = currSampleTime; // t is used to iterate through tick times - var nextSampleTime = sampleTimes[1]; - var tmod = t % tickLen; // ms past a tick time - var rem, - ratio; - - // handle edge case of only one t sample - if (sampleTimes.length === 1) { - if (tmod !== 0) - t += tickLen - tmod; - this._ticks[t] = samples[0]; - this._orientations[t] = 0; - this._startTime = t; - this._endTime = t; - return; - } - - // interpolate first tick if t not a tick time - if (tmod !== 0) { - rem = tickLen - tmod; - ratio = rem / (nextSampleTime - currSampleTime); - t += rem; - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._ticks[t] = currSample; - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } - - this._startTime = t; - t += tickLen; - while (t < nextSampleTime) { - ratio = (t - currSampleTime) / (nextSampleTime - currSampleTime); - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - t += tickLen; - } - - // iterating through the rest of the samples - for (var i = 1, len = samples.length; i < len; i++) { - currSample = samples[i]; - nextSample = samples[i + 1]; - t = currSampleTime = sampleTimes[i]; - nextSampleTime = sampleTimes[i + 1]; - - tmod = t % tickLen; - if (tmod !== 0 && nextSampleTime) { - rem = tickLen - tmod; - ratio = rem / (nextSampleTime - currSampleTime); - t += rem; - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - if(nextSample){ - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } else { - this._ticks[t] = currSample; - if(nextSample){ - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } - - t += tickLen; - while (t < nextSampleTime) { - ratio = (t - currSampleTime) / (nextSampleTime - currSampleTime); - - if (nextSampleTime - currSampleTime > options.maxInterpolationTime){ - this._ticks[t] = currSample; - - if(nextSample){ - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } - else { - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - if(nextSample) { - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } - - t += tickLen; - } - } - - // the last t in the while would be past bounds - this._endTime = t - tickLen; - this._lastTick = this._ticks[this._endTime]; - + this._startTime = sampleTimes[0]; + this._endTime = sampleTimes[sampleTimes.length - 1]; }, _interpolatePoint : function (start, end, ratio) { @@ -373,11 +269,11 @@ L.Playback.Track = L.Class.extend({ }, getFirstTick : function () { - return this._ticks[this._startTime]; + return this._ticks[0]; }, getLastTick : function () { - return this._ticks[this._endTime]; + return this._ticks[this._ticks.length - 1]; }, getStartTime : function () { @@ -389,26 +285,7 @@ L.Playback.Track = L.Class.extend({ }, getTickMultiPoint : function () { - var t = this.getStartTime(); - var endT = this.getEndTime(); - var coordinates = []; - var time = []; - while (t <= endT) { - time.push(t); - coordinates.push(this.tick(t)); - t += this._tickLen; - } - - return { - type : 'Feature', - geometry : { - type : 'MultiPoint', - coordinates : coordinates - }, - properties : { - time : time - } - }; + return this._geoJSON; }, trackPresentAtTick : function(timestamp) @@ -422,21 +299,43 @@ L.Playback.Track = L.Class.extend({ }, tick : function (timestamp) { - if (timestamp > this._endTime) - timestamp = this._endTime; - if (timestamp < this._startTime) - timestamp = this._startTime; - return this._ticks[timestamp]; + if (timestamp >= this._endTime) + return this.getLastTick(); + if (timestamp <= this._startTime) + return this.getFirstTick(); + + var samples = this._ticks; + + var times = this._times; + + var index = search(times, timestamp, true); + + var currSampleTime = times[index - 1], + nextSampleTime = times[index], + currSample = samples[index - 1], + nextSample = samples[index]; + + if (nextSampleTime === timestamp) { + return nextSample; + } + + var ratio = (timestamp - currSampleTime) / (nextSampleTime - currSampleTime); + + return this._interpolatePoint(currSample, nextSample, ratio); }, courseAtTime: function(timestamp) { - //return 90; - if (timestamp > this._endTime) - timestamp = this._endTime; - if (timestamp < this._startTime) - timestamp = this._startTime; - return this._orientations[timestamp]; + var samples = this._ticks; + + var times = this._times; + + var index = search(times, timestamp, true); + + var currSample = samples[index - 1], + nextSample = samples[index]; + + return currSample && nextSample ? this._directionOfPoint(currSample,nextSample) : 0; }, setMarker : function(timestamp, options){ @@ -677,10 +576,6 @@ L.Playback.Clock = L.Class.extend({ setCursor: function (ms) { var time = parseInt(ms); if (!time) return; - var mod = time % this._tickLen; - if (mod !== 0) { - time += this._tickLen - mod; - } this._cursor = time; this._trackController.tock(this._cursor, 0); this._callbacks(this._cursor); diff --git a/dist/LeafletPlayback.min.js b/dist/LeafletPlayback.min.js index 4f7edad..354fb26 100644 --- a/dist/LeafletPlayback.min.js +++ b/dist/LeafletPlayback.min.js @@ -1 +1 @@ -!function(a){var b;if("function"==typeof define&&define.amd)define(["leaflet"],a);else if("object"==typeof module&&"object"==typeof module.exports)b=require("leaflet"),module.exports=a(b);else{if("undefined"==typeof window.L)throw"Leaflet must be loaded first";a(window.L)}}(function(a){return a.Playback=a.Playback||{},a.Playback.Util=a.Class.extend({statics:{DateStr:function(a){return new Date(a).toDateString()},TimeStr:function(a){var b=new Date(a),c=b.getHours(),d=b.getMinutes(),e=b.getSeconds(),f=a/1e3,g=(f-Math.floor(f)).toFixed(2).slice(1),h="AM";return c>11&&(c%=12,h="PM"),0===c&&(c=12),10>d&&(d="0"+d),10>e&&(e="0"+e),c+":"+d+":"+e+g+" "+h},ParseGPX:function(a){for(var b={type:"Feature",geometry:{type:"MultiPoint",coordinates:[]},properties:{time:[],speed:[],altitude:[]},bbox:[]},c=$.parseXML(a),d=$(c).find("trkpt"),e=0,f=d.length;f>e;e++){var g=d[e],h=parseFloat(g.getAttribute("lat")),i=parseFloat(g.getAttribute("lon")),j=$(g).find("time").text(),k=$(g).find("ele").text(),l=new Date(j).getTime(),m=parseFloat(k),n=b.geometry.coordinates,o=b.properties,p=o.time,q=b.properties.altitude;n.push([i,h]),p.push(l),q.push(m)}return b}}}),a.Playback=a.Playback||{},a.Playback.MoveableMarker=a.Marker.extend({initialize:function(b,c,d){var e=c.marker||{};jQuery.isFunction(e)&&(e=e(d)),a.Marker.prototype.initialize.call(this,b,e),this.popupContent="",this.feature=d,e.getPopup&&(this.popupContent=e.getPopup(d)),c.popups&&this.bindPopup(this.getPopupContent()+b.toString()),c.labels&&(this.bindLabel?this.bindLabel(this.getPopupContent()):console.log("Label binding requires leaflet-label (https://github.com/Leaflet/Leaflet.label)"))},getPopupContent:function(){return""!==this.popupContent?""+this.popupContent+"
":""},move:function(b,c){a.DomUtil.TRANSITION&&(this._icon&&(this._icon.style[a.DomUtil.TRANSITION]="all "+c+"ms linear",this._popup&&this._popup._wrapper&&(this._popup._wrapper.style[a.DomUtil.TRANSITION]="all "+c+"ms linear")),this._shadow&&(this._shadow.style[a.DomUtil.TRANSITION]="all "+c+"ms linear")),this.setLatLng(b),this._popup&&this._popup.setContent(this.getPopupContent()+this._latlng.toString())},_old__setPos:a.Marker.prototype._setPos,_updateImg:function(b,c,d){c=a.point(d).divideBy(2)._subtract(a.point(c));var e="";e+=" translate("+-c.x+"px, "+-c.y+"px)",e+=" rotate("+this.options.iconAngle+"deg)",e+=" translate("+c.x+"px, "+c.y+"px)",b.style[a.DomUtil.TRANSFORM]+=e},setIconAngle:function(a){this.options.iconAngle=a,this._map&&this.update()},_setPos:function(b){if(this._icon&&(this._icon.style[a.DomUtil.TRANSFORM]=""),this._shadow&&(this._shadow.style[a.DomUtil.TRANSFORM]=""),this._old__setPos.apply(this,[b]),this.options.iconAngle){var c,d=this.options.icon.options.iconAnchor,e=this.options.icon.options.iconSize;this._icon&&(c=this._icon,this._updateImg(c,d,e)),this._shadow&&(e=this.options.icon.options.shadowSize,c=this._shadow,this._updateImg(c,d,e))}}}),a.Playback=a.Playback||{},a.Playback.Track=a.Class.extend({initialize:function(a,b){b=b||{};var c=b.tickLen||250;this._staleTime=b.staleTime||36e5,this._fadeMarkersWhenStale=b.fadeMarkersWhenStale||!1,this._geoJSON=a,this._tickLen=c,this._ticks=[],this._marker=null,this._orientations=[];var d=a.properties.time;this._orientIcon=b.orientIcons;var e,f,g,h=a.geometry.coordinates,i=h[0],j=h[1],k=d[0],l=k,m=d[1],n=l%c;if(1===d.length)return 0!==n&&(l+=c-n),this._ticks[l]=h[0],this._orientations[l]=0,this._startTime=l,void(this._endTime=l);for(0!==n?(f=c-n,g=f/(m-k),l+=f,this._ticks[l]=this._interpolatePoint(i,j,g),this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):(this._ticks[l]=i,this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]),this._startTime=l,l+=c;m>l;)g=(l-k)/(m-k),this._ticks[l]=this._interpolatePoint(i,j,g),this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l],l+=c;for(var o=1,p=h.length;p>o;o++)for(i=h[o],j=h[o+1],l=k=d[o],m=d[o+1],n=l%c,0!==n&&m?(f=c-n,g=f/(m-k),l+=f,this._ticks[l]=this._interpolatePoint(i,j,g),j?(this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):this._orientations[l]=e):(this._ticks[l]=i,j?(this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):this._orientations[l]=e),l+=c;m>l;)g=(l-k)/(m-k),m-k>b.maxInterpolationTime?(this._ticks[l]=i,j?(this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):this._orientations[l]=e):(this._ticks[l]=this._interpolatePoint(i,j,g),j?(this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):this._orientations[l]=e),l+=c;this._endTime=l-c,this._lastTick=this._ticks[this._endTime]},_interpolatePoint:function(a,b,c){try{var d=[b[0]-a[0],b[1]-a[1]],e=[d[0]*c,d[1]*c];return[a[0]+e[0],a[1]+e[1]]}catch(f){console.log("err: cant interpolate a point"),console.log(["start",a]),console.log(["end",b]),console.log(["ratio",c])}},_directionOfPoint:function(a,b){return this._getBearing(a[1],a[0],b[1],b[0])},_getBearing:function(a,b,c,d){a=this._radians(a),b=this._radians(b),c=this._radians(c),d=this._radians(d);var e=d-b,f=Math.log(Math.tan(c/2+Math.PI/4)/Math.tan(a/2+Math.PI/4));return Math.abs(e)>Math.PI&&(e=e>0?-(2*Math.PI-e):2*Math.PI+e),(this._degrees(Math.atan2(e,f))+360)%360},_radians:function(a){return a*(Math.PI/180)},_degrees:function(a){return a*(180/Math.PI)},getFirstTick:function(){return this._ticks[this._startTime]},getLastTick:function(){return this._ticks[this._endTime]},getStartTime:function(){return this._startTime},getEndTime:function(){return this._endTime},getTickMultiPoint:function(){for(var a=this.getStartTime(),b=this.getEndTime(),c=[],d=[];b>=a;)d.push(a),c.push(this.tick(a)),a+=this._tickLen;return{type:"Feature",geometry:{type:"MultiPoint",coordinates:c},properties:{time:d}}},trackPresentAtTick:function(a){return a>=this._startTime},trackStaleAtTick:function(a){return this._endTime+this._staleTime<=a},tick:function(a){return a>this._endTime&&(a=this._endTime),athis._endTime&&(a=this._endTime),a0;){var a=this._tracks.pop(),b=a.getMarker();b&&this._map.removeLayer(b)}},setTracks:function(a){this.clearTracks(),this.addTracks(a)},addTracks:function(a){if(a)if(a instanceof Array)for(var b=0,c=a.length;c>b;b++)this.addTrack(a[b]);else this.addTrack(a)},addTrack:function(a,b){if(a){var c=a.setMarker(b,this.options);c&&(c.addTo(this._map),this._tracks.push(a))}},tock:function(b,c){for(var d=0,e=this._tracks.length;e>d;d++){var f=this._tracks[d].tick(b),g=new a.LatLng(f[1],f[0]);this._tracks[d].moveMarker(g,c,b)}},getStartTime:function(){var a=0;if(this._tracks.length>0){a=this._tracks[0].getStartTime();for(var b=1,c=this._tracks.length;c>b;b++){var d=this._tracks[b].getStartTime();a>d&&(a=d)}}return a},getEndTime:function(){var a=0;if(this._tracks.length>0){a=this._tracks[0].getEndTime();for(var b=1,c=this._tracks.length;c>b;b++){var d=this._tracks[b].getEndTime();d>a&&(a=d)}}return a},getTracks:function(){return this._tracks}}),a.Playback=a.Playback||{},a.Playback.Clock=a.Class.extend({initialize:function(b,c,d){this._trackController=b,this._callbacksArry=[],c&&this.addCallback(c),a.setOptions(this,d),this._speed=this.options.speed,this._tickLen=this.options.tickLen,this._cursor=b.getStartTime(),this._transitionTime=this._tickLen/this._speed},_tick:function(a){return a._cursor>a._trackController.getEndTime()?void clearInterval(a._intervalID):(a._trackController.tock(a._cursor,a._transitionTime),a._callbacks(a._cursor),void(a._cursor+=a._tickLen))},_callbacks:function(a){for(var b=this._callbacksArry,c=0,d=b.length;d>c;c++)b[c](a)},addCallback:function(a){this._callbacksArry.push(a)},start:function(){this._intervalID||(this._intervalID=window.setInterval(this._tick,this._transitionTime,this))},stop:function(){this._intervalID&&(clearInterval(this._intervalID),this._intervalID=null)},getSpeed:function(){return this._speed},isPlaying:function(){return this._intervalID?!0:!1},setSpeed:function(a){this._speed=a,this._transitionTime=this._tickLen/a,this._intervalID&&(this.stop(),this.start())},setCursor:function(a){var b=parseInt(a);if(b){var c=b%this._tickLen;0!==c&&(b+=this._tickLen-c),this._cursor=b,this._trackController.tock(this._cursor,0),this._callbacks(this._cursor)}},getTime:function(){return this._cursor},getStartTime:function(){return this._trackController.getStartTime()},getEndTime:function(){return this._trackController.getEndTime()},getTickLen:function(){return this._tickLen}}),a.Playback=a.Playback||{},a.Playback.TracksLayer=a.Class.extend({initialize:function(b,c){var d=c.layer||{};jQuery.isFunction(d)&&(d=d(feature)),d.pointToLayer||(d.pointToLayer=function(b,c){return new a.CircleMarker(c,{radius:5})}),this.layer=new a.GeoJSON(null,d);var e={"GPS Tracks":this.layer};a.control.layers(null,e,{collapsed:!1}).addTo(b)},clearLayer:function(){for(var a in this.layer._layers)this.layer.removeLayer(this.layer._layers[a])},addLayer:function(a){this.layer.addData(a)}}),a.Playback=a.Playback||{},a.Playback.DateControl=a.Control.extend({options:{position:"bottomleft",dateFormatFn:a.Playback.Util.DateStr,timeFormatFn:a.Playback.Util.TimeStr},initialize:function(b,c){a.setOptions(this,c),this.playback=b},onAdd:function(b){this._container=a.DomUtil.create("div","leaflet-control-layers leaflet-control-layers-expanded");var c=this,d=this.playback,e=d.getTime(),f=a.DomUtil.create("div","datetimeControl",this._container);return this._date=a.DomUtil.create("p","",f),this._time=a.DomUtil.create("p","",f),this._date.innerHTML=this.options.dateFormatFn(e),this._time.innerHTML=this.options.timeFormatFn(e),d.addCallback(function(a){c._date.innerHTML=c.options.dateFormatFn(a),c._time.innerHTML=c.options.timeFormatFn(a)}),this._container}}),a.Playback.PlayControl=a.Control.extend({options:{position:"bottomright"},initialize:function(a){this.playback=a},onAdd:function(b){function c(){e.isPlaying()?(e.stop(),d._button.innerHTML="Play"):(e.start(),d._button.innerHTML="Stop")}this._container=a.DomUtil.create("div","leaflet-control-layers leaflet-control-layers-expanded");var d=this,e=this.playback;e.setSpeed(100);var f=a.DomUtil.create("div","playControl",this._container);this._button=a.DomUtil.create("button","",f),this._button.innerHTML="Play";var g=a.DomEvent.stopPropagation;return a.DomEvent.on(this._button,"click",g).on(this._button,"mousedown",g).on(this._button,"dblclick",g).on(this._button,"click",a.DomEvent.preventDefault).on(this._button,"click",c,this),this._container}}),a.Playback.SliderControl=a.Control.extend({options:{position:"bottomleft"},initialize:function(a){this.playback=a},onAdd:function(b){function c(a){var b=Number(a.target.value);e.setCursor(b)}this._container=a.DomUtil.create("div","leaflet-control-layers leaflet-control-layers-expanded");var d=this,e=this.playback;this._slider=a.DomUtil.create("input","slider",this._container),this._slider.type="range",this._slider.min=e.getStartTime(),this._slider.max=e.getEndTime(),this._slider.value=e.getTime();var f=a.DomEvent.stopPropagation;return a.DomEvent.on(this._slider,"click",f).on(this._slider,"mousedown",f).on(this._slider,"dblclick",f).on(this._slider,"click",a.DomEvent.preventDefault).on(this._slider,"change",c,this).on(this._slider,"mousemove",c,this),e.addCallback(function(a){d._slider.value=a}),b.on("playback:add_tracks",function(){d._slider.min=e.getStartTime(),d._slider.max=e.getEndTime(),d._slider.value=e.getTime()}),this._container}}),a.Playback=a.Playback.Clock.extend({statics:{MoveableMarker:a.Playback.MoveableMarker,Track:a.Playback.Track,TrackController:a.Playback.TrackController,Clock:a.Playback.Clock,Util:a.Playback.Util,TracksLayer:a.Playback.TracksLayer,PlayControl:a.Playback.PlayControl,DateControl:a.Playback.DateControl,SliderControl:a.Playback.SliderControl},options:{tickLen:250,speed:1,maxInterpolationTime:3e5,tracksLayer:!0,playControl:!1,dateControl:!1,sliderControl:!1,layer:{},marker:{}},initialize:function(b,c,d,e){a.setOptions(this,e),this._map=b,this._trackController=new a.Playback.TrackController(b,null,this.options),a.Playback.Clock.prototype.initialize.call(this,this._trackController,d,this.options),this.options.tracksLayer&&(this._tracksLayer=new a.Playback.TracksLayer(b,e)),this.setData(c),this.options.playControl&&(this.playControl=new a.Playback.PlayControl(this),this.playControl.addTo(b)),this.options.sliderControl&&(this.sliderControl=new a.Playback.SliderControl(this),this.sliderControl.addTo(b)),this.options.dateControl&&(this.dateControl=new a.Playback.DateControl(this,e),this.dateControl.addTo(b))},clearData:function(){this._trackController.clearTracks(),this._tracksLayer&&this._tracksLayer.clearLayer()},setData:function(a){this.clearData(),this.addData(a,this.getTime()),this.setCursor(this.getStartTime())},addData:function(b,c){if(b){if(b instanceof Array)for(var d=0,e=b.length;e>d;d++)this._trackController.addTrack(new a.Playback.Track(b[d],this.options),c);else this._trackController.addTrack(new a.Playback.Track(b,this.options),c);this._map.fire("playback:set:data"),this.options.tracksLayer&&this._tracksLayer.addLayer(b)}},destroy:function(){this.clearData(),this.playControl&&this._map.removeControl(this.playControl),this.sliderControl&&this._map.removeControl(this.sliderControl),this.dateControl&&this._map.removeControl(this.dateControl)}}),a.Map.addInitHook(function(){this.options.playback&&(this.playback=new a.Playback(this))}),a.playback=function(b,c,d,e){return new a.Playback(b,c,d,e)},a.Playback}); \ No newline at end of file +!function(a){var b;if("function"==typeof define&&define.amd)define(["leaflet"],a);else if("object"==typeof module&&"object"==typeof module.exports)b=require("leaflet"),module.exports=a(b);else{if("undefined"==typeof window.L)throw"Leaflet must be loaded first";a(window.L)}}(function(a){function b(a,b,c){for(var d,e=a.length,f=-1;e-f>1;)a[d=e+f>>1]11&&(c%=12,h="PM"),0===c&&(c=12),10>d&&(d="0"+d),10>e&&(e="0"+e),c+":"+d+":"+e+g+" "+h},ParseGPX:function(a){for(var b={type:"Feature",geometry:{type:"MultiPoint",coordinates:[]},properties:{time:[],speed:[],altitude:[]},bbox:[]},c=$.parseXML(a),d=$(c).find("trkpt"),e=0,f=d.length;f>e;e++){var g=d[e],h=parseFloat(g.getAttribute("lat")),i=parseFloat(g.getAttribute("lon")),j=$(g).find("time").text(),k=$(g).find("ele").text(),l=new Date(j).getTime(),m=parseFloat(k),n=b.geometry.coordinates,o=b.properties,p=o.time,q=b.properties.altitude;n.push([i,h]),p.push(l),q.push(m)}return b}}}),a.Playback=a.Playback||{},a.Playback.MoveableMarker=a.Marker.extend({initialize:function(b,c,d){var e=c.marker||{};jQuery.isFunction(e)&&(e=e(d)),a.Marker.prototype.initialize.call(this,b,e),this.popupContent="",this.feature=d,e.getPopup&&(this.popupContent=e.getPopup(d)),c.popups&&this.bindPopup(this.getPopupContent()+b.toString()),c.labels&&(this.bindLabel?this.bindLabel(this.getPopupContent()):console.log("Label binding requires leaflet-label (https://github.com/Leaflet/Leaflet.label)"))},getPopupContent:function(){return""!==this.popupContent?""+this.popupContent+"
":""},move:function(b,c){a.DomUtil.TRANSITION&&(this._icon&&(this._icon.style[a.DomUtil.TRANSITION]="all "+c+"ms linear",this._popup&&this._popup._wrapper&&(this._popup._wrapper.style[a.DomUtil.TRANSITION]="all "+c+"ms linear")),this._shadow&&(this._shadow.style[a.DomUtil.TRANSITION]="all "+c+"ms linear")),this.setLatLng(b),this._popup&&this._popup.setContent(this.getPopupContent()+this._latlng.toString())},_old__setPos:a.Marker.prototype._setPos,_updateImg:function(b,c,d){c=a.point(d).divideBy(2)._subtract(a.point(c));var e="";e+=" translate("+-c.x+"px, "+-c.y+"px)",e+=" rotate("+this.options.iconAngle+"deg)",e+=" translate("+c.x+"px, "+c.y+"px)",b.style[a.DomUtil.TRANSFORM]+=e},setIconAngle:function(a){this.options.iconAngle=a,this._map&&this.update()},_setPos:function(b){if(this._icon&&(this._icon.style[a.DomUtil.TRANSFORM]=""),this._shadow&&(this._shadow.style[a.DomUtil.TRANSFORM]=""),this._old__setPos.apply(this,[b]),this.options.iconAngle){var c,d=this.options.icon.options.iconAnchor,e=this.options.icon.options.iconSize;this._icon&&(c=this._icon,this._updateImg(c,d,e)),this._shadow&&(e=this.options.icon.options.shadowSize,c=this._shadow,this._updateImg(c,d,e))}}}),a.Playback=a.Playback||{},a.Playback.Track=a.Class.extend({initialize:function(a,b){b=b||{};var c=b.tickLen||250,d=a.properties.time,e=a.geometry.coordinates;this._staleTime=b.staleTime||36e5,this._fadeMarkersWhenStale=b.fadeMarkersWhenStale||!1,this._geoJSON=a,this._tickLen=c,this._ticks=e,this._times=d,this._marker=null,this._orientIcon=b.orientIcons,this._startTime=d[0],this._endTime=d[d.length-1]},_interpolatePoint:function(a,b,c){try{var d=[b[0]-a[0],b[1]-a[1]],e=[d[0]*c,d[1]*c];return[a[0]+e[0],a[1]+e[1]]}catch(f){console.log("err: cant interpolate a point"),console.log(["start",a]),console.log(["end",b]),console.log(["ratio",c])}},_directionOfPoint:function(a,b){return this._getBearing(a[1],a[0],b[1],b[0])},_getBearing:function(a,b,c,d){a=this._radians(a),b=this._radians(b),c=this._radians(c),d=this._radians(d);var e=d-b,f=Math.log(Math.tan(c/2+Math.PI/4)/Math.tan(a/2+Math.PI/4));return Math.abs(e)>Math.PI&&(e=e>0?-(2*Math.PI-e):2*Math.PI+e),(this._degrees(Math.atan2(e,f))+360)%360},_radians:function(a){return a*(Math.PI/180)},_degrees:function(a){return a*(180/Math.PI)},getFirstTick:function(){return this._ticks[0]},getLastTick:function(){return this._ticks[this._ticks.length-1]},getStartTime:function(){return this._startTime},getEndTime:function(){return this._endTime},getTickMultiPoint:function(){return this._geoJSON},trackPresentAtTick:function(a){return a>=this._startTime},trackStaleAtTick:function(a){return this._endTime+this._staleTime<=a},tick:function(a){if(a>=this._endTime)return this.getLastTick();if(a<=this._startTime)return this.getFirstTick();var c=this._ticks,d=this._times,e=b(d,a,!0),f=d[e-1],g=d[e],h=c[e-1],i=c[e];if(g===a)return i;var j=(a-f)/(g-f);return this._interpolatePoint(h,i,j)},courseAtTime:function(a){var c=this._ticks,d=this._times,e=b(d,a,!0),f=c[e-1],g=c[e];return f&&g?this._directionOfPoint(f,g):0},setMarker:function(b,c){var d=null;if(d=b?this.tick(b):this.getFirstTick()){var e=new a.LatLng(d[1],d[0]);this._marker=new a.Playback.MoveableMarker(e,c,this._geoJSON),c.mouseOverCallback&&this._marker.on("mouseover",c.mouseOverCallback),c.clickCallback&&this._marker.on("click",c.clickCallback),this._fadeMarkersWhenStale&&!this.trackPresentAtTick(b)&&this._marker.setOpacity(0)}return this._marker},moveMarker:function(a,b,c){this._marker&&(this._fadeMarkersWhenStale&&(this.trackPresentAtTick(c)?this._marker.setOpacity(1):this._marker.setOpacity(0),this.trackStaleAtTick(c)&&this._marker.setOpacity(.25)),this._orientIcon&&this._marker.setIconAngle(this.courseAtTime(c)),this._marker.move(a,b))},getMarker:function(){return this._marker}}),a.Playback=a.Playback||{},a.Playback.TrackController=a.Class.extend({initialize:function(a,b,c){this.options=c||{},this._map=a,this._tracks=[],this.setTracks(b)},clearTracks:function(){for(;this._tracks.length>0;){var a=this._tracks.pop(),b=a.getMarker();b&&this._map.removeLayer(b)}},setTracks:function(a){this.clearTracks(),this.addTracks(a)},addTracks:function(a){if(a)if(a instanceof Array)for(var b=0,c=a.length;c>b;b++)this.addTrack(a[b]);else this.addTrack(a)},addTrack:function(a,b){if(a){var c=a.setMarker(b,this.options);c&&(c.addTo(this._map),this._tracks.push(a))}},tock:function(b,c){for(var d=0,e=this._tracks.length;e>d;d++){var f=this._tracks[d].tick(b),g=new a.LatLng(f[1],f[0]);this._tracks[d].moveMarker(g,c,b)}},getStartTime:function(){var a=0;if(this._tracks.length>0){a=this._tracks[0].getStartTime();for(var b=1,c=this._tracks.length;c>b;b++){var d=this._tracks[b].getStartTime();a>d&&(a=d)}}return a},getEndTime:function(){var a=0;if(this._tracks.length>0){a=this._tracks[0].getEndTime();for(var b=1,c=this._tracks.length;c>b;b++){var d=this._tracks[b].getEndTime();d>a&&(a=d)}}return a},getTracks:function(){return this._tracks}}),a.Playback=a.Playback||{},a.Playback.Clock=a.Class.extend({initialize:function(b,c,d){this._trackController=b,this._callbacksArry=[],c&&this.addCallback(c),a.setOptions(this,d),this._speed=this.options.speed,this._tickLen=this.options.tickLen,this._cursor=b.getStartTime(),this._transitionTime=this._tickLen/this._speed},_tick:function(a){return a._cursor>a._trackController.getEndTime()?void clearInterval(a._intervalID):(a._trackController.tock(a._cursor,a._transitionTime),a._callbacks(a._cursor),void(a._cursor+=a._tickLen))},_callbacks:function(a){for(var b=this._callbacksArry,c=0,d=b.length;d>c;c++)b[c](a)},addCallback:function(a){this._callbacksArry.push(a)},start:function(){this._intervalID||(this._intervalID=window.setInterval(this._tick,this._transitionTime,this))},stop:function(){this._intervalID&&(clearInterval(this._intervalID),this._intervalID=null)},getSpeed:function(){return this._speed},isPlaying:function(){return this._intervalID?!0:!1},setSpeed:function(a){this._speed=a,this._transitionTime=this._tickLen/a,this._intervalID&&(this.stop(),this.start())},setCursor:function(a){var b=parseInt(a);b&&(this._cursor=b,this._trackController.tock(this._cursor,0),this._callbacks(this._cursor))},getTime:function(){return this._cursor},getStartTime:function(){return this._trackController.getStartTime()},getEndTime:function(){return this._trackController.getEndTime()},getTickLen:function(){return this._tickLen}}),a.Playback=a.Playback||{},a.Playback.TracksLayer=a.Class.extend({initialize:function(b,c){var d=c.layer||{};jQuery.isFunction(d)&&(d=d(feature)),d.pointToLayer||(d.pointToLayer=function(b,c){return new a.CircleMarker(c,{radius:5})}),this.layer=new a.GeoJSON(null,d);var e={"GPS Tracks":this.layer};a.control.layers(null,e,{collapsed:!1}).addTo(b)},clearLayer:function(){for(var a in this.layer._layers)this.layer.removeLayer(this.layer._layers[a])},addLayer:function(a){this.layer.addData(a)}}),a.Playback=a.Playback||{},a.Playback.DateControl=a.Control.extend({options:{position:"bottomleft",dateFormatFn:a.Playback.Util.DateStr,timeFormatFn:a.Playback.Util.TimeStr},initialize:function(b,c){a.setOptions(this,c),this.playback=b},onAdd:function(b){this._container=a.DomUtil.create("div","leaflet-control-layers leaflet-control-layers-expanded");var c=this,d=this.playback,e=d.getTime(),f=a.DomUtil.create("div","datetimeControl",this._container);return this._date=a.DomUtil.create("p","",f),this._time=a.DomUtil.create("p","",f),this._date.innerHTML=this.options.dateFormatFn(e),this._time.innerHTML=this.options.timeFormatFn(e),d.addCallback(function(a){c._date.innerHTML=c.options.dateFormatFn(a),c._time.innerHTML=c.options.timeFormatFn(a)}),this._container}}),a.Playback.PlayControl=a.Control.extend({options:{position:"bottomright"},initialize:function(a){this.playback=a},onAdd:function(b){function c(){e.isPlaying()?(e.stop(),d._button.innerHTML="Play"):(e.start(),d._button.innerHTML="Stop")}this._container=a.DomUtil.create("div","leaflet-control-layers leaflet-control-layers-expanded");var d=this,e=this.playback;e.setSpeed(100);var f=a.DomUtil.create("div","playControl",this._container);this._button=a.DomUtil.create("button","",f),this._button.innerHTML="Play";var g=a.DomEvent.stopPropagation;return a.DomEvent.on(this._button,"click",g).on(this._button,"mousedown",g).on(this._button,"dblclick",g).on(this._button,"click",a.DomEvent.preventDefault).on(this._button,"click",c,this),this._container}}),a.Playback.SliderControl=a.Control.extend({options:{position:"bottomleft"},initialize:function(a){this.playback=a},onAdd:function(b){function c(a){var b=Number(a.target.value);e.setCursor(b)}this._container=a.DomUtil.create("div","leaflet-control-layers leaflet-control-layers-expanded");var d=this,e=this.playback;this._slider=a.DomUtil.create("input","slider",this._container),this._slider.type="range",this._slider.min=e.getStartTime(),this._slider.max=e.getEndTime(),this._slider.value=e.getTime();var f=a.DomEvent.stopPropagation;return a.DomEvent.on(this._slider,"click",f).on(this._slider,"mousedown",f).on(this._slider,"dblclick",f).on(this._slider,"click",a.DomEvent.preventDefault).on(this._slider,"change",c,this).on(this._slider,"mousemove",c,this),e.addCallback(function(a){d._slider.value=a}),b.on("playback:add_tracks",function(){d._slider.min=e.getStartTime(),d._slider.max=e.getEndTime(),d._slider.value=e.getTime()}),this._container}}),a.Playback=a.Playback.Clock.extend({statics:{MoveableMarker:a.Playback.MoveableMarker,Track:a.Playback.Track,TrackController:a.Playback.TrackController,Clock:a.Playback.Clock,Util:a.Playback.Util,TracksLayer:a.Playback.TracksLayer,PlayControl:a.Playback.PlayControl,DateControl:a.Playback.DateControl,SliderControl:a.Playback.SliderControl},options:{tickLen:250,speed:1,maxInterpolationTime:3e5,tracksLayer:!0,playControl:!1,dateControl:!1,sliderControl:!1,layer:{},marker:{}},initialize:function(b,c,d,e){a.setOptions(this,e),this._map=b,this._trackController=new a.Playback.TrackController(b,null,this.options),a.Playback.Clock.prototype.initialize.call(this,this._trackController,d,this.options),this.options.tracksLayer&&(this._tracksLayer=new a.Playback.TracksLayer(b,e)),this.setData(c),this.options.playControl&&(this.playControl=new a.Playback.PlayControl(this),this.playControl.addTo(b)),this.options.sliderControl&&(this.sliderControl=new a.Playback.SliderControl(this),this.sliderControl.addTo(b)),this.options.dateControl&&(this.dateControl=new a.Playback.DateControl(this,e),this.dateControl.addTo(b))},clearData:function(){this._trackController.clearTracks(),this._tracksLayer&&this._tracksLayer.clearLayer()},setData:function(a){this.clearData(),this.addData(a,this.getTime()),this.setCursor(this.getStartTime())},addData:function(b,c){if(b){if(b instanceof Array)for(var d=0,e=b.length;e>d;d++)this._trackController.addTrack(new a.Playback.Track(b[d],this.options),c);else this._trackController.addTrack(new a.Playback.Track(b,this.options),c);this._map.fire("playback:set:data"),this.options.tracksLayer&&this._tracksLayer.addLayer(b)}},destroy:function(){this.clearData(),this.playControl&&this._map.removeControl(this.playControl),this.sliderControl&&this._map.removeControl(this.sliderControl),this.dateControl&&this._map.removeControl(this.dateControl)}}),a.Map.addInitHook(function(){this.options.playback&&(this.playback=new a.Playback(this))}),a.playback=function(b,c,d,e){return new a.Playback(b,c,d,e)},a.Playback}); \ No newline at end of file diff --git a/src/Clock.js b/src/Clock.js index ac347ea..1296d8e 100644 --- a/src/Clock.js +++ b/src/Clock.js @@ -68,10 +68,6 @@ L.Playback.Clock = L.Class.extend({ setCursor: function (ms) { var time = parseInt(ms); if (!time) return; - var mod = time % this._tickLen; - if (mod !== 0) { - time += this._tickLen - mod; - } this._cursor = time; this._trackController.tock(this._cursor, 0); this._callbacks(this._cursor); diff --git a/src/Track.js b/src/Track.js index 1af9e0f..6c237ad 100644 --- a/src/Track.js +++ b/src/Track.js @@ -1,133 +1,29 @@ L.Playback = L.Playback || {}; - - - +function search(o, v, i){ + var h = o.length, l = -1, m; + while(h - l > 1) + if(o[m = h + l >> 1] < v) l = m; + else h = m; + return o[h] != v ? i ? h : -1 : h; +} L.Playback.Track = L.Class.extend({ initialize : function (geoJSON, options) { options = options || {}; var tickLen = options.tickLen || 250; + var sampleTimes = geoJSON.properties.time; + var samples = geoJSON.geometry.coordinates; this._staleTime = options.staleTime || 60*60*1000; this._fadeMarkersWhenStale = options.fadeMarkersWhenStale || false; this._geoJSON = geoJSON; this._tickLen = tickLen; - this._ticks = []; + this._ticks = samples; + this._times = sampleTimes; this._marker = null; - this._orientations = []; - - var sampleTimes = geoJSON.properties.time; - this._orientIcon = options.orientIcons; - var previousOrientation; - - var samples = geoJSON.geometry.coordinates; - var currSample = samples[0]; - var nextSample = samples[1]; - - var currSampleTime = sampleTimes[0]; - var t = currSampleTime; // t is used to iterate through tick times - var nextSampleTime = sampleTimes[1]; - var tmod = t % tickLen; // ms past a tick time - var rem, - ratio; - - // handle edge case of only one t sample - if (sampleTimes.length === 1) { - if (tmod !== 0) - t += tickLen - tmod; - this._ticks[t] = samples[0]; - this._orientations[t] = 0; - this._startTime = t; - this._endTime = t; - return; - } - - // interpolate first tick if t not a tick time - if (tmod !== 0) { - rem = tickLen - tmod; - ratio = rem / (nextSampleTime - currSampleTime); - t += rem; - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._ticks[t] = currSample; - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } - - this._startTime = t; - t += tickLen; - while (t < nextSampleTime) { - ratio = (t - currSampleTime) / (nextSampleTime - currSampleTime); - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - t += tickLen; - } - - // iterating through the rest of the samples - for (var i = 1, len = samples.length; i < len; i++) { - currSample = samples[i]; - nextSample = samples[i + 1]; - t = currSampleTime = sampleTimes[i]; - nextSampleTime = sampleTimes[i + 1]; - - tmod = t % tickLen; - if (tmod !== 0 && nextSampleTime) { - rem = tickLen - tmod; - ratio = rem / (nextSampleTime - currSampleTime); - t += rem; - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - if(nextSample){ - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } else { - this._ticks[t] = currSample; - if(nextSample){ - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } - - t += tickLen; - while (t < nextSampleTime) { - ratio = (t - currSampleTime) / (nextSampleTime - currSampleTime); - - if (nextSampleTime - currSampleTime > options.maxInterpolationTime){ - this._ticks[t] = currSample; - - if(nextSample){ - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } - else { - this._ticks[t] = this._interpolatePoint(currSample, nextSample, ratio); - if(nextSample) { - this._orientations[t] = this._directionOfPoint(currSample,nextSample); - previousOrientation = this._orientations[t]; - } else { - this._orientations[t] = previousOrientation; - } - } - - t += tickLen; - } - } - - // the last t in the while would be past bounds - this._endTime = t - tickLen; - this._lastTick = this._ticks[this._endTime]; - + this._startTime = sampleTimes[0]; + this._endTime = sampleTimes[sampleTimes.length - 1]; }, _interpolatePoint : function (start, end, ratio) { @@ -174,11 +70,11 @@ L.Playback.Track = L.Class.extend({ }, getFirstTick : function () { - return this._ticks[this._startTime]; + return this._ticks[0]; }, getLastTick : function () { - return this._ticks[this._endTime]; + return this._ticks[this._ticks.length - 1]; }, getStartTime : function () { @@ -190,26 +86,7 @@ L.Playback.Track = L.Class.extend({ }, getTickMultiPoint : function () { - var t = this.getStartTime(); - var endT = this.getEndTime(); - var coordinates = []; - var time = []; - while (t <= endT) { - time.push(t); - coordinates.push(this.tick(t)); - t += this._tickLen; - } - - return { - type : 'Feature', - geometry : { - type : 'MultiPoint', - coordinates : coordinates - }, - properties : { - time : time - } - }; + return this._geoJSON; }, trackPresentAtTick : function(timestamp) @@ -223,21 +100,43 @@ L.Playback.Track = L.Class.extend({ }, tick : function (timestamp) { - if (timestamp > this._endTime) - timestamp = this._endTime; - if (timestamp < this._startTime) - timestamp = this._startTime; - return this._ticks[timestamp]; + if (timestamp >= this._endTime) + return this.getLastTick(); + if (timestamp <= this._startTime) + return this.getFirstTick(); + + var samples = this._ticks; + + var times = this._times; + + var index = search(times, timestamp, true); + + var currSampleTime = times[index - 1], + nextSampleTime = times[index], + currSample = samples[index - 1], + nextSample = samples[index]; + + if (nextSampleTime === timestamp) { + return nextSample; + } + + var ratio = (timestamp - currSampleTime) / (nextSampleTime - currSampleTime); + + return this._interpolatePoint(currSample, nextSample, ratio); }, courseAtTime: function(timestamp) { - //return 90; - if (timestamp > this._endTime) - timestamp = this._endTime; - if (timestamp < this._startTime) - timestamp = this._startTime; - return this._orientations[timestamp]; + var samples = this._ticks; + + var times = this._times; + + var index = search(times, timestamp, true); + + var currSample = samples[index - 1], + nextSample = samples[index]; + + return currSample && nextSample ? this._directionOfPoint(currSample,nextSample) : 0; }, setMarker : function(timestamp, options){