├── fabfile.pyc ├── img ├── down-triangle.png ├── logos │ ├── mpg_logo.png │ ├── nsf_logo.gif │ ├── obs_logo.gif │ ├── nceas_logo.png │ ├── tags_logo.jpg │ ├── cornell_logo.jpg │ ├── movebank_logo.png │ ├── unknown_logo.gif │ ├── imas_utas_logo.jpg │ └── cybercommons_logo.png └── threshold-triangle.png ├── js ├── log.js ├── utilities.js ├── cybercommons.js ├── dataset_parser.js ├── base.js ├── jquery.cookie.js └── new.js ├── bootstrap ├── img │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png └── css │ └── bootstrap-responsive.min.css ├── tests ├── spec │ ├── app │ │ ├── MapSpec.js │ │ ├── CalibrationSpec.js │ │ ├── ExporterSpec.js │ │ ├── AppSpec.js │ │ ├── TwilightsSpec.js │ │ ├── ChartSpec.js │ │ └── ProcessSpec.js │ ├── SpecHelper.js │ ├── js │ │ ├── DataSetParserSpec.js │ │ └── BaseSpec.js │ └── PlayerSpec.js ├── lib │ └── jasmine-1.3.1 │ │ ├── MIT.LICENSE │ │ ├── jasmine.css │ │ └── jasmine-html.js └── SpecRunner.html ├── lib ├── jq.spin.js ├── jquery_serialize.js ├── FileSaver.min.js ├── jquery.cookie.js ├── tablesort.min.js ├── spin.min.js ├── google_leaflet.js ├── cctasks.js └── Blob.js ├── app ├── app.js ├── calibration.js ├── map.js ├── exporter.js ├── process.js ├── twilights.js └── chart.js ├── fabfile.py ├── css ├── index.css └── calibrate.css ├── index.html └── calibrate.html /fabfile.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/fabfile.pyc -------------------------------------------------------------------------------- /img/down-triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/down-triangle.png -------------------------------------------------------------------------------- /img/logos/mpg_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/mpg_logo.png -------------------------------------------------------------------------------- /img/logos/nsf_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/nsf_logo.gif -------------------------------------------------------------------------------- /img/logos/obs_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/obs_logo.gif -------------------------------------------------------------------------------- /img/logos/nceas_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/nceas_logo.png -------------------------------------------------------------------------------- /img/logos/tags_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/tags_logo.jpg -------------------------------------------------------------------------------- /img/logos/cornell_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/cornell_logo.jpg -------------------------------------------------------------------------------- /img/logos/movebank_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/movebank_logo.png -------------------------------------------------------------------------------- /img/logos/unknown_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/unknown_logo.gif -------------------------------------------------------------------------------- /img/threshold-triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/threshold-triangle.png -------------------------------------------------------------------------------- /img/logos/imas_utas_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/imas_utas_logo.jpg -------------------------------------------------------------------------------- /js/log.js: -------------------------------------------------------------------------------- 1 | function log() { 2 | if (window.console) { 3 | console.log.apply(console, arguments); 4 | } 5 | } -------------------------------------------------------------------------------- /img/logos/cybercommons_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/img/logos/cybercommons_logo.png -------------------------------------------------------------------------------- /bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tags/geologgerui/master/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /tests/spec/app/MapSpec.js: -------------------------------------------------------------------------------- 1 | describe("Map", function() { 2 | it("should be defined", function() { 3 | expect(app.map).toBeDefined(); 4 | }); 5 | }); -------------------------------------------------------------------------------- /tests/spec/app/CalibrationSpec.js: -------------------------------------------------------------------------------- 1 | describe("Calibration", function() { 2 | it("should be defined", function() { 3 | expect(app.calibration).toBeDefined(); 4 | }); 5 | }); -------------------------------------------------------------------------------- /tests/spec/app/ExporterSpec.js: -------------------------------------------------------------------------------- 1 | describe("Export", function() { 2 | var exp = app.exporter; 3 | 4 | it("should be defined", function() { 5 | expect(app.exporter).toBeDefined(); 6 | }); 7 | }); -------------------------------------------------------------------------------- /lib/jq.spin.js: -------------------------------------------------------------------------------- 1 | $.fn.spin = function(opts) { 2 | this.each(function() { 3 | var $this = $(this), 4 | data = $this.data(); 5 | 6 | if (data.spinner) { 7 | data.spinner.stop(); 8 | delete data.spinner; 9 | } 10 | if (opts !== false) { 11 | data.spinner = new Spinner($.extend({color: $this.css('color')}, opts)).spin(this); 12 | } 13 | }); 14 | return this; 15 | }; -------------------------------------------------------------------------------- /js/utilities.js: -------------------------------------------------------------------------------- 1 | _.templateSettings = { 2 | interpolate : /\{\{(.+?)\}\}/g 3 | }; 4 | 5 | _.reversecopy = function(a) { 6 | var temp = []; 7 | var len = a.length; 8 | for (var i = (len - 1); i >= 0; i--) { 9 | temp.push(a[i]); 10 | } 11 | return temp; 12 | } 13 | 14 | _.filterUntil = function(flag, list, iterator) { 15 | var results = []; 16 | for ( var i = 0; i < list.length; i++ ) { 17 | var res = iterator(list[i]); 18 | if (res==false) continue; 19 | if (res==flag) break; 20 | results.push(list[i]); 21 | } 22 | return results; 23 | } 24 | 25 | String.prototype.capitalize = function() { 26 | return this.charAt(0).toUpperCase() + this.slice(1); 27 | } -------------------------------------------------------------------------------- /tests/spec/SpecHelper.js: -------------------------------------------------------------------------------- 1 | 2 | // custom matchers 3 | 4 | beforeEach(function() { 5 | this.addMatchers({ 6 | toBeWithinOneSecondOf: function(expected) { 7 | return ( this.actual >= expected-1000 && 8 | this.actual <= expected+1000 ); 9 | } 10 | }); 11 | }); 12 | 13 | // from chart.js 14 | 15 | var parseUTCDate = d3.time.format.iso.parse; 16 | 17 | function zonedDateString(datestring) { 18 | return datestring.charAt(datestring.length-1) == 'Z' ? 19 | datestring : 20 | datestring+"Z" 21 | } 22 | 23 | // normalize data like the chart does 24 | 25 | function normalizedData(data) { 26 | data.forEach(function(d) { 27 | d.datetime = parseUTCDate(zonedDateString(d.datetime)); 28 | d.light = +d.light; 29 | }); 30 | return data; 31 | } 32 | -------------------------------------------------------------------------------- /tests/spec/js/DataSetParserSpec.js: -------------------------------------------------------------------------------- 1 | describe("DataSetParser", function() { 2 | var parser; 3 | 4 | beforeEach(function() { 5 | parser = new DataSetParser; 6 | }); 7 | 8 | it("should recognize the TAGS format from the header", function() { 9 | var eli = "datetime,light"; 10 | expect(parser.formatOf(eli).name).toEqual("TAGS"); 11 | }); 12 | it("should recognize the TAGS format from the data", function() { 13 | var eli = "2011-06-18 10:24:30,2"; 14 | expect(parser.formatOf(eli).name).toEqual("TAGS"); 15 | }); 16 | 17 | it("should recognize the BAS format from the data", function() { 18 | var bas = "ok,19/02/10 20:16:00,40228.844444,9"; 19 | expect(parser.formatOf(bas).name).toEqual("BAS"); 20 | }); 21 | 22 | /* 23 | var sumner = "1 1 2000-10-27 22:44:00 191 NA 0"; 24 | var stefan1 = "datetime light"; 25 | var stefan2 = "01.07.2010 00:01 2"; 26 | */ 27 | }); -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | var app = app || Base.extend(); 2 | 3 | // global time constants 4 | 5 | var OneMinute = 1000*60; 6 | var OneHour = 1000*60*60; 7 | var OneDay = 1000*60*60*24; 8 | var OneHalfDay = OneDay/2; 9 | var OneQuarterDay = OneDay/4; 10 | 11 | (function() { 12 | 13 | // app hostname 14 | app.set('host', 'http://tags.animalmigration.org'); 15 | 16 | // default values, some explicitly undefined 17 | 18 | app.set('threshold',5.5); // threshold 19 | app.set('data',[]); // lightvalues, the dataset 20 | app.set('events',[]); // threshold events 21 | app.set('extent',[]); // focused extent 22 | app.set('problems',[]); // noisy twilight areas 23 | app.set('calibrationPeriod',[undefined,undefined]); // calibration 24 | app.set('releaseLocation',[undefined,undefined]); // lat & lon 25 | app.set('angleComputed', false); 26 | app.set('birdLocations',[]); 27 | 28 | app.set('sunangle',undefined); 29 | app.set('tagname',undefined); 30 | app.set('notes',undefined); 31 | 32 | // settings 33 | app.set('showSurroundingDays', false); 34 | })(); 35 | -------------------------------------------------------------------------------- /tests/lib/jasmine-1.3.1/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /js/cybercommons.js: -------------------------------------------------------------------------------- 1 | 2 | var CyberCommons = function() { 3 | function pollStatus(args) { 4 | $.getJSON(args.host + args.task_id + '?callback=?') 5 | .fail(function(jqXHR, status) { 6 | args.onFailure(jqXHR, status); 7 | }) 8 | .done(function(data) { 9 | if (data.status == "PENDING") { 10 | args.onPending(args.task_id); 11 | } else if (data.status == "FAILURE") { 12 | args.onFailure(data); 13 | } else if (data.status == "SUCCESS") { 14 | args.onSuccess(data); 15 | } 16 | }); 17 | } 18 | 19 | this.getStatusOfTask = function(appHost, taskID) { 20 | var promise = new $.Deferred(); 21 | var timer; 22 | var args = { 23 | host: appHost + "/queue/task/", 24 | task_id: taskID, 25 | onFailure: function(data) { 26 | clearInterval(timer); 27 | promise.reject(undefined,data); 28 | }, 29 | onSuccess: function(data) { 30 | clearInterval(timer); 31 | promise.resolve(data); 32 | }, 33 | onPending: function() { 34 | ; // nothing 35 | } 36 | } 37 | timer = setInterval(function () { 38 | pollStatus(args); 39 | }, 2000); 40 | return promise; 41 | } 42 | } -------------------------------------------------------------------------------- /tests/spec/app/AppSpec.js: -------------------------------------------------------------------------------- 1 | describe("App", function() { 2 | it("should be defined", function() { 3 | expect(app).toBeDefined(); 4 | }); 5 | it("should define global time constants", function() { 6 | expect(OneMinute).toBeDefined(); 7 | expect(OneHour).toBeDefined(); 8 | expect(OneDay).toBeDefined(); 9 | expect(OneHalfDay).toBeDefined(); 10 | expect(OneQuarterDay).toBeDefined(); 11 | }); 12 | it("should define sensible defaults", function() { 13 | expect(app.get('threshold')).toBeDefined(); 14 | expect(app.get('data')).toBeDefined(); 15 | expect(app.get('events')).toBeDefined(); 16 | expect(app.get('extent')).toBeDefined(); 17 | expect(app.get('problems')).toBeDefined(); 18 | 19 | expect(app.get('calibrationPeriod')).toBeDefined(); 20 | expect(app.get('releaseLocation')).toBeDefined(); 21 | expect(app.get('angleComputed')).toBeDefined(); 22 | expect(app.get('birdLocations')).toBeDefined(); 23 | expect(app.get('showSurroundingDays')).toBeDefined(); 24 | }); 25 | it("should leave some default values undefined", function() { 26 | expect(app.get('sunangle')).toBeUndefined(); 27 | expect(app.get('tagname')).toBeUndefined(); 28 | expect(app.get('notes')).toBeUndefined(); 29 | }); 30 | }); -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | ''' This fabfile is for use with deploying javascript-only applications to the cybercommons ''' 2 | 3 | from fabric.api import * 4 | from fabric.contrib.files import exists 5 | from fabric.colors import red 6 | import os 7 | 8 | env.sitename = os.path.basename(os.getcwd()) 9 | 10 | def statictest(): 11 | """ 12 | Work on staging environment 13 | """ 14 | env.settings = 'production' 15 | env.path = '/var/www/html/test/%(sitename)s' % env 16 | env.hosts = ['tags.animalmigration.org'] 17 | 18 | def static(): 19 | """ 20 | Work on staging environment 21 | """ 22 | env.settings = 'production' 23 | env.path = '/var/www/html/%(sitename)s' % env 24 | env.hosts = ['tags.animalmigration.org '] 25 | 26 | def setup_directories(): 27 | run('mkdir -p %(path)s' % env) 28 | 29 | def setup(): 30 | setup_directories() 31 | copy_working_dir() 32 | 33 | def deploy(): 34 | copy_working_dir() 35 | 36 | def copy_working_dir(): 37 | local('tar --exclude fabfile.py --exclude fabfile.pyc -czf /tmp/deploy_%(sitename)s.tgz .' % env) 38 | put('/tmp/deploy_%(sitename)s.tgz' % env, '%(path)s/deploy_%(sitename)s.tgz' % env) 39 | run('cd %(path)s; tar -xf deploy_%(sitename)s.tgz; rm deploy_%(sitename)s.tgz' % env) 40 | local('rm /tmp/deploy_%(sitename)s.tgz' % env) 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/spec/PlayerSpec.js: -------------------------------------------------------------------------------- 1 | describe("Player", function() { 2 | var player; 3 | var song; 4 | 5 | beforeEach(function() { 6 | player = new Player(); 7 | song = new Song(); 8 | }); 9 | 10 | it("should be able to play a Song", function() { 11 | player.play(song); 12 | expect(player.currentlyPlayingSong).toEqual(song); 13 | 14 | //demonstrates use of custom matcher 15 | expect(player).toBePlaying(song); 16 | }); 17 | 18 | describe("when song has been paused", function() { 19 | beforeEach(function() { 20 | player.play(song); 21 | player.pause(); 22 | }); 23 | 24 | it("should indicate that the song is currently paused", function() { 25 | expect(player.isPlaying).toBeFalsy(); 26 | 27 | // demonstrates use of 'not' with a custom matcher 28 | expect(player).not.toBePlaying(song); 29 | }); 30 | 31 | it("should be possible to resume", function() { 32 | player.resume(); 33 | expect(player.isPlaying).toBeTruthy(); 34 | expect(player.currentlyPlayingSong).toEqual(song); 35 | }); 36 | }); 37 | 38 | // demonstrates use of spies to intercept and test method calls 39 | it("tells the current song if the user has made it a favorite", function() { 40 | spyOn(song, 'persistFavoriteStatus'); 41 | 42 | player.play(song); 43 | player.makeFavorite(); 44 | 45 | expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true); 46 | }); 47 | 48 | //demonstrates use of expected exceptions 49 | describe("#resume", function() { 50 | it("should throw an exception if song is already playing", function() { 51 | player.play(song); 52 | 53 | expect(function() { 54 | player.resume(); 55 | }).toThrow("song is already playing"); 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /js/dataset_parser.js: -------------------------------------------------------------------------------- 1 | var DataSetParser = function() {}; 2 | 3 | DataSetParser.prototype.expressions = function() { 4 | return [ 5 | { 6 | name: "TAGS", 7 | header: /^(datetime,light)$/, 8 | hasHeader: true, 9 | re: /(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d),(\d+)/, 10 | parse: function(d) { 11 | // new Date(d) works as well, but let's be specific 12 | return new Date(d.replace(' ','T') + 'Z'); 13 | }, 14 | dateIndex: 1, 15 | lightIndex: 2 16 | }, 17 | { 18 | name: "BAS", 19 | re: /\w*,(\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d),\d+.*\d+,(\d+)/, 20 | parse: function(d) { 21 | // day/month/2year 22 | var day = d.slice(0,2), 23 | month = d.slice(3,5), 24 | year = "20" + d.slice(6,8), 25 | time = d.slice(9,17); 26 | return new Date(year+"-"+month+"-"+day+"T"+time+"Z"); 27 | }, 28 | dateIndex: 1, 29 | lightIndex: 2 30 | }, 31 | { 32 | name: "LUX", 33 | header: /^DD\/MM\/YYYY\s*HH:MM:SS\s*light\(lux\)$/, 34 | re: /(\d\d\/\d\d\/\d\d\d\d\s*\d\d:\d\d:\d\d)\s*(\d+.*\d*)/, 35 | hasHeader: true, 36 | parse: function(d) { 37 | var day = d.slice(0,2), 38 | month = d.slice(3,5), 39 | year = d.slice(6,10), 40 | time = d.slice(11,19); 41 | return new Date(year+"-"+month+"-"+day+"T"+time+"Z"); 42 | }, 43 | dateIndex: 1, 44 | lightIndex: 2 45 | } 46 | ]; 47 | } 48 | 49 | DataSetParser.prototype.formatOf = function(line) { 50 | var exp = this.expressions(); 51 | for (var i = 0; i < exp.length; i++) { 52 | var format = exp[i]; 53 | if (format.header && line.match(format.header)) { 54 | return format; 55 | } else if (line.match(format.re)) { 56 | return format; 57 | } 58 | } 59 | return null; 60 | } -------------------------------------------------------------------------------- /lib/jquery_serialize.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | // serialize a form in preparation for json submission 4 | // http://stackoverflow.com/questions/1184624/convert-form-data-to-js-object-with-jquery 5 | $.fn.serializeObject = function(){ 6 | 7 | var self = this, 8 | json = {}, 9 | push_counters = {}, 10 | patterns = { 11 | "validate": /^[a-zA-Z][a-zA-Z0-9_]*(?:\[(?:\d*|[a-zA-Z0-9_]+)\])*$/, 12 | "key": /[a-zA-Z0-9_]+|(?=\[\])/g, 13 | "push": /^$/, 14 | "fixed": /^\d+$/, 15 | "named": /^[a-zA-Z0-9_]+$/ 16 | }; 17 | 18 | this.build = function(base, key, value){ 19 | base[key] = value; 20 | return base; 21 | }; 22 | 23 | this.push_counter = function(key){ 24 | if(push_counters[key] === undefined){ 25 | push_counters[key] = 0; 26 | } 27 | return push_counters[key]++; 28 | }; 29 | 30 | $.each($(this).serializeArray(), function(){ 31 | 32 | // skip invalid keys 33 | if(!patterns.validate.test(this.name)){ 34 | return; 35 | } 36 | 37 | var k, 38 | keys = this.name.match(patterns.key), 39 | merge = this.value, 40 | reverse_key = this.name; 41 | 42 | while((k = keys.pop()) !== undefined){ 43 | // adjust reverse_key 44 | reverse_key = reverse_key.replace(new RegExp("\\[" + k + "\\]$"), ''); 45 | // push 46 | if(k.match(patterns.push)){ 47 | merge = self.build([], self.push_counter(reverse_key), merge); 48 | } 49 | // fixed 50 | else if(k.match(patterns.fixed)){ 51 | merge = self.build([], k, merge); 52 | } 53 | // named 54 | else if(k.match(patterns.named)){ 55 | merge = self.build({}, k, merge); 56 | } 57 | } 58 | 59 | json = $.extend(true, json, merge); 60 | }); 61 | 62 | return json; 63 | }; 64 | })(jQuery); -------------------------------------------------------------------------------- /app/calibration.js: -------------------------------------------------------------------------------- 1 | var app = app || Base.extend(); 2 | 3 | (function() { 4 | 5 | app.calibration = Base.extend((function() { 6 | 7 | var calibPattern = d3.select('#chart svg').append('pattern') 8 | .attr('id', 'calibration-pattern') 9 | .attr('patternUnits', 'userSpaceOnUse') 10 | .attr('x', 0) 11 | .attr('y', 0) 12 | .attr('width', 4) 13 | .attr('height', 4); 14 | calibPattern.append('rect') 15 | .attr('class','background') 16 | .attr('x', 0) 17 | .attr('y', 0) 18 | .attr('width', 4) 19 | .attr('height', 4); 20 | calibPattern 21 | .append('path') 22 | .attr('d', 'M 0 4 4 0'); 23 | 24 | d3.select('#chart svg defs').append("clipPath") 25 | .attr('id', 'calibClip') 26 | .append('rect') 27 | .attr('width',870) // hardcoded from chart 28 | .attr('height',12) // approx. hardcoding 29 | .attr('y',-12); 30 | 31 | function redrawRangeIndicators(range) { 32 | if (!range.length == 2 || !range[0] || !range[1]) 33 | return; 34 | 35 | var focus = d3.select("#chart svg .focus"); 36 | var context = d3.select("#chart svg .context .background"); 37 | var x2 = app.chart.get('x2'); 38 | var x = app.chart.get('x'); 39 | 40 | // draw in two places? at least on the brush 41 | 42 | context.select('.rangerect').remove(); 43 | context.append('rect') 44 | .attr('class','rangerect') 45 | .attr('x', x2(range[0])) 46 | .attr('y', -6) 47 | .attr('width',x2(range[1])-x2(range[0])) 48 | .attr('height',5) 49 | .attr('fill','url(#calibration-pattern)'); 50 | 51 | focus.select('.rangerect').remove(); 52 | focus.append('rect') 53 | .attr('clip-path','url(#calibClip)') 54 | .attr('class','rangerect') 55 | .attr('x', x(range[0])) 56 | .attr('y', -12) 57 | .attr('width',x(range[1])-x(range[0])) 58 | .attr('height',10) 59 | .attr('fill','url(#calibration-pattern)'); 60 | } 61 | 62 | app.addObserver('calibrationPeriod', function(range) { 63 | redrawRangeIndicators(range); 64 | }); 65 | app.addObserver('extent', function(extent) { 66 | redrawRangeIndicators(app.get('calibrationPeriod')); 67 | }); 68 | 69 | return {}; 70 | 71 | }())); 72 | 73 | })(); -------------------------------------------------------------------------------- /lib/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 2 | var saveAs=saveAs||navigator.msSaveBlob&&navigator.msSaveBlob.bind(navigator)||function(a){"use strict";var b=a.document,c=function(){return a.URL||a.webkitURL||a},d=a.URL||a.webkitURL||a,e=b.createElementNS("http://www.w3.org/1999/xhtml","a"),f="download"in e,g=function(c){var d=b.createEvent("MouseEvents");return d.initMouseEvent("click",!0,!1,a,0,0,0,0,0,!1,!1,!1,!1,0,null),c.dispatchEvent(d)},h=a.webkitRequestFileSystem,i=a.requestFileSystem||h||a.mozRequestFileSystem,j=function(b){(a.setImmediate||a.setTimeout)(function(){throw b},0)},k="application/octet-stream",l=0,m=[],n=function(){for(var a=m.length;a--;){var b=m[a];"string"==typeof b?d.revokeObjectURL(b):b.remove()}m.length=0},o=function(a,b,c){b=[].concat(b);for(var d=b.length;d--;){var e=a["on"+b[d]];if("function"==typeof e)try{e.call(a,c||a)}catch(f){j(f)}}},p=function(b,d){var q,r,x,j=this,n=b.type,p=!1,s=function(){var a=c().createObjectURL(b);return m.push(a),a},t=function(){o(j,"writestart progress write writeend".split(" "))},u=function(){(p||!q)&&(q=s(b)),r&&(r.location.href=q),j.readyState=j.DONE,t()},v=function(a){return function(){return j.readyState!==j.DONE?a.apply(this,arguments):void 0}},w={create:!0,exclusive:!1};return j.readyState=j.INIT,d||(d="download"),f&&(q=s(b),e.href=q,e.download=d,g(e))?(j.readyState=j.DONE,t(),void 0):(a.chrome&&n&&n!==k&&(x=b.slice||b.webkitSlice,b=x.call(b,0,b.size,k),p=!0),h&&"download"!==d&&(d+=".download"),r=n===k||h?a:a.open(),i?(l+=b.size,i(a.TEMPORARY,l,v(function(a){a.root.getDirectory("saved",w,v(function(a){var c=function(){a.getFile(d,w,v(function(a){a.createWriter(v(function(c){c.onwriteend=function(b){r.location.href=a.toURL(),m.push(a),j.readyState=j.DONE,o(j,"writeend",b)},c.onerror=function(){var a=c.error;a.code!==a.ABORT_ERR&&u()},"writestart progress write abort".split(" ").forEach(function(a){c["on"+a]=j["on"+a]}),c.write(b),j.abort=function(){c.abort(),j.readyState=j.DONE},j.readyState=j.WRITING}),u)}),u)};a.getFile(d,{create:!1},v(function(a){a.remove(),c()}),v(function(a){a.code===a.NOT_FOUND_ERR?c():u()}))}),u)}),u),void 0):(u(),void 0))},q=p.prototype,r=function(a,b){return new p(a,b)};return q.abort=function(){var a=this;a.readyState=a.DONE,o(a,"abort")},q.readyState=q.INIT=0,q.WRITING=1,q.DONE=2,q.error=q.onwritestart=q.onprogress=q.onwrite=q.onabort=q.onerror=q.onwriteend=null,a.addEventListener("unload",n,!1),r}(self); 3 | -------------------------------------------------------------------------------- /js/base.js: -------------------------------------------------------------------------------- 1 | // the base object, with key-value obvserving and private properties 2 | 3 | var Base = function() { 4 | 5 | var _properties = {}; 6 | var _observers = {}; 7 | var _me = this; 8 | 9 | function informObservers(n) { 10 | var obs = _observers[n]; 11 | if ( typeof obs === 'undefined' ) { 12 | return; 13 | } 14 | obs.forEach(function(o) { 15 | o.call(_me,_properties[n]); 16 | }); 17 | } 18 | 19 | function _addObserver(n,f) { 20 | if ( typeof _observers[n] === 'undefined' ) { 21 | _observers[n] = []; 22 | } 23 | if (_observers[n].indexOf(f) == -1) { 24 | _observers[n].push(f); 25 | } 26 | } 27 | 28 | this.get = function(n) { 29 | return _properties[n]; 30 | }; 31 | this.set = function(n,v) { 32 | _properties[n] = v; 33 | informObservers(n); 34 | }; 35 | this.addObserver = function(n,f) { 36 | if ( n instanceof Array ) { 37 | for (var i = 0; i < n.length; i++) { 38 | _addObserver(n[i],f); 39 | } 40 | } else { 41 | _addObserver(n,f); 42 | } 43 | }; 44 | this.removeObserver = function(n,f) { 45 | if ( typeof _observers[n] === 'undefined' ) { 46 | return; 47 | } 48 | var i = _observers[n].indexOf(f); 49 | if ( i == -1 ) { 50 | return; 51 | } 52 | _observers[n].splice(i,1); 53 | }; 54 | this.toObject = function() { 55 | var obj = {}; 56 | for ( var at in _properties ) { 57 | if ( _properties.hasOwnProperty(at) ) { 58 | obj[at] = _properties[at]; 59 | } 60 | } 61 | return obj; 62 | }; 63 | }; 64 | 65 | // extend the base object with a set of key-value properties and 66 | // private properties (attrs) 67 | 68 | Base.extend = function(child, attrs) { 69 | var obj = new Base(); 70 | if ( child ) { 71 | for ( var prop in child ) { 72 | if ( child.hasOwnProperty(prop) ) { 73 | obj[prop] = child[prop]; 74 | } 75 | } 76 | } 77 | if ( attrs ) { 78 | for ( var at in attrs ) { 79 | if ( attrs.hasOwnProperty(at) ) { 80 | obj.set(at, attrs[at]); 81 | } 82 | } 83 | } 84 | return obj; 85 | }; 86 | 87 | // for arrays composed of Base objects 88 | 89 | Array.prototype.addObserver = Array.prototype.addObserver 90 | || function(n,f) { 91 | this.forEach(function(el) { 92 | el.addObserver(n,f) 93 | }); 94 | }; 95 | 96 | Array.prototype.removeObserver = Array.prototype.removeObserver 97 | || function(n,f) { 98 | this.forEach(function(el) { 99 | el.removeObserver(n,f); 100 | }); 101 | }; -------------------------------------------------------------------------------- /css/index.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin-bottom: 10px; 4 | } 5 | 6 | #content { 7 | margin-top: 20px; 8 | padding-bottom: 20px; 9 | } 10 | 11 | #index { 12 | min-height: 560px; 13 | position: relative; 14 | } 15 | 16 | #index .footer { 17 | margin-top: 20px; 18 | padding-top: 15px; 19 | border-top: 1px solid #ccc; 20 | width: 600px; 21 | bottom: 0; 22 | position: absolute; 23 | } 24 | 25 | #index .footer img { 26 | float: left; 27 | margin: 10px 10px 0 10px; 28 | height: 40px; 29 | } 30 | 31 | #tags-logo { 32 | width: 200px; 33 | margin: 20px 0 30px; 34 | } 35 | 36 | .instructions { 37 | font-size: 18px; 38 | line-height: 24px; 39 | } 40 | 41 | #datasets { 42 | min-height: 560px; 43 | max-height: 580px; 44 | overflow-y: scroll; 45 | overflow-x: auto; 46 | } 47 | 48 | #progress-indicator { 49 | display: inline-block; 50 | margin: 15px 6px 0 0; 51 | } 52 | 53 | .show-edit { 54 | padding-top: 13px; 55 | } 56 | 57 | .dataset-link .date { 58 | display: block; 59 | } 60 | 61 | #sets { 62 | margin-left: 0; 63 | padding-left: 0; 64 | } 65 | 66 | /* new birds */ 67 | 68 | .help-inline { 69 | padding-bottom: 10px; 70 | } 71 | 72 | legend { 73 | margin-bottom: 10px; 74 | } 75 | 76 | legend.select-file { 77 | margin-bottom: 10px; 78 | } 79 | 80 | /*input[type=file] { 81 | margin: 10px 0; 82 | }*/ 83 | 84 | #drop-zone { 85 | border: 2px #ccc dashed; 86 | margin: 10px 0 30px; 87 | padding: 20px; 88 | font-size: 1.2em; 89 | } 90 | #drop-zone .or-drop { 91 | text-align: center; 92 | } 93 | 94 | input[type=file].error { 95 | color: #b94a48; 96 | } 97 | 98 | .file-success { 99 | border-color: #468847 !important; 100 | background: #dff0db; 101 | } 102 | 103 | .file-error { 104 | border-color: #b94a48 !important; 105 | background: #f2dede; 106 | } 107 | 108 | .file-invalid { 109 | border-color: #b94a48 !important; 110 | color: #b94a48; 111 | } 112 | 113 | #validation-error { 114 | margin-left: 10px; 115 | display: none; 116 | } 117 | 118 | .uploading-notification { 119 | margin-top:20px; 120 | display:none; 121 | } 122 | 123 | #upload-indicator { 124 | display: inline-block; 125 | margin: 5px 15px; 126 | } 127 | 128 | .alert h4 { 129 | margin-bottom: 20px; 130 | } 131 | 132 | #upload-success { 133 | display: none; 134 | } 135 | 136 | #upload-error { 137 | display: none; 138 | } 139 | 140 | .upload-help { 141 | font-size: 0.8em; 142 | } -------------------------------------------------------------------------------- /tests/spec/app/TwilightsSpec.js: -------------------------------------------------------------------------------- 1 | describe("Twilights", function() { 2 | beforeEach(function() { 3 | var data = normalizedData(lightvalues()); 4 | app.set('data',data); 5 | }); 6 | 7 | it("should be defined", function() { 8 | expect(app.twilights).toBeDefined(); 9 | }); 10 | it("should update twilight events when the threshold is set", function() { 11 | app.set('threshold',5); 12 | expect(app.get('events').length).toBeGreaterThan(0); 13 | }); 14 | 15 | describe("twilight events", function() { 16 | it("should all have light values equal to an integer threshold", function() { 17 | app.set('threshold',5); 18 | _.each(app.get('events'), function(d) { 19 | expect(d.get('threshold')).toEqual(5); 20 | }); 21 | }); 22 | it("should all have light values equal to a real value", function() { 23 | app.set('threshold',5.5); 24 | _.each(app.get('events'), function(d) { 25 | expect(d.get('threshold')).toEqual(5.5); 26 | }); 27 | }); 28 | it("should be set to the correct dates for an integer value", function() { 29 | app.set('threshold',5); 30 | var expected = twilightEventsAt5(); 31 | var twilights = app.get('events'); 32 | expect(twilights.length).toEqual(expected.length); 33 | for (var i = 0; i < expected.length; i++) { 34 | expect(twilights[i].get('datetime').getTime()).toBeWithinOneSecondOf(expected[i].getTime()); 35 | } 36 | }); 37 | it("should be set to the correct dates for a real value", function() { 38 | app.set('threshold',5.5); 39 | var expected = twilightEventsAt5Point5(); 40 | var twilights = app.get('events'); 41 | expect(twilights.length).toEqual(expected.length); 42 | for (var i = 0; i < expected.length; i++) { 43 | expect(twilights[i].get('datetime').getTime()).toBeWithinOneSecondOf(expected[i].getTime()); 44 | } 45 | }); 46 | it("should be a sunrise when the light value is rising", function() { 47 | app.set('threshold',5); 48 | var twilights = app.get('events'); 49 | for (var i = 0; i < twilights.length; i++) { 50 | if (i%2==0) { 51 | expect(twilights[i].get("type")).toEqual("sunrise"); 52 | } 53 | } 54 | }); 55 | it("should be a sunset when the light value is falling", function() { 56 | app.set('threshold',5); 57 | var twilights = app.get('events'); 58 | for (var i = 0; i < twilights.length; i++) { 59 | if (i%2==1) { 60 | expect(twilights[i].get("type")).toEqual("sunset"); 61 | } 62 | } 63 | }); 64 | it("should be a ??? when the light values are the same", function() { 65 | //pending(); 66 | }); 67 | }); 68 | }); -------------------------------------------------------------------------------- /js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.3.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as anonymous module. 11 | define(['jquery'], factory); 12 | } else { 13 | // Browser globals. 14 | factory(jQuery); 15 | } 16 | }(function ($) { 17 | 18 | var pluses = /\+/g; 19 | 20 | function raw(s) { 21 | return s; 22 | } 23 | 24 | function decoded(s) { 25 | return decodeURIComponent(s.replace(pluses, ' ')); 26 | } 27 | 28 | function converted(s) { 29 | if (s.indexOf('"') === 0) { 30 | // This is a quoted cookie as according to RFC2068, unescape 31 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 32 | } 33 | try { 34 | return config.json ? JSON.parse(s) : s; 35 | } catch(er) {} 36 | } 37 | 38 | var config = $.cookie = function (key, value, options) { 39 | 40 | // write 41 | if (value !== undefined) { 42 | options = $.extend({}, config.defaults, options); 43 | 44 | if (typeof options.expires === 'number') { 45 | var days = options.expires, t = options.expires = new Date(); 46 | t.setDate(t.getDate() + days); 47 | } 48 | 49 | value = config.json ? JSON.stringify(value) : String(value); 50 | 51 | return (document.cookie = [ 52 | config.raw ? key : encodeURIComponent(key), 53 | '=', 54 | config.raw ? value : encodeURIComponent(value), 55 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 56 | options.path ? '; path=' + options.path : '', 57 | options.domain ? '; domain=' + options.domain : '', 58 | options.secure ? '; secure' : '' 59 | ].join('')); 60 | } 61 | 62 | // read 63 | var decode = config.raw ? raw : decoded; 64 | var cookies = document.cookie.split('; '); 65 | var result = key ? undefined : {}; 66 | for (var i = 0, l = cookies.length; i < l; i++) { 67 | var parts = cookies[i].split('='); 68 | var name = decode(parts.shift()); 69 | var cookie = decode(parts.join('=')); 70 | 71 | if (key && key === name) { 72 | result = converted(cookie); 73 | break; 74 | } 75 | 76 | if (!key) { 77 | result[name] = converted(cookie); 78 | } 79 | } 80 | 81 | return result; 82 | }; 83 | 84 | config.defaults = {}; 85 | 86 | $.removeCookie = function (key, options) { 87 | if ($.cookie(key) !== undefined) { 88 | // Must not alter options, thus extending a fresh object... 89 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 90 | return true; 91 | } 92 | return false; 93 | }; 94 | 95 | })); 96 | -------------------------------------------------------------------------------- /lib/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.3.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as anonymous module. 11 | define(['jquery'], factory); 12 | } else { 13 | // Browser globals. 14 | factory(jQuery); 15 | } 16 | }(function ($) { 17 | 18 | var pluses = /\+/g; 19 | 20 | function raw(s) { 21 | return s; 22 | } 23 | 24 | function decoded(s) { 25 | return decodeURIComponent(s.replace(pluses, ' ')); 26 | } 27 | 28 | function converted(s) { 29 | if (s.indexOf('"') === 0) { 30 | // This is a quoted cookie as according to RFC2068, unescape 31 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 32 | } 33 | try { 34 | return config.json ? JSON.parse(s) : s; 35 | } catch(er) {} 36 | } 37 | 38 | var config = $.cookie = function (key, value, options) { 39 | 40 | // write 41 | if (value !== undefined) { 42 | options = $.extend({}, config.defaults, options); 43 | 44 | if (typeof options.expires === 'number') { 45 | var days = options.expires, t = options.expires = new Date(); 46 | t.setDate(t.getDate() + days); 47 | } 48 | 49 | value = config.json ? JSON.stringify(value) : String(value); 50 | 51 | return (document.cookie = [ 52 | config.raw ? key : encodeURIComponent(key), 53 | '=', 54 | config.raw ? value : encodeURIComponent(value), 55 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 56 | options.path ? '; path=' + options.path : '', 57 | options.domain ? '; domain=' + options.domain : '', 58 | options.secure ? '; secure' : '' 59 | ].join('')); 60 | } 61 | 62 | // read 63 | var decode = config.raw ? raw : decoded; 64 | var cookies = document.cookie.split('; '); 65 | var result = key ? undefined : {}; 66 | for (var i = 0, l = cookies.length; i < l; i++) { 67 | var parts = cookies[i].split('='); 68 | var name = decode(parts.shift()); 69 | var cookie = decode(parts.join('=')); 70 | 71 | if (key && key === name) { 72 | result = converted(cookie); 73 | break; 74 | } 75 | 76 | if (!key) { 77 | result[name] = converted(cookie); 78 | } 79 | } 80 | 81 | return result; 82 | }; 83 | 84 | config.defaults = {}; 85 | 86 | $.removeCookie = function (key, options) { 87 | if ($.cookie(key) !== undefined) { 88 | // Must not alter options, thus extending a fresh object... 89 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 90 | return true; 91 | } 92 | return false; 93 | }; 94 | 95 | })); 96 | -------------------------------------------------------------------------------- /lib/tablesort.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * tablesort v1.6.1 (2013-02-14) 3 | * http://tristen.ca/tablesort/demo 4 | * Copyright (c) 2013 ; Licensed MIT 5 | */ 6 | (function(){function e(e,t){if(e.tagName!=="TABLE")throw new Error("Element must be a table");this.init(e,t||{})}e.prototype={init:function(e,t){var n=this,r;this.thead=!1,this.options=t,this.options.d=t.descending||!1,e.rows&&e.rows.length>0&&(e.tHead&&e.tHead.rows.length>0?(r=e.tHead.rows[e.tHead.rows.length-1],n.thead=!0):r=e.rows[0]);if(!r)return;var i=function(e){var t=o(u,"tr").getElementsByTagName("th");for(var r=0;r-1},h=function(e,t,n){e.attachEvent?(e["e"+t+n]=n,e[t+n]=function(){e["e"+t+n](window.event)},e.attachEvent("on"+t,e[t+n])):e.addEventListener(t,n,!1)};window.Tablesort=e})(); -------------------------------------------------------------------------------- /app/map.js: -------------------------------------------------------------------------------- 1 | var app = app || Base.extend(); 2 | 3 | (function() { 4 | 5 | app.map = Base.extend((function() { 6 | 7 | // don't like the code that depends on the presence of 8 | // outside html elements: bad depedency: $('a[href=#map-tab]') 9 | 10 | // TODO: clear layer 11 | 12 | var mapRendered = false; 13 | var geoLayer = undefined; 14 | 15 | /*var wholeEarth = new L.LatLngBounds( 16 | new L.LatLng(-90,-180), 17 | new L.LatLng(90,180));*/ 18 | var map = L.map('map', { 19 | keyboard: false, 20 | center: [25, 0], 21 | /*maxBounds: wholeEarth,*/ 22 | minZoom: 1, 23 | zoom: 1 24 | }); 25 | var googleLayer = new L.Google('ROADMAP'); 26 | map.addLayer(googleLayer); 27 | 28 | /* 29 | L.tileLayer('http://{s}.tile.cloudmade.com/41d74d0a4bce4413bde0ea29c36e63cb/997/256/{z}/{x}/{y}.png', { 30 | attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © CloudMade', 31 | maxZoom: 16 32 | }).addTo(map); 33 | */ 34 | 35 | var geojsonMarkerOptions = { 36 | radius: 4, 37 | fillColor: "#ff7800", 38 | color: "#000", 39 | weight: 1, 40 | opacity: 1, 41 | fillOpacity: 0.8 42 | }; 43 | 44 | function removeNullCoordinates(coordinates) { 45 | return _.filter(coordinates, function(coord) { 46 | return (coord[0] && coord[1]); 47 | }); 48 | } 49 | 50 | function setConsistent(consistent) { 51 | if (!consistent) { 52 | $('a[href=#map-tab]').addClass('inconsistent-state'); 53 | $('[for=map]').tooltip({ 54 | title: 'Map is inconsistent: re-submit to update', 55 | placement: 'right' 56 | }); 57 | } else { 58 | $('a[href=#map-tab]').removeClass('inconsistent-state'); 59 | $('[for=map]').tooltip('destroy'); 60 | } 61 | } 62 | 63 | function invalidateMap() { 64 | if (!mapRendered) return; 65 | setConsistent(false); 66 | } 67 | 68 | // map does not draw initially when hidden, force redraw on show 69 | 70 | $('a[href=#map-tab]').on('shown', function (e) { 71 | map.invalidateSize(); 72 | }); 73 | 74 | // when calibration parameters change, show that map is inconsistent 75 | 76 | app.addObserver(['threshold','calibrationPeriod','releaseLocation', 77 | 'sunangle'], function() { 78 | invalidateMap(); 79 | }); 80 | 81 | app.addObserver('events', function(evts) { 82 | evts.addObserver('active', function(a) { 83 | invalidateMap(); 84 | }); 85 | }); 86 | 87 | return { 88 | drawGeoJSON: function(geo) { 89 | mapRendered = true; // damn you, state, damn you! 90 | setConsistent(true); 91 | 92 | if (geoLayer) map.removeLayer(geoLayer); 93 | 94 | //geo.geometry.coordinates = removeNullCoordinates(geo.geometry.coordinates); 95 | geoLayer = L.geoJson(geo, { 96 | pointToLayer: function (feature, latlng) { 97 | return L.circleMarker(latlng, geojsonMarkerOptions); 98 | }, 99 | onEachFeature: function(feature, layer) { 100 | layer.on('click',function(e) { 101 | //log(feature.properties); 102 | var start = d3.time.format.iso.parse(feature.properties.tFirst); 103 | var stop = d3.time.format.iso.parse(feature.properties.tSecond); 104 | if (start.getTime() == stop.getTime()) { 105 | stop = new Date(stop.getTime() + 24*OneHour); 106 | } 107 | app.chart.scrollTo(start,stop); 108 | }) 109 | } 110 | }); 111 | geoLayer.addTo(map); 112 | }, 113 | 114 | redrawMap: function() { 115 | map.invalidateSize(); 116 | }, 117 | 118 | invalidateMap: function() { 119 | invalidateMap(); 120 | } 121 | 122 | }; 123 | })()); 124 | 125 | })(); -------------------------------------------------------------------------------- /lib/spin.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e,i){var o=["webkit","Moz","ms","O"],r={},n;function a(t,i){var o=e.createElement(t||"div"),r;for(r in i)o[r]=i[r];return o}function s(t){for(var e=1,i=arguments.length;e>1):parseInt(i.left,10)+r)+"px",top:(i.top=="auto"?f.y-s.y+(t.offsetHeight>>1):parseInt(i.top,10)+r)+"px"})}o.setAttribute("aria-role","progressbar");e.lines(o,e.opts);if(!n){var l=0,p=i.fps,c=p/i.speed,h=(1-i.opacity)/(c*i.trail/100),m=c/i.lines;(function y(){l++;for(var t=i.lines;t;t--){var r=Math.max(1-(l+t*m)%c*h,i.opacity);e.opacity(o,i.lines-t,r,i)}e.timeout=e.el&&setTimeout(y,~~(1e3/p))})()}return e},stop:function(){var t=this.el;if(t){clearTimeout(this.timeout);if(t.parentNode)t.parentNode.removeChild(t);this.el=i}return this},lines:function(t,e){var i=0,o;function r(t,o){return u(a(),{position:"absolute",width:e.length+e.width+"px",height:e.width+"px",background:t,boxShadow:o,transformOrigin:"left",transform:"rotate("+~~(360/e.lines*i+e.rotate)+"deg) translate("+e.radius+"px"+",0)",borderRadius:(e.corners*e.width>>1)+"px"})}for(;i',e)}var e=u(a("group"),{behavior:"url(#default#VML)"});if(!p(e,"transform")&&e.adj){f.addRule(".spin-vml","behavior:url(#default#VML)");m.prototype.lines=function(e,i){var o=i.length+i.width,r=2*o;function n(){return u(t("group",{coordsize:r+" "+r,coordorigin:-o+" "+-o}),{width:r,height:r})}var a=-(i.width+i.length)*2+"px",f=u(n(),{position:"absolute",top:a,left:a}),l;function p(e,r,a){s(f,s(u(n(),{rotation:360/i.lines*e+"deg",left:~~r}),s(u(t("roundrect",{arcsize:i.corners}),{width:o,height:i.width,left:i.radius,top:-i.width>>1,filter:a}),t("fill",{color:i.color,opacity:i.opacity}),t("stroke",{opacity:0}))))}if(i.shadow)for(l=1;l<=i.lines;l++)p(l,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(l=1;l<=i.lines;l++)p(l);return s(e,f)};m.prototype.opacity=function(t,e,i,o){var r=t.firstChild;o=o.shadow&&o.lines||0;if(r&&e+o 3 | 4 | 5 | Jasmine Spec Runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /lib/cctasks.js: -------------------------------------------------------------------------------- 1 | // Some helpers for running Cybercommons Tasks 2 | // Should point to queue submission target 3 | // Configuration object for calling cybercom queue tasks. 4 | // Parameters can be specified in [params] list object, or a special list of 5 | // jQuery selectors can be provided to grab the current values of these form elements at run-time. 6 | /* 7 | taskdesc = { 8 | "taskname": 'cybercomq.static.tasks.modiscountry', 9 | "taskq": 'static', 10 | "params": ['MOD09A1_ndvi','MX','2010-10-10','2010-11-1'], // Fixed params 11 | "uiparams": ['#product','#country','#start_date','#end_date'],// UI Selected 12 | "status": '#status', 13 | "spinner": '#spinner', 14 | "pollinterval": 2000 15 | } 16 | 17 | */ 18 | // Called by call task to poll queue status of task based on task_id 19 | 20 | function test_auth_tkt() { 21 | $("#auth_dialog").hide(); 22 | if ($.cookie('auth_tkt') ) { 23 | $('#auth_message').html("you're logged in"); 24 | } 25 | else { 26 | $("#auth_dialog").html("In order to keep track of jobs you've requested, please login to the cyberCommons.") 27 | .dialog( { height:200, modal: true} ) 28 | .dialog("open"); 29 | $('#auth_message').html('Please login to track your tasks via the cybercommons') 30 | .addClass('label warning'); 31 | } 32 | 33 | } 34 | 35 | function poll_status(args) { 36 | $.getJSON(args.host + args.task_id + '?callback=?', function (data) { 37 | if (data.status == "PENDING") { 38 | options.onPending(args.task_id); 39 | } else if (data.status == "FAILURE") { 40 | options.onFailure(data); 41 | } else if (data.status == "SUCCESS") { 42 | options.onSuccess(data); 43 | } 44 | }); 45 | } 46 | 47 | function calltask(taskdesc) { 48 | defaults = { 49 | "service_host": 'http://test.cybercommons.org/queue/run/', 50 | "poll_target": 'http://test.cybercommons.org/queue/task/', 51 | "status": '#status', 52 | "spinner": '#spinner', 53 | "pollinterval": 2000, 54 | "onPending": function (task_id) { 55 | $(options.status).show() 56 | .removeClass('label success warning important') 57 | .addClass('label warning') 58 | .text("Working..."); 59 | $(options.spinner).show(); 60 | setTimeout(function () { 61 | var poll = {}; 62 | poll.host = options.poll_target; 63 | poll.task_id = task_id; 64 | poll_status(poll); 65 | }, options.pollinterval); 66 | 67 | }, 68 | "onFailure": function (data) { 69 | $(options.status).show() 70 | .removeClass('label success warning important') 71 | .addClass('label important') 72 | .text("Task failed!"); 73 | $(options.spinner).hide(); 74 | }, 75 | "onSuccess": function (data) { 76 | $(options.status).show() 77 | .removeClass('label success warning important') 78 | .addClass('label success') 79 | .html('Download'); 80 | $(options.spinner).hide(); 81 | } 82 | }; 83 | 84 | options = $.extend(true, {}, defaults, taskdesc); 85 | 86 | var taskparams = ""; 87 | if (options.params) { 88 | for (item in options.params) { 89 | taskparams = taskparams.concat('/' + options.params[item]); 90 | } 91 | } else if (options.uiparams) { 92 | for (item in options.uiparams) { 93 | taskparams = taskparams.concat('/' + $(options.uiparams[item]).val()); 94 | } 95 | } 96 | var taskcall = ""; 97 | if (options.taskq) { 98 | taskcall = options.taskname + '@' + options.taskq; 99 | } else { 100 | taskcall = options.taskname; 101 | } 102 | 103 | var request = options.service_host + taskcall + taskparams; 104 | 105 | $.getJSON(request + '?callback=?', function (data) { 106 | $(options.status).text('Task submitted...'); 107 | var task_id = data.task_id; 108 | setTimeout(function () { 109 | var poll = {}; 110 | poll.host = options.poll_target; 111 | poll.task_id = task_id; 112 | poll_status(poll); 113 | }, taskparams.pollinterval); 114 | }); 115 | } 116 | -------------------------------------------------------------------------------- /tests/spec/app/ChartSpec.js: -------------------------------------------------------------------------------- 1 | describe("Chart", function() { 2 | var lvs, firstObj, astObj; 3 | var start, stop; 4 | 5 | // factory data start: 2011-06-18T10:24:30 6 | // factory data stop: 2011-07-18T10:54:30 7 | 8 | beforeEach(function() { 9 | lvs = lightvalues(); 10 | app.chart.loadData(lvs); 11 | firstObj = app.get('data')[0]; 12 | lastObj = app.get('data')[app.get('data').length-1]; 13 | // scroll tests 14 | start = new Date("July 1, 2011"); 15 | stop = new Date("July 14, 2011"); 16 | }); 17 | 18 | it("should be defined", function() { 19 | expect(app.chart).toBeDefined(); 20 | }); 21 | describe("#loadData()", function() { 22 | it("should set the app data", function() { 23 | expect(app.get('data').length).toEqual(lvs.length); 24 | }); 25 | it("should convert datetime strings to dates", function() { 26 | expect(firstObj.datetime instanceof Date).toBe(true); 27 | }); 28 | it("should convert light value strings to numbers", function() { 29 | expect(typeof(firstObj.light)).toBe("number"); 30 | }); 31 | it("should set the domainMin to the first datetime", function() { 32 | expect(app.chart.domainMin()).toEqual(firstObj.datetime.getTime()); 33 | }); 34 | it("should set the domainMax to the last datetime", function() { 35 | expect(app.chart.domainMax()).toEqual(lastObj.datetime.getTime()); 36 | }); 37 | it("should scroll to the beginning of the data", function() { 38 | expect(app.chart.extentMin()).toEqual(firstObj.datetime.getTime()); 39 | }); 40 | }); 41 | describe("#extentRange()", function() { 42 | it("should be the range length between extentMin and extentMax", function() { 43 | expect(app.chart.extentRange()).toEqual(app.chart.extentMax()-app.chart.extentMin()); 44 | }); 45 | }); 46 | describe("#extentCenter()", function() { 47 | it("should be the middle of the visible extent", function() { 48 | expect(app.chart.extentCenter()).toEqual(app.chart.extentMin()+app.chart.extentRange()/2); 49 | }); 50 | }); 51 | describe("#scrollTo()", function() { 52 | it("should set the extentMin to the start time", function() { 53 | app.chart.scrollTo(start, stop); 54 | expect(app.chart.extentMin()).toEqual(start.getTime()); 55 | }); 56 | it("should set the extentMax to the end time", function() { 57 | app.chart.scrollTo(start,stop); 58 | expect(app.chart.extentMax()).toEqual(stop.getTime()); 59 | }); 60 | it("should not affect the domain range", function() { 61 | var dmin = app.chart.domainMin(); 62 | var dmax = app.chart.domainMax(); 63 | app.chart.scrollTo(start,stop); 64 | expect(app.chart.domainMin()).toEqual(dmin); 65 | expect(app.chart.domainMax()).toEqual(dmax); 66 | }); 67 | }); 68 | describe("#constrainToDomainMax()", function() { 69 | it("should return the domain max if the value is greater than the domain max", function() { 70 | var date = new Date("Jan 1, 2012").getTime(); 71 | var constrained = app.chart.constrainToDomainMax(date); 72 | expect(constrained).toEqual(app.chart.domainMax()); 73 | }); 74 | it("should return the value if it less than the domain max", function() { 75 | var date = new Date("Jun 30, 2011").getTime(); 76 | var constrained = app.chart.constrainToDomainMax(date); 77 | expect(constrained).toEqual(date); 78 | }); 79 | }); 80 | describe("#constrainToDomainMin()", function() { 81 | it("should return the domain min if the value is less than the domain main", function() { 82 | var date = new Date("Jun 30, 2010").getTime(); 83 | var constrained = app.chart.constrainToDomainMin(date); 84 | expect(constrained).toEqual(app.chart.domainMin()); 85 | }); 86 | it("should return the value if it is greater than the domain min", function() { 87 | var date = new Date("Jun 30, 2011").getTime(); 88 | var constrained = app.chart.constrainToDomainMin(date); 89 | expect(constrained).toEqual(date); 90 | }); 91 | }); 92 | describe("#gotoBeginning()", function() { 93 | it("should scroll to the beginning of the data", function() { 94 | app.chart.scrollTo(start,stop); 95 | app.chart.gotoBeginning(); 96 | expect(app.chart.extentMin()).toEqual(firstObj.datetime.getTime()); 97 | }); 98 | it("should preserve the visble range", function() { 99 | app.chart.scrollTo(start,stop); 100 | app.chart.gotoBeginning(); 101 | expect(app.chart.extentRange()).toEqual(stop.getTime()-start.getTime()); 102 | }); 103 | }); 104 | describe("#gotoEnd()", function() { 105 | it("should scroll to the end of the data", function() { 106 | app.chart.scrollTo(start,stop); 107 | app.chart.gotoEnd(); 108 | expect(app.chart.extentMax()).toEqual(lastObj.datetime.getTime()); 109 | }); 110 | it("should preserve the visible range", function() { 111 | app.chart.scrollTo(start,stop); 112 | app.chart.gotoEnd(); 113 | expect(app.chart.extentRange()).toEqual(stop.getTime()-start.getTime()); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /tests/spec/js/BaseSpec.js: -------------------------------------------------------------------------------- 1 | describe("Base", function() { 2 | var base; 3 | 4 | beforeEach(function() { 5 | base = new Base; 6 | }); 7 | 8 | describe("getters and setters", function() { 9 | it("should work with simple values", function() { 10 | base.set('foo','bar'); 11 | expect(base.get('foo')).toEqual('bar'); 12 | }); 13 | it("should work with objects", function() { 14 | var bar = {a:12, b:'hi'}; 15 | base.set('foo',bar); 16 | expect(base.get('foo')).toEqual(bar); 17 | }); 18 | it("should use the most recent value", function() { 19 | base.set('foo','bar'); 20 | base.set('foo','baz'); 21 | expect(base.get('foo')).toEqual('baz'); 22 | }); 23 | }); 24 | 25 | describe("observers", function() { 26 | var observer; 27 | 28 | beforeEach(function() { 29 | observer = new Object; 30 | observer.callback = function(){}; 31 | observer.callhairyback = function(){}; 32 | spyOn(observer,'callback'); 33 | spyOn(observer,'callhairyback'); 34 | }); 35 | 36 | it("should inform a single observer", function() { 37 | base.addObserver('foo',observer.callback); 38 | base.set('foo'); 39 | expect(observer.callback).toHaveBeenCalled(); 40 | }); 41 | it("should inform multiple observers", function() { 42 | base.addObserver('foo',observer.callback); 43 | base.addObserver('foo',observer.callhairyback); 44 | base.set('foo'); 45 | expect(observer.callback).toHaveBeenCalled(); 46 | }); 47 | it("should call an observer once", function() { 48 | base.addObserver('foo',observer.callback); 49 | base.set('foo'); 50 | expect(observer.callback.calls.length).toEqual(1); 51 | }); 52 | it("should call an observer once even if it has been added more than once", function() { 53 | base.addObserver('foo',observer.callback); 54 | base.addObserver('foo',observer.callback); 55 | base.set('foo'); 56 | expect(observer.callback.calls.length).toEqual(1); 57 | }); 58 | it("should not call a removed observer", function() { 59 | base.addObserver('foo',observer.callback); 60 | base.removeObserver('foo',observer.callback); 61 | base.set('foo'); 62 | expect(observer.callback.calls.length).toEqual(0); 63 | }); 64 | it("should still call observers that haven't been removed", function() { 65 | base.addObserver('foo',observer.callback); 66 | base.addObserver('foo',observer.callhairyback); 67 | base.removeObserver('foo',observer.callback); 68 | base.set('foo'); 69 | expect(observer.callhairyback).toHaveBeenCalled(); 70 | }); 71 | }); 72 | 73 | describe("#toObject()", function() { 74 | it("should work with a single value", function() { 75 | base.set('foo','bar'); 76 | var expectedResult = {foo: 'bar'}; 77 | var result = base.toObject(); 78 | expect(result).toEqual(expectedResult); 79 | }); 80 | it("should work with multiple values", function() { 81 | base.set('foo',42); 82 | base.set('bar',101); 83 | var expectedResult = { foo: 42, bar: 101 }; 84 | var result = base.toObject(); 85 | expect(result).toEqual(expectedResult); 86 | }); 87 | }); 88 | 89 | describe("::extend()", function() { 90 | it("should create a new Base object", function() { 91 | var base = Base.extend(); 92 | expect(base instanceof Base).toBe(true); 93 | }); 94 | it("should extend base with the values in an object", function() { 95 | var obj = {foo: 42, bar: 'dont worry'}; 96 | var base = Base.extend(obj); 97 | expect(base.foo).toEqual(42); 98 | expect(base.bar).toEqual('dont worry'); 99 | }); 100 | it("should extend base with the function values in an object", function() { 101 | function fooFunction(){}; 102 | function barFunction(){}; 103 | var obj = {foo: fooFunction, bar: barFunction}; 104 | var base = Base.extend(obj); 105 | expect(base.foo).toEqual(fooFunction); 106 | expect(base.bar).toEqual(barFunction); 107 | }); 108 | it("should add key-value pairs for observed values", function() { 109 | var vals = {foo: 42, bar: 'happy'}; 110 | var base = Base.extend(null,vals); 111 | expect(base.get('foo')).toEqual(42); 112 | expect(base.get('bar')).toEqual('happy'); 113 | }); 114 | }); 115 | }); 116 | 117 | describe("Array: Base extensions", function() { 118 | describe("observers", function() { 119 | var observer; 120 | 121 | beforeEach(function() { 122 | observer = new Object; 123 | observer.callback = function(){}; 124 | spyOn(observer,'callback'); 125 | }); 126 | 127 | it("should add observers to a single item", function() { 128 | var array = [new Base]; 129 | array.addObserver('foo',observer.callback); 130 | array[0].set('foo','bar'); 131 | expect(observer.callback).toHaveBeenCalled(); 132 | }); 133 | it("should add observers to many items", function() { 134 | var array = [new Base, new Base]; 135 | array.addObserver('foo',observer.callback); 136 | array[0].set('foo','bar'); 137 | array[1].set('foo','bar'); 138 | expect(observer.callback).toHaveBeenCalled(); 139 | expect(observer.callback.calls.length).toEqual(2); 140 | }); 141 | }); 142 | }); -------------------------------------------------------------------------------- /tests/spec/app/ProcessSpec.js: -------------------------------------------------------------------------------- 1 | describe("Process", function() { 2 | var ps = app.process; 3 | 4 | it("should be defined", function() { 5 | expect(app.process).toBeDefined(); 6 | }); 7 | describe("#sunAngle()", function() { 8 | it("should be defined", function() { 9 | expect(app.process.sunAngle).toBeDefined(); 10 | }); 11 | }); 12 | describe("#locations()", function() { 13 | it("should be defined", function() { 14 | expect(app.process.locations).toBeDefined(); 15 | }); 16 | }); 17 | 18 | describe("#eventDataIsValid()", function() { 19 | it("should return true if there are two or more data points", function() { 20 | expect(ps.eventDataIsValid([1,2])).toBe(true); 21 | }); 22 | it("should return false if there are less than two data points", function() { 23 | expect(ps.eventDataIsValid([1])).toBe(false); 24 | expect(ps.eventDataIsValid([])).toBe(false); 25 | }); 26 | }); 27 | 28 | describe("#lightThresholdIsValid()", function() { 29 | it("should return true for a positive number", function() { 30 | expect(ps.lightThresholdIsValid(1)).toBe(true); 31 | }); 32 | it("should return false if undefined", function() { 33 | expect(ps.lightThresholdIsValid(undefined)).toBe(false); 34 | }); 35 | }); 36 | 37 | describe("#releaseLocationIsValid()", function() { 38 | it("should return true for a two element array", function() { 39 | expect(ps.releaseLocationIsValid([1,2])).toBe(true); 40 | }); 41 | it("should return false for an array of more or less than two elements", function() { 42 | expect(ps.releaseLocationIsValid([1,2,3])).toBe(false); 43 | expect(ps.releaseLocationIsValid([1])).toBe(false); 44 | }); 45 | it("should return false if the first element is undefined", function() { 46 | expect(ps.releaseLocationIsValid([undefined,1])).toBe(false); 47 | }); 48 | it("should return false if the second element is undefined", function() { 49 | expect(ps.releaseLocationIsValid([1,undefined])).toBe(false); 50 | }); 51 | 52 | it("should return false if latitude is less than -90", function() { 53 | expect(ps.releaseLocationIsValid([-91,0])).toBe(false); 54 | }); 55 | it("should return false if latitude is greater than 90", function() { 56 | expect(ps.releaseLocationIsValid([91,0])).toBe(false); 57 | }); 58 | it("should return true if latitude is between -90 and 90", function() { 59 | expect(ps.releaseLocationIsValid([-90,0])).toBe(true); 60 | expect(ps.releaseLocationIsValid([0,0])).toBe(true); 61 | expect(ps.releaseLocationIsValid([90,0])).toBe(true); 62 | }); 63 | it("should return false if longitude is less than -180", function() { 64 | expect(ps.releaseLocationIsValid([0,-181])).toBe(false); 65 | }); 66 | it("should return false if longitude is greater than 180", function() { 67 | expect(ps.releaseLocationIsValid([0,181])).toBe(false); 68 | }); 69 | it("should return true if longitude is between -180 and 180", function() { 70 | expect(ps.releaseLocationIsValid([0,-180])).toBe(true); 71 | expect(ps.releaseLocationIsValid([0,0])).toBe(true); 72 | expect(ps.releaseLocationIsValid([0,180])).toBe(true); 73 | }); 74 | }); 75 | 76 | describe("#calibrationPeriodIsValid()", function() { 77 | it("should return true for a two element array", function() { 78 | expect(ps.calibrationPeriodIsValid([1,2])).toBe(true); 79 | }); 80 | it("should return false for an array of more or less than two elements", function() { 81 | expect(ps.calibrationPeriodIsValid([1,2,3])).toBe(false); 82 | expect(ps.calibrationPeriodIsValid([1])).toBe(false); 83 | }); 84 | it("should return false if the first element is undefined", function() { 85 | expect(ps.calibrationPeriodIsValid([undefined,1])).toBe(false); 86 | }); 87 | it("should return false if the second element is undefined", function() { 88 | expect(ps.calibrationPeriodIsValid([1,undefined])).toBe(false); 89 | }); 90 | it("should return false if the second el is less than the first", function() { 91 | expect(ps.calibrationPeriodIsValid([100,50])).toBe(false); 92 | }); 93 | }); 94 | 95 | describe("#sunAngleIsValid()", function() { 96 | it("should return true for a positive number", function() { 97 | expect(ps.sunAngleIsValid(1)).toBe(true); 98 | }); 99 | it("should return true for a negative number", function() { 100 | expect(ps.sunAngleIsValid(-1)).toBe(true); 101 | }); 102 | it("should return true for zero", function() { 103 | expect(ps.sunAngleIsValid(0)).toBe(true); 104 | }); 105 | it("should return false if undefined", function() { 106 | expect(ps.sunAngleIsValid(undefined)).toBe(false); 107 | }); 108 | }); 109 | 110 | describe("#formattedReleaseLocation()", function() { 111 | var dec, com; 112 | beforeEach(function() { 113 | dec = ["56.4", "-140.3"]; 114 | com = ["56,4", "-140,3"]; 115 | }); 116 | 117 | it("should leave period notation for decimal point unchanged", function() { 118 | expect(ps.formattedReleaseLocation(dec)).toEqual(dec); 119 | }); 120 | it("should replace comma notation for decimal point with a period", function() { 121 | expect(ps.formattedReleaseLocation(com)).toEqual(dec); 122 | }); 123 | }); 124 | 125 | describe("#formattedEventData()", function() { 126 | //... 127 | }); 128 | }); -------------------------------------------------------------------------------- /css/calibrate.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-bottom: 20px; 3 | } 4 | 5 | label { 6 | display: inline-block; 7 | } 8 | 9 | .well { 10 | background-color: #f9f9f9; 11 | } 12 | 13 | .alerts { 14 | margin-top: 10px; 15 | } 16 | 17 | .shortcuts.btn-toolbar { 18 | padding-top: 10px; 19 | } 20 | .shortcuts.btn-toolbar .btn { 21 | min-width: 32px; 22 | } 23 | .input-append { 24 | display: inline; 25 | } 26 | 27 | #map { 28 | height: 400px; 29 | } 30 | 31 | .inconsistent-state { 32 | color: red !important; 33 | } 34 | 35 | #chart { 36 | font: 10px sans-serif; 37 | margin-bottom: 20px; 38 | /*overflow-x: scroll;*/ 39 | border: 1px solid #ddd; 40 | position: relative; 41 | } 42 | 43 | #activity-indicator { 44 | position: absolute; 45 | left: 400px; /* 960/2 - 160/2 */ 46 | top: 100px; 47 | background: #333; 48 | color: white; 49 | width: 160px; 50 | height: 100px; 51 | text-align: center; 52 | -webkit-border-radius: 10px; 53 | -moz-border-radius: 10px; 54 | border-radius: 10px; 55 | } 56 | #activity-spinner { 57 | margin-top: 40px; 58 | } 59 | #activity-indicator p { 60 | clear: both; 61 | bottom: 10px; 62 | font-size: 16px; 63 | margin-top: 70px; 64 | } 65 | 66 | .control-interface { 67 | min-height: 420px; 68 | } 69 | .control-interface form { 70 | margin-bottom: 10px; 71 | } 72 | .control-interface .control-group { 73 | margin-bottom: 10px; 74 | } 75 | .control-interface .control-label { 76 | width: 100px; 77 | } 78 | .control-interface .controls { 79 | margin-left: 110px; 80 | } 81 | .control-interface a { 82 | font-size: 0.9em; 83 | } 84 | .control-interface hr { 85 | margin: 10px 0; 86 | } 87 | .control-interface .xor { 88 | text-align:center; 89 | text-transform: uppercase; 90 | font-size: 0.8em; 91 | font-weight: bold; 92 | } 93 | .control-interface .submit { 94 | width: 100%; 95 | text-align: center; 96 | } 97 | 98 | #table-events .visible td { 99 | background-color: #FFEF99; 100 | } 101 | 102 | /* table sort */ 103 | 104 | th.sort-header { 105 | cursor:pointer; 106 | } 107 | th.sort-header::-moz-selection, 108 | th.sort-header::selection { 109 | background:transparent; 110 | } 111 | table th.sort-header:after { 112 | content:''; 113 | float:right; 114 | margin-top:7px; 115 | border-width:0 4px 4px; 116 | border-style:solid; 117 | border-color:#404040 transparent; 118 | visibility:hidden; 119 | } 120 | table th.sort-header:hover:after { 121 | visibility:visible; 122 | } 123 | table th.sort-up:after, 124 | table th.sort-down:after, 125 | table th.sort-down:hover:after { 126 | visibility:visible; 127 | opacity:0.4; 128 | } 129 | table th.sort-up:after { 130 | border-bottom:none; 131 | border-width:4px 4px 0; 132 | } 133 | 134 | .ui-datepicker { 135 | font-size: 12px !important; 136 | line-height: 14px !important; 137 | } 138 | 139 | /* the chart */ 140 | 141 | #calibration-pattern path { 142 | fill: none; 143 | stroke: white; 144 | stroke-width: 1px; 145 | stroke-linejoin: 'miter'; 146 | stroke-linecap: 'square'; 147 | } 148 | #calibration-pattern .background { 149 | fill: green; 150 | stroke: none; 151 | stroke-width: 0; 152 | } 153 | 154 | .axis path, 155 | .axis line { 156 | shape-rendering: crispEdges; 157 | fill: none; 158 | stroke: #000; 159 | } 160 | 161 | .x.axis path { 162 | display: none; 163 | } 164 | 165 | .line { 166 | fill: none; 167 | stroke: #37b3f6; 168 | stroke-width: 1px; 169 | } 170 | 171 | .context .line { 172 | stroke: #37b3f6; 173 | } 174 | 175 | .line.daybefore { 176 | stroke: #dcdcdc; 177 | } 178 | .line.dayafter { 179 | stroke: #dcdcdc; 180 | } 181 | 182 | .datapoint { 183 | stroke: none; 184 | stroke-width: 0; 185 | fill: #000; 186 | } 187 | 188 | .brush .extent { 189 | shape-rendering: crispEdges; 190 | stroke: rgb(201, 117, 117); 191 | fill-opacity: .125; /*ms?*/ 192 | } 193 | .rangerect { 194 | shape-rendering: crispEdges; 195 | stroke: #fff; 196 | fill-opacity: .30; 197 | } 198 | 199 | .threshold { 200 | shape-rendering: crispEdges; 201 | fill: none; 202 | stroke: #d42111; 203 | } 204 | .threshold-indicator { 205 | cursor: ns-resize; 206 | } 207 | 208 | .calibration-event { 209 | shape-rendering: crispEdges; 210 | fill: none; 211 | stroke: #000; 212 | filter: alpha(opacity=100); 213 | opacity: 1.0; 214 | } 215 | 216 | .calibration-indicator { 217 | fill: none; 218 | stroke: #000; 219 | stroke-width: 1px; 220 | filter: alpha(opacity=50); 221 | opacity: 0.5; 222 | } 223 | 224 | .calibration-indicator.inactive { 225 | filter: alpha(opacity=20); 226 | opacity: 0.2; 227 | } 228 | 229 | .sunset { 230 | fill: #888; 231 | } 232 | 233 | .sunrise { 234 | fill: white; 235 | } 236 | 237 | .removed { 238 | filter: alpha(opacity=20); 239 | opacity: 0.2; 240 | } 241 | 242 | th.include, td.include { 243 | text-align: center; 244 | } 245 | 246 | .problem { 247 | shape-rendering: crispEdges; 248 | stroke: none; 249 | stroke-width: 0; 250 | fill: red; 251 | 252 | filter: alpha(opacity=20); 253 | opacity: 0.2; 254 | } 255 | 256 | .info-hud text { 257 | shape-rendering: crispEdges; 258 | font-size: 11px; 259 | stroke-width: 0; 260 | stroke: none; 261 | fill: #555; 262 | } 263 | .info-hud > rect { 264 | fill: #fff; 265 | stroke-width: 0; 266 | stroke: none; 267 | filter: alpha(opacity=50); 268 | opacity: 0.5; 269 | } -------------------------------------------------------------------------------- /app/exporter.js: -------------------------------------------------------------------------------- 1 | var app = app || Base.extend(); 2 | 3 | (function() { 4 | 5 | app.exporter = Base.extend((function() { 6 | // http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop 7 | // All browsers respect definition order with the exception of Chrome and 8 | // Opera which do for every non-numerical property name. 9 | // Really need to test this output 10 | 11 | return { 12 | exportCsv: function(dataOut) { 13 | 14 | // one more pass converting date values to iso 15 | 16 | _.each(dataOut, function(d) { 17 | d.datetime = d.datetime.toISOString(); 18 | }); 19 | 20 | // add csv fields 21 | dataOut.unshift({ 22 | datetime: 'datetime', 23 | light: 'light', 24 | twilight: 'twilight', 25 | interp: 'interp', 26 | excluded: 'excluded' 27 | }); 28 | 29 | var csv = this.arrayOfObjectsToCSV(dataOut); 30 | saveAs( 31 | new Blob([csv], { 32 | type: "text/csv;charset=" + document.characterSet 33 | }), app.get('tagname')+".csv" 34 | ); 35 | 36 | //var uriContent = "data:text/csv;charset=utf-8," + encodeURIComponent(csv); 37 | //var myWindow = window.open(uriContent, "Tilde CSV"); 38 | }, 39 | 40 | exportJson: function(dataOut) { 41 | 42 | // R serializes table data strangely but economically 43 | 44 | var tilde = { 45 | datetime: [], 46 | light: [], 47 | twilight: [], 48 | interp: [], 49 | excluded: [] 50 | } 51 | 52 | _.each(dataOut, function(d) { 53 | tilde.datetime.push(d.datetime); 54 | tilde.light.push(d.light); 55 | tilde.twilight.push(d.twilight); 56 | tilde.interp.push(d.interp); 57 | tilde.excluded.push(d.excluded); 58 | }); 59 | 60 | // and we have metadata 61 | var location = this.formattedReleaseLocation(app.get('releaseLocation')); 62 | var allOut = { 63 | tilde: tilde, 64 | latlngs: app.get('birdLocations'), 65 | metadata: { 66 | tagname: app.get('tagname'), 67 | tagid: app.get('tagname'), 68 | solar_elevation: app.get('sunangle'), 69 | calibration: [ 70 | { 71 | location: location, 72 | start_time: app.get('calibrationPeriod')[0], 73 | stop_time: app.get('calibrationPeriod')[1] 74 | } 75 | ], 76 | release_location: location, 77 | release_time: "NA", 78 | recapture_location: "NA", 79 | recapture_time: "NA", 80 | notes: app.get('notes'), 81 | species: "NA" 82 | } 83 | } 84 | 85 | var json = JSON.stringify(allOut, null, " "); 86 | saveAs( 87 | new Blob([json], { 88 | type: "application/json;charset=" + document.characterSet 89 | }), app.get('tagname')+".json" 90 | ); 91 | }, 92 | 93 | exportMapCsv: function(dataOut) { 94 | dataOut.unshift(['lng', 'lat', 'tFirst', 'tSecond']); 95 | var csv = this.arrayOfArraysToCSV(dataOut); 96 | saveAs( 97 | new Blob([csv], { 98 | type: "text/csv;charset=" + document.characterSet 99 | }), app.get('tagname')+"_latlng.csv" 100 | ); 101 | }, 102 | 103 | tildeData: function() { 104 | var data = _.map(app.get('data'), function(d) { 105 | return _.clone(d); // js references are problematic 106 | }); 107 | 108 | // add required csv fields to existing light data 109 | 110 | _.each(data, function(d) { 111 | d.twilight = 0; 112 | d.interp = "FALSE"; 113 | d.excluded = "FALSE"; 114 | }); 115 | 116 | // format twilight events 117 | 118 | var events = _.map(app.get('events'), function(e) { 119 | var t = e.toObject(); 120 | var type = 0; 121 | if (t.type == "sunrise") type = 1; 122 | else if (t.type == "sunset") type = 2; 123 | var r = {}; 124 | 125 | r.datetime = t.datetime; 126 | r.light = t.threshold; 127 | r.twilight = type; 128 | r.interp = "TRUE"; 129 | r.excluded = (t.active?"FALSE":"TRUE"); 130 | 131 | return r; 132 | }); 133 | 134 | // add twilight events to light data and sort 135 | 136 | data = data.concat(events); 137 | data = _.sortBy(data, function(d) { 138 | return d.datetime; 139 | }); 140 | 141 | // filter out light data that has been duplicated by twilight events 142 | // and set interp to false for the twilight event 143 | 144 | var dataOut = []; 145 | for (var i = 0; i < data.length-1; i++) { 146 | if (data[i].datetime.getTime() != data[i+1].datetime.getTime()) { 147 | dataOut.push(data[i]); 148 | } else { 149 | // ensure the duplication is a light-twilight dup 150 | // and not a light-light 151 | if (data[i+1].twilight != 0) { 152 | data[i+1].interp = "FALSE"; 153 | } 154 | } 155 | } 156 | 157 | return dataOut; 158 | }, 159 | 160 | // copied from process.js: abstract to String or use some kind of 161 | // localization to handle formatting 162 | formattedReleaseLocation: function(location) { 163 | return _.map(location, function(x) { 164 | if (typeof x === "string") { 165 | return x.replace(',','.'); 166 | } else { 167 | return x; 168 | } 169 | }); 170 | }, 171 | 172 | arrayOfObjectsToCSV: function(array) { 173 | var keys = _.keys(array[0]); 174 | var csv = ''; 175 | _.each(array, function(d) { 176 | csv += _.map(keys,function(k) { 177 | return d[k]; 178 | }).join(','); 179 | csv += '\r\n'; 180 | }); 181 | return csv; 182 | }, 183 | 184 | arrayOfArraysToCSV: function(array) { 185 | var csv = ''; 186 | _.each(array, function(d) { 187 | csv += d.join(','); 188 | csv += '\r\n'; 189 | }); 190 | return csv; 191 | } 192 | }; 193 | })()); 194 | 195 | })(); -------------------------------------------------------------------------------- /lib/Blob.js: -------------------------------------------------------------------------------- 1 | /* Blob.js 2 | * A Blob implementation. 3 | * 2013-01-23 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * By Devin Samarin, https://github.com/eboyjr 7 | * License: X11/MIT 8 | * See LICENSE.md 9 | */ 10 | 11 | /*global self, unescape */ 12 | /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, 13 | plusplus: true */ 14 | 15 | /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ 16 | 17 | if (typeof Blob !== "function") 18 | var Blob = (function (view) { 19 | "use strict"; 20 | 21 | var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) { 22 | var 23 | get_class = function(object) { 24 | return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; 25 | } 26 | , FakeBlobBuilder = function BlobBuilder() { 27 | this.data = []; 28 | } 29 | , FakeBlob = function Blob(data, type, encoding) { 30 | this.data = data; 31 | this.size = data.length; 32 | this.type = type; 33 | this.encoding = encoding; 34 | } 35 | , FBB_proto = FakeBlobBuilder.prototype 36 | , FB_proto = FakeBlob.prototype 37 | , FileReaderSync = view.FileReaderSync 38 | , FileException = function(type) { 39 | this.code = this[this.name = type]; 40 | } 41 | , file_ex_codes = ( 42 | "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " 43 | + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" 44 | ).split(" ") 45 | , file_ex_code = file_ex_codes.length 46 | , real_URL = view.URL || view.webkitURL || view 47 | , real_create_object_URL = real_URL.createObjectURL 48 | , real_revoke_object_URL = real_URL.revokeObjectURL 49 | , URL = real_URL 50 | , btoa = view.btoa 51 | , atob = view.atob 52 | , can_apply_typed_arrays = false 53 | , can_apply_typed_arrays_test = function(pass) { 54 | can_apply_typed_arrays = !pass; 55 | } 56 | 57 | , ArrayBuffer = view.ArrayBuffer 58 | , Uint8Array = view.Uint8Array 59 | ; 60 | FakeBlob.fake = FB_proto.fake = true; 61 | while (file_ex_code--) { 62 | FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; 63 | } 64 | try { 65 | if (Uint8Array) { 66 | can_apply_typed_arrays_test.apply(0, new Uint8Array(1)); 67 | } 68 | } catch (ex) {} 69 | if (!real_URL.createObjectURL) { 70 | URL = view.URL = {}; 71 | } 72 | URL.createObjectURL = function(blob) { 73 | var 74 | type = blob.type 75 | , data_URI_header 76 | ; 77 | if (type === null) { 78 | type = "application/octet-stream"; 79 | } 80 | if (blob instanceof FakeBlob) { 81 | data_URI_header = "data:" + type; 82 | if (blob.encoding === "base64") { 83 | return data_URI_header + ";base64," + blob.data; 84 | } else if (blob.encoding === "URI") { 85 | return data_URI_header + "," + decodeURIComponent(blob.data); 86 | } if (btoa) { 87 | return data_URI_header + ";base64," + btoa(blob.data); 88 | } else { 89 | return data_URI_header + "," + encodeURIComponent(blob.data); 90 | } 91 | } else if (real_create_object_URL) { 92 | return real_create_object_URL.call(real_URL, blob); 93 | } 94 | }; 95 | URL.revokeObjectURL = function(object_URL) { 96 | if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { 97 | real_revoke_object_URL.call(real_URL, object_URL); 98 | } 99 | }; 100 | FBB_proto.append = function(data/*, endings*/) { 101 | var bb = this.data; 102 | // decode data to a binary string 103 | if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { 104 | if (can_apply_typed_arrays) { 105 | bb.push(String.fromCharCode.apply(String, new Uint8Array(data))); 106 | } else { 107 | var 108 | str = "" 109 | , buf = new Uint8Array(data) 110 | , i = 0 111 | , buf_len = buf.length 112 | ; 113 | for (; i < buf_len; i++) { 114 | str += String.fromCharCode(buf[i]); 115 | } 116 | } 117 | } else if (get_class(data) === "Blob" || get_class(data) === "File") { 118 | if (FileReaderSync) { 119 | var fr = new FileReaderSync; 120 | bb.push(fr.readAsBinaryString(data)); 121 | } else { 122 | // async FileReader won't work as BlobBuilder is sync 123 | throw new FileException("NOT_READABLE_ERR"); 124 | } 125 | } else if (data instanceof FakeBlob) { 126 | if (data.encoding === "base64" && atob) { 127 | bb.push(atob(data.data)); 128 | } else if (data.encoding === "URI") { 129 | bb.push(decodeURIComponent(data.data)); 130 | } else if (data.encoding === "raw") { 131 | bb.push(data.data); 132 | } 133 | } else { 134 | if (typeof data !== "string") { 135 | data += ""; // convert unsupported types to strings 136 | } 137 | // decode UTF-16 to binary string 138 | bb.push(unescape(encodeURIComponent(data))); 139 | } 140 | }; 141 | FBB_proto.getBlob = function(type) { 142 | if (!arguments.length) { 143 | type = null; 144 | } 145 | return new FakeBlob(this.data.join(""), type, "raw"); 146 | }; 147 | FBB_proto.toString = function() { 148 | return "[object BlobBuilder]"; 149 | }; 150 | FB_proto.slice = function(start, end, type) { 151 | var args = arguments.length; 152 | if (args < 3) { 153 | type = null; 154 | } 155 | return new FakeBlob( 156 | this.data.slice(start, args > 1 ? end : this.data.length) 157 | , type 158 | , this.encoding 159 | ); 160 | }; 161 | FB_proto.toString = function() { 162 | return "[object Blob]"; 163 | }; 164 | return FakeBlobBuilder; 165 | }(view)); 166 | 167 | return function Blob(blobParts, options) { 168 | var type = options ? (options.type || "") : ""; 169 | var builder = new BlobBuilder(); 170 | if (blobParts) { 171 | for (var i = 0, len = blobParts.length; i < len; i++) { 172 | builder.append(blobParts[i]); 173 | } 174 | } 175 | return builder.getBlob(type); 176 | }; 177 | }(self)); 178 | -------------------------------------------------------------------------------- /tests/lib/jasmine-1.3.1/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | #HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | #HTMLReporter a { text-decoration: none; } 5 | #HTMLReporter a:hover { text-decoration: underline; } 6 | #HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } 7 | #HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } 8 | #HTMLReporter #jasmine_content { position: fixed; right: 100%; } 9 | #HTMLReporter .version { color: #aaaaaa; } 10 | #HTMLReporter .banner { margin-top: 14px; } 11 | #HTMLReporter .duration { color: #aaaaaa; float: right; } 12 | #HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } 13 | #HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } 14 | #HTMLReporter .symbolSummary li.passed { font-size: 14px; } 15 | #HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } 16 | #HTMLReporter .symbolSummary li.failed { line-height: 9px; } 17 | #HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 18 | #HTMLReporter .symbolSummary li.skipped { font-size: 14px; } 19 | #HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } 20 | #HTMLReporter .symbolSummary li.pending { line-height: 11px; } 21 | #HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } 22 | #HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 23 | #HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 24 | #HTMLReporter .runningAlert { background-color: #666666; } 25 | #HTMLReporter .skippedAlert { background-color: #aaaaaa; } 26 | #HTMLReporter .skippedAlert:first-child { background-color: #333333; } 27 | #HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } 28 | #HTMLReporter .passingAlert { background-color: #a6b779; } 29 | #HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } 30 | #HTMLReporter .failingAlert { background-color: #cf867e; } 31 | #HTMLReporter .failingAlert:first-child { background-color: #b03911; } 32 | #HTMLReporter .results { margin-top: 14px; } 33 | #HTMLReporter #details { display: none; } 34 | #HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } 35 | #HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 36 | #HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 37 | #HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 38 | #HTMLReporter.showDetails .summary { display: none; } 39 | #HTMLReporter.showDetails #details { display: block; } 40 | #HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 41 | #HTMLReporter .summary { margin-top: 14px; } 42 | #HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } 43 | #HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } 44 | #HTMLReporter .summary .specSummary.failed a { color: #b03911; } 45 | #HTMLReporter .description + .suite { margin-top: 0; } 46 | #HTMLReporter .suite { margin-top: 14px; } 47 | #HTMLReporter .suite a { color: #333333; } 48 | #HTMLReporter #details .specDetail { margin-bottom: 28px; } 49 | #HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } 50 | #HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } 51 | #HTMLReporter .resultMessage span.result { display: block; } 52 | #HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 53 | 54 | #TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } 55 | #TrivialReporter a:visited, #TrivialReporter a { color: #303; } 56 | #TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } 57 | #TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } 58 | #TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } 59 | #TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } 60 | #TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } 61 | #TrivialReporter .runner.running { background-color: yellow; } 62 | #TrivialReporter .options { text-align: right; font-size: .8em; } 63 | #TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } 64 | #TrivialReporter .suite .suite { margin: 5px; } 65 | #TrivialReporter .suite.passed { background-color: #dfd; } 66 | #TrivialReporter .suite.failed { background-color: #fdd; } 67 | #TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } 68 | #TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } 69 | #TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } 70 | #TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } 71 | #TrivialReporter .spec.skipped { background-color: #bbb; } 72 | #TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } 73 | #TrivialReporter .passed { background-color: #cfc; display: none; } 74 | #TrivialReporter .failed { background-color: #fbb; } 75 | #TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } 76 | #TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } 77 | #TrivialReporter .resultMessage .mismatch { color: black; } 78 | #TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } 79 | #TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } 80 | #TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } 81 | #TrivialReporter #jasmine_content { position: fixed; right: 100%; } 82 | #TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } 83 | -------------------------------------------------------------------------------- /js/new.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Requires dataset_parser.js 4 | 5 | $(function() { 6 | "use strict"; 7 | 8 | var data = undefined; 9 | 10 | function handleFileSelect(evt) { 11 | var files = evt.target.files; // FileList object 12 | var file = files[0]; 13 | importFile(file); 14 | } 15 | 16 | function handleFileDrop(evt) { 17 | evt.stopPropagation(); 18 | evt.preventDefault(); 19 | 20 | var files = evt.dataTransfer.files; // FileList object. 21 | var file = files[0]; 22 | importFile(file); 23 | } 24 | 25 | function importFile(file) { 26 | var reader = new FileReader(); 27 | 28 | reader.onload = function(f) { 29 | 30 | var lines = f.target.result.trim().split(/\r?\n|\r/g); 31 | var format = (new DataSetParser).formatOf(lines[0]); 32 | var errors = 0; 33 | 34 | if (!format) { 35 | console.log("Unable to parse file"); 36 | $('#drop-zone').addClass("file-error"); 37 | $('#drop-zone .or-drop').html( 38 | "

Unable to parse this file. TAGS, BAS and LUX formats are currently supported. " + 39 | "Please refer to the help files " + 40 | "for more information.

"); 41 | return; 42 | } else { 43 | console.log("Parsed file format:", format.name); 44 | } 45 | 46 | if (format.hasHeader) { 47 | lines.shift(); 48 | } 49 | 50 | var vals = _.map(lines, function(x) { 51 | var match = x.match(format.re); 52 | //console.log(match); 53 | if (!match) { 54 | errors++; 55 | return undefined; 56 | } else { 57 | return { 58 | datetime: format.parse(match[format.dateIndex]), 59 | light: match[format.lightIndex] 60 | } 61 | } 62 | }); 63 | 64 | if (errors) { 65 | console.log("Encountered errors while parsing file"); 66 | $('#drop-zone').addClass("file-error"); 67 | $('#drop-zone .or-drop').html( 68 | "

The file format was recognized, but errors were encountered while parsing. " + 69 | "Please ensure that each line of the file includes a valid date and time

"); 70 | return; 71 | } 72 | 73 | console.log(vals); 74 | 75 | var template = _.template($('script.dataset-info-template').html()); 76 | $('#drop-zone .or-drop').html(template({ 77 | filename: escape(file.name), 78 | filesize: file.size, 79 | format: format.name, 80 | datacount: vals.length 81 | })); 82 | 83 | $('#drop-zone').removeClass("file-invalid"); 84 | $('#drop-zone').removeClass("file-error"); 85 | $('#drop-zone').addClass('file-success'); 86 | $('#file-input').removeClass('error'); 87 | data = vals; 88 | } 89 | reader.readAsText(file); 90 | } 91 | 92 | function handleDragOver(evt) { 93 | evt.stopPropagation(); 94 | evt.preventDefault(); 95 | evt.dataTransfer.dropEffect = 'copy'; 96 | } 97 | 98 | document.getElementById('file-input').addEventListener('change', handleFileSelect, false); 99 | 100 | var dropZone = document.getElementById('drop-zone'); 101 | dropZone.addEventListener('dragover', handleDragOver, false); 102 | dropZone.addEventListener('drop', handleFileDrop, false); 103 | 104 | 105 | var DateFormat = "yy-mm-dd"; 106 | $('input[name=release-date], input[name=capture-date]').datepicker({ 107 | showOtherMonths: true, 108 | selectOtherMonths: true, 109 | defaultDate: new Date(), 110 | dateFormat: DateFormat 111 | }); 112 | 113 | 114 | $('#create-new-dataset').submit(function() { 115 | if (!validateInput()) { 116 | $('#validation-error').show(); 117 | return false; 118 | } else { 119 | $('#validation-error').hide(); 120 | } 121 | 122 | var url = app.host + "/queue/run/geologger.importTagData@geologger"; 123 | var upload = dataForUpload(); 124 | 125 | //console.log(JSON.stringify(upload)); 126 | console.log("uploading"); 127 | 128 | $('#upload-indicator').spin({ 129 | color: '#333', 130 | radius: 4, 131 | width: 2 132 | }); 133 | $('.uploading-notification').show(); 134 | 135 | 136 | $.ajax({type:"POST", url: url, data: {data: JSON.stringify(upload), user_id: "true" }, dataType: "json", xhrFields: {withCredentials: true}, crossDomain: false }).then(function(data) { 137 | return (new CyberCommons()).getStatusOfTask(app.host, data.task_id); 138 | }).then(function(data) { 139 | console.log("post completed", data); 140 | showUploadSuccess(data); 141 | },function(error) { 142 | console.log("post failed", error); 143 | showUploadError(data); 144 | }); 145 | 146 | return false; 147 | }); 148 | 149 | function dataForUpload() { 150 | return { 151 | data: data, 152 | tagname: get('name'), 153 | species: get('species'), 154 | notes: getNotes(), 155 | 156 | // ignore these 157 | release_location: getLocation('release'), 158 | release_time: get('release-date'), 159 | recapture_location: getLocation('capture'), 160 | recapture_time: get('capture-date') 161 | 162 | }; 163 | } 164 | 165 | function validateInput() { 166 | var valid = true; 167 | valid = validateData() && valid; 168 | valid = validateName() && valid; 169 | return valid; 170 | } 171 | 172 | function validateData() { 173 | if (!data) { 174 | $('#drop-zone').addClass("file-invalid"); 175 | $('#file-input').addClass('error'); 176 | return false; 177 | } else { 178 | $('#drop-zone').removeClass("file-invalid"); 179 | $('#file-input').removeClass('error'); 180 | return true; 181 | } 182 | } 183 | 184 | function validateName() { 185 | if (get('name').length == 0) { 186 | $('#name').addClass('error'); 187 | return false; 188 | } else { 189 | $('#name').removeClass('error'); 190 | return true; 191 | } 192 | } 193 | 194 | function get(id) { 195 | return $('input[name='+id+']').val().trim(); 196 | } 197 | 198 | function getLocation(id) { 199 | var lat = parseFloat(get(id+'-latitude')); 200 | var lon = parseFloat(get(id+'-longitude')); 201 | if (isNaN(lat) || isNaN(lon)) return []; 202 | else return [lat, lon]; 203 | } 204 | 205 | function getNotes() { 206 | return $('textarea[name=notes]').val().trim(); 207 | } 208 | 209 | function showUploadSuccess(data) { 210 | // stop indicators 211 | $('.uploading-notification').hide(); 212 | $('#upload-indicator').spin(false); 213 | 214 | // reload dataset list 215 | showDatasets(function() { 216 | 217 | // build success html 218 | $('#upload-success .title').text(get('name')); 219 | $('#upload-success .link').attr('href', "datasets/"+get('name')); 220 | 221 | // show success notifications 222 | $('#upload-success').show(); 223 | $(window).scrollTop(0); 224 | 225 | // respond to a link click 226 | $('#upload-success .link').click(function() { 227 | $('#newbird').hide(); 228 | $('#viewbird').show(); 229 | loadDataset($('#upload-success .title').text()); 230 | return false; 231 | }); 232 | 233 | });; 234 | } 235 | 236 | function showUploadError(data) { 237 | $('.uploading-notification').hide(); 238 | $('#upload-indicator').spin(false); 239 | 240 | $('#upload-error').show(); 241 | $(window).scrollTop(0); 242 | } 243 | 244 | }); 245 | -------------------------------------------------------------------------------- /app/process.js: -------------------------------------------------------------------------------- 1 | var app = app || Base.extend(); 2 | 3 | (function() { 4 | app.process = Base.extend((function() { 5 | return { 6 | sunAngle: function() { 7 | if (!this.validateForSunAngle()) 8 | return false; 9 | 10 | var $indicator = $('#sun-angle-indicator'); 11 | var me = this; 12 | 13 | var url = app.get('host') + "/queue/run/geologger.getElevation@geologger"; 14 | var postdata = this.sunAngleData(); 15 | 16 | this.startProgressIndicator($indicator); 17 | 18 | $.post(url,{data: JSON.stringify(postdata), user_id: true}, null, "jsonp").then(function(data) { 19 | return (new CyberCommons()).getStatusOfTask(app.get('host'), data.task_id); 20 | }).then(function(data) { 21 | var angle = data.tombstone[0].result.sunelevation; 22 | me.stopProgressIndicator($indicator); 23 | app.set('angleComputed',true); 24 | app.set('sunangle',angle); 25 | },function(error) { 26 | me.stopProgressIndicator($indicator); 27 | log("post failed", error); 28 | }); 29 | }, 30 | 31 | sunAngleData: function() { 32 | var twilights = this.formattedEventData(app.get('events'),app.get('calibrationPeriod')); 33 | var location = this.formattedReleaseLocation(app.get('releaseLocation')); 34 | return { 35 | release_location: location, 36 | threshold: app.get('threshold'), 37 | tagname: app.get('tagname'), 38 | twilights: twilights 39 | }; 40 | }, 41 | 42 | locations: function() { 43 | if (!this.validateForProcessing()) 44 | return false; 45 | 46 | var $indicator = $('#submit-for-processing i'); 47 | var $text = $('#submit-for-processing .text'); 48 | var $btn = $('#submit-for-processing'); 49 | var me = this; 50 | 51 | var url = app.get('host') + "/queue/run/geologger.coord@geologger"; 52 | var postdata = this.locationsData(); 53 | 54 | this.startProgressIndicator($indicator); 55 | 56 | $.post(url,{data: JSON.stringify(postdata), user_id: true}, null, "json").then(function(data) { 57 | $text.text('Data are being processed'); 58 | return (new CyberCommons()).getStatusOfTask(app.get('host'), data.task_id); 59 | }).then(function(data) { 60 | $text.text('Success! Loading map data'); 61 | $btn.removeClass('btn-primary'); 62 | $btn.addClass('btn-info'); 63 | 64 | $('#app-tabs a[href="#map-tab"]').tab('show'); 65 | me.getGeoJSON(); 66 | 67 | me.stopProgressIndicator($indicator); 68 | me.resetProcessLocationsButton(); 69 | },function(error) { 70 | me.stopProgressIndicator($indicator); 71 | me.showProcessLocationsError(); 72 | log("post failed", error); 73 | }); 74 | }, 75 | 76 | locationsData: function() { 77 | var twilights = this.formattedEventData(app.get('events')); 78 | var location = this.formattedReleaseLocation(app.get('releaseLocation')); 79 | return { 80 | threshold: +app.get('threshold'), 81 | tagname: app.get('tagname'), 82 | twilights: twilights, 83 | calibperiod: app.get('calibrationPeriod'), 84 | sunelevation: app.get('sunangle'), 85 | computed: app.get('angleComputed'), 86 | release_location: location 87 | }; 88 | }, 89 | 90 | showProcessLocationsError: function() { 91 | var $text = $('#submit-for-processing .text'); 92 | var $btn = $('#submit-for-processing'); 93 | $text.text('Error! Try again in a few seconds'); 94 | $btn.removeClass('btn-primary'); 95 | $btn.addClass('btn-danger'); 96 | this.resetProcessLocationsButton(); 97 | }, 98 | 99 | resetProcessLocationsButton: function() { 100 | var $text = $('#submit-for-processing .text'); 101 | var $btn = $('#submit-for-processing'); 102 | setTimeout(function() { 103 | $text.text("Submit for processing"); 104 | $btn.removeClass('btn-danger'); 105 | $btn.removeClass('btn-info'); 106 | $btn.addClass('btn-primary'); 107 | },8000); 108 | }, 109 | 110 | getGeoJSON: function() { 111 | // move this into the locations processing promise? 112 | var me = this; 113 | var url = app.get('host') + '/geologger/coord/tagname/' + app.get('tagname'); 114 | 115 | $.getJSON(url).then(function(data) { 116 | me.updateCoordinates(data[0].features); 117 | app.map.drawGeoJSON(data[0].features); 118 | }, function(error) { 119 | log("geojson get failed", error); 120 | }); 121 | }, 122 | 123 | updateCoordinates: function(features) { 124 | // bird location includes the lng/lat as well as the datetimes passed 125 | // to the r script for processing 126 | app.set('birdLocations', _.map(features, function(d) { 127 | return [ 128 | d.geometry.coordinates[0], 129 | d.geometry.coordinates[1], 130 | d.properties.tFirst, 131 | d.properties.tSecond 132 | ] 133 | //return d.geometry.coordinates; 134 | })); 135 | }, 136 | 137 | validateForSunAngle: function() { 138 | this.hideAllLabelTooltips(); 139 | 140 | var twilights = this.formattedEventData(app.get('events'),app.get('calibrationPeriod')); 141 | var location = this.formattedReleaseLocation(app.get('releaseLocation')); 142 | 143 | if (!this.eventDataIsValid(twilights)) { 144 | this.showTooltipForLabel('cal-stop-date'); 145 | return false; 146 | } 147 | if (!this.lightThresholdIsValid(app.get('threshold'))) { 148 | this.showTooltipForLabel('threshold'); 149 | return false; 150 | } 151 | if (!this.releaseLocationIsValid(location)) { 152 | this.showTooltipForLabel('latitude'); 153 | return false; 154 | } 155 | if (!this.calibrationPeriodIsValid(app.get('calibrationPeriod'))) { 156 | this.showTooltipForLabel('cal-start-date'); 157 | return false; 158 | } 159 | return true; 160 | }, 161 | 162 | validateForProcessing: function() { 163 | this.hideAllLabelTooltips(); 164 | 165 | var twilights = this.formattedEventData(app.get('events')); 166 | var location = this.formattedReleaseLocation(app.get('releaseLocation')); 167 | 168 | if (!this.eventDataIsValid(twilights)) { 169 | this.showTooltipForLabel('cal-stop-date'); 170 | return false; 171 | } 172 | if (!this.lightThresholdIsValid(app.get('threshold'))) { 173 | this.showTooltipForLabel('threshold'); 174 | return false; 175 | } 176 | if (!this.releaseLocationIsValid(location)) { 177 | this.showTooltipForLabel('latitude'); 178 | return false; 179 | } 180 | if (!this.sunAngleIsValid(app.get('sunangle'))) { 181 | this.showTooltipForLabel('sun-angle'); 182 | return false; 183 | } 184 | return true; 185 | }, 186 | 187 | lightThresholdIsValid: function(threshold) { 188 | if (!threshold) return false; 189 | return true; 190 | }, 191 | 192 | releaseLocationIsValid: function(location) { 193 | var lat = location[0], lon = location[1]; 194 | if (location.length!=2 || lat === undefined || lon === undefined) { 195 | return false; 196 | } else { 197 | return (lat >= -90 && lat <= 90 && 198 | lon >= -180 && lon <= 180); 199 | } 200 | }, 201 | 202 | calibrationPeriodIsValid: function(period) { 203 | if (period.length!=2 || !period[0] || !period[1] || 204 | period[0] >= period[1]) { 205 | return false; 206 | } else { 207 | return true; 208 | } 209 | }, 210 | 211 | eventDataIsValid: function(data) { 212 | return data.length >= 2; 213 | }, 214 | 215 | calibrationCoordinatesAreValid: function(lat, lon) { 216 | return (lat >= -90 && lat <= 90 && 217 | lon >= -180 && lon <= 180); 218 | }, 219 | 220 | sunAngleIsValid: function(angle) { 221 | if (!angle && angle!==0) return false; 222 | return true; 223 | }, 224 | 225 | formattedEventData: function(data,range) { 226 | // convert datetime fields to tFirst and tSecond for R processing 227 | var fdata = _.map(data, function(d) { return d.toObject(); }); 228 | if (range) fdata = _.filter(fdata, function(d) { 229 | return d.datetime >= range[0] && d.datetime <= range[1]; 230 | }); 231 | 232 | for (var i = 0; i < fdata.length-1; i++) { 233 | fdata[i].tFirst = fdata[i].datetime; 234 | fdata[i].tSecond = fdata[i+1].datetime; 235 | delete fdata[i].datetime; 236 | delete fdata[i].threshold; 237 | delete fdata[i].problem; 238 | } 239 | return fdata.slice(0,-1); 240 | }, 241 | 242 | // copied to exporter.js: abstract to String or use some kind of 243 | // localization to handle formatting 244 | formattedReleaseLocation: function(location) { 245 | return _.map(location, function(x) { 246 | if (typeof x === "string") { 247 | return x.replace(',','.'); 248 | } else { 249 | return x; 250 | } 251 | }); 252 | }, 253 | 254 | startProgressIndicator: function($indicator) { 255 | $indicator.show(); 256 | }, 257 | 258 | stopProgressIndicator: function($indicator) { 259 | $indicator.hide(); 260 | }, 261 | 262 | hideAllLabelTooltips: function() { 263 | $('label[data-error=tooltip]').tooltip('destroy'); 264 | }, 265 | 266 | showTooltipForLabel: function(name) { 267 | $('[for='+name+']').tooltip('show'); 268 | } 269 | }; 270 | })()); 271 | })(); 272 | -------------------------------------------------------------------------------- /app/twilights.js: -------------------------------------------------------------------------------- 1 | var app = app || Base.extend(); 2 | 3 | (function() { 4 | 5 | app.twilights = Base.extend((function() { 6 | 7 | var eventSort = new Tablesort($('#table-events').get(0)); 8 | eventSort.current = $('#table-events .event-header').get(0); 9 | eventSort.sortTable($('#table-events .event-header').get(0)); 10 | 11 | var isSunrise = function(d) { return d.get('type') == "sunrise"; } 12 | var isSunset = function(d) { return d.get('type') == "sunset"; } 13 | 14 | var eventsLayer = d3.select("#chart svg .focus") 15 | .append('g').attr('class','twiglight-events'); 16 | 17 | function drawEvents() { 18 | 19 | var focus = d3.select("#chart svg .focus"); 20 | var events = app.get('events'); 21 | var x = app.chart.get('x'); 22 | var y = app.chart.get('y'); 23 | 24 | eventsLayer.selectAll('.calibration-indicator').remove(); 25 | eventsLayer.selectAll('.calibration-indicator') 26 | .data(events) 27 | .enter() 28 | .append('circle') 29 | .attr("clip-path", "url(#clip)") 30 | .attr("class","calibration-indicator") 31 | .attr('data-index', function(d) { return events.indexOf(d); }) 32 | .attr('id', function(d) { return 'data-index-'+events.indexOf(d); }) 33 | .classed("sunset", isSunset) 34 | .classed("sunrise", isSunrise) 35 | .classed('inactive', function(d) { return !d.get('active'); }) 36 | .attr("cx", function(d) { return x(d.get('datetime')); }) 37 | .attr("cy", y(app.get('threshold'))) 38 | .attr("r",4); 39 | 40 | eventsLayer.selectAll('.calibration-indicator') 41 | .on("click", function(d,i) { 42 | d.set('active',!d.get('active')); 43 | this.parentNode.insertBefore(this, this.parentNode.firstChild); 44 | d3.select(this).classed('inactive', !d.get('active')); 45 | }) 46 | .on("mouseover", function(d,i) { 47 | d3.select(this).transition() 48 | .attr('r',5); 49 | d3.select('.info-hud') 50 | .append('text') 51 | .attr('class','datetime') 52 | .attr('text-anchor','end') 53 | .attr('x',230) 54 | .attr('y',28) 55 | .text(d.get('datetime')); 56 | d3.select('.info-hud') 57 | .append('text') 58 | .attr('class','event-type') 59 | .attr('text-anchor','end') 60 | .attr('x',230) 61 | .attr('y',42) 62 | .text(d.get('type').capitalize()); 63 | }) 64 | .on("mouseout", function(d,i) { 65 | d3.select(this).transition() 66 | .attr('r',4); 67 | d3.select('.info-hud .datetime') 68 | .remove(); 69 | d3.select('.info-hud .event-type') 70 | .remove(); 71 | }); 72 | } 73 | 74 | function recomputeCalibrationEvents() { 75 | 76 | // WARNING: this algorithm slows down significantly as the size of the dataset increases 77 | 78 | var t = +app.get('threshold').toFixed(1); 79 | var data = app.get('data'); 80 | var events = []; 81 | 82 | for ( var i = 1; i < data.length-1; i++ ) { 83 | var p1 = data[i], p2 = data[i+1], pm1 = data[i-1]; 84 | if (lightValueCrossesThreshold(p1,p2,t)) { 85 | var twilightEvent = twilightEventAt(p1,p2,pm1,t); 86 | if ( twilightEvent) events.push(twilightEvent); 87 | } 88 | }; 89 | 90 | return events; 91 | } 92 | 93 | function twilightEventAt(p1,p2,pm1,t) { 94 | var attrs = { threshold: t, active: true }; 95 | 96 | if ( lightValueIsAtThreshold(p1,t) ) { 97 | attrs.type = twilightTypeAtThreshold(pm1, p2, t); 98 | attrs.datetime = p1.datetime; 99 | } else if ( lightValueCrossesThresholdFalling(p1,p2,t) ) { 100 | attrs.datetime = new Date(x3at(p1,p2,t)); 101 | attrs.type = 'sunset'; 102 | } else if ( lightValueCrossesThresholdRising(p1,p2,t) ) { 103 | attrs.datetime = new Date(x3at(p1,p2,t)); 104 | attrs.type = 'sunrise'; 105 | } else { 106 | return null; 107 | } 108 | 109 | return Base.extend(null,attrs); 110 | } 111 | 112 | function lightValueIsAtThreshold(p,t) { 113 | return (p.light == t); 114 | } 115 | 116 | function lightValueCrossesThresholdFalling(p1,p2,t) { 117 | return (p1.light > t && p2.light < t); 118 | } 119 | 120 | function lightValueCrossesThresholdRising(p1,p2,t) { 121 | return (p1.light < t && p2.light > t); 122 | } 123 | 124 | function lightValueCrossesThreshold(d1,d2,t) { 125 | return ( lightValueIsAtThreshold(d1,t) || 126 | lightValueCrossesThresholdFalling(d1,d2,t) || 127 | lightValueCrossesThresholdRising(d1,d2,t) ); 128 | } 129 | 130 | function twilightTypeAtThreshold(da,db,t) { 131 | if ( da.light >= t && db.light <= t ) return 'sunset'; 132 | else return 'sunrise'; 133 | } 134 | 135 | function x3at(point1, point2, y3) { 136 | var x1 = point1.datetime.getTime(), 137 | y1 = point1.light, 138 | x2 = point2.datetime.getTime(), 139 | y2 = point2.light; 140 | 141 | var m = (y2-y1)/(x2-x1); 142 | var x3 = ((y3-y1)+(x1*m))/m; 143 | return x3; 144 | } 145 | 146 | function recomputeProblemEvents(events) { 147 | 148 | // spec: a problem knows which events it encompasses, 149 | // and events know which problem they lie within 150 | 151 | var kspan = OneHour*3; 152 | var problems = []; 153 | 154 | for (var i = 0; i < events.length-1; i++) { 155 | var start = events[i].get('datetime'); 156 | 157 | var r = 1; // range 158 | while ( i+r < events.length ) { // misses final problem 159 | var stop = events[i+r].get('datetime'); 160 | if ( stop.getTime() - start.getTime() > kspan ) { 161 | if ( r > 1 ) { 162 | var actStop = events[i+r-1].get('datetime'); 163 | var problem = Base.extend(null,{ 164 | events: events.slice(i,i+r), 165 | start: start, 166 | stop: actStop, 167 | active: true 168 | }); 169 | events.slice(i,i+r).forEach(function(d) { 170 | d.set('problem',problem); 171 | }); 172 | problems.push(problem); 173 | i = i+r-1; // skip 174 | } 175 | break; 176 | } 177 | r++; 178 | } 179 | } 180 | 181 | return problems; 182 | } 183 | 184 | function activeProblems() { 185 | return _.filter(app.get('problems'), function(d) { 186 | return d.get('active'); 187 | }); 188 | } 189 | 190 | function problemIsActive(problem) { 191 | return ( _.reduce( problem.get('events'), function(memo,e) { 192 | return memo + ( e.get('active') ? 1 : 0 ); 193 | }, 0) > 1 ); 194 | } 195 | 196 | function drawProblems() { 197 | var background = d3.select("#chart svg .focus .background"); 198 | var problems = activeProblems(); 199 | var x = app.chart.get('x'); 200 | 201 | background.selectAll('.problem').remove(); 202 | background.selectAll('.problem') 203 | .data(problems).enter() 204 | .append('rect') 205 | .attr("clip-path", "url(#clip)") 206 | .attr('class', 'problem') 207 | .attr('x', function(d) { return x(d.get('start'))-7; }) 208 | .attr('y',0) 209 | .attr('width',function(d) { 210 | return x(d.get('stop'))-x(d.get('start'))+14; 211 | }) 212 | .attr('height',280); 213 | 214 | var description = "Problem areas: " + String(problems.length); 215 | d3.select('.info-hud .problem-description').remove(); 216 | d3.select('.info-hud') 217 | .append('text') 218 | .attr('class','problem-description') 219 | .attr('text-anchor','end') 220 | .attr('x',230) 221 | .attr('y',14) 222 | .text(description); 223 | } 224 | 225 | function clearExcludedEventsTable() { 226 | $('#table-events tbody .event').remove(); 227 | $('#table-events tbody .nodata').css('display','table-row'); 228 | } 229 | 230 | function updateExcludedEventsTable(event) { 231 | var id = event.get('datetime').getTime(); 232 | var tbody = $('#table-events tbody'); 233 | var row = tbody.children('[data-datetime='+id+']'); 234 | if (row.length > 0 && event.get('active')) { 235 | row.remove(); 236 | } else if (row.length == 0 && !event.get('active')) { 237 | var template = $('script#event-row').html(); 238 | var newrow = tbody.append( _.template(template,{ 239 | datetime: event.get('datetime').toUTCString(), 240 | type: event.get('type'), 241 | id: id 242 | })); 243 | newrow.find('a').click(handleEventTableClick); 244 | } 245 | // update the visibility of the nodata row 246 | if ( tbody.children('.event').length > 0 ) { 247 | tbody.children('.nodata').css('display','none'); 248 | } else { 249 | tbody.children('.nodata').css('display','table-row'); 250 | } 251 | // and sort 252 | eventSort.refresh(); 253 | } 254 | 255 | function updateVisibilityInExcludedEventsTable(range) { 256 | $('#table-events tbody tr').each(function(i) { 257 | var dt = $(this).attr('data-datetime'); 258 | if ( dt >= range[0] && dt <= range[1] ) $(this).addClass('visible'); 259 | else $(this).removeClass('visible'); 260 | }); 261 | } 262 | 263 | // selecting an event in the excluded table shows it on the chart 264 | 265 | function handleEventTableClick() { 266 | var center = $(this).attr('data-datetime'); 267 | var visible = app.chart.extentRange(); 268 | var start = app.chart.constrainToDomainMin(center-visible/2); 269 | var stop = app.chart.constrainToDomainMax(start+visible); 270 | app.chart.scrollTo(start,stop); 271 | return false; 272 | }; 273 | 274 | app.addObserver('threshold', function(t) { 275 | var events = recomputeCalibrationEvents(); 276 | if (events) app.set('events',events); 277 | var problems = recomputeProblemEvents(events); 278 | if (problems) app.set('problems',problems); 279 | clearExcludedEventsTable(); 280 | drawEvents(); 281 | }); 282 | 283 | app.addObserver('extent', function(e) { 284 | drawEvents(); 285 | drawProblems(); 286 | updateVisibilityInExcludedEventsTable(e); 287 | }); 288 | 289 | app.addObserver('problems',function(p) { 290 | drawProblems(); 291 | }); 292 | 293 | app.addObserver('events', function(evts) { 294 | evts.addObserver('active', function(a) { 295 | var problem = this.get('problem'); 296 | if (problem) { 297 | problem.set('active', problemIsActive(problem) ); 298 | drawProblems(); 299 | } 300 | updateExcludedEventsTable(this); 301 | updateVisibilityInExcludedEventsTable(app.get('extent')); 302 | }); 303 | }); 304 | 305 | return { 306 | 307 | // previousProblem, nextProblem: 308 | // find the problem before/after the max current extent 309 | // and center on it 310 | 311 | previousProblem: function() { 312 | return _.find(_.reversecopy(activeProblems()), function(x) { 313 | return x.get('stop').getTime() < app.chart.extentCenter()-OneHour; 314 | }); 315 | }, 316 | 317 | nextProblem: function() { 318 | return _.find(activeProblems(), function(x) { 319 | return x.get('start').getTime() > app.chart.extentCenter()+OneHour; 320 | }); 321 | }, 322 | 323 | gotoPreviousProblem: function() { 324 | var visiblility = app.chart.extentRange(); 325 | var previous = this.previousProblem(); 326 | if (previous) { 327 | var start = app.chart.constrainToDomainMin( 328 | previous.get('start').getTime()-visiblility/2 329 | ); 330 | app.chart.scrollTo(start,start+visiblility); 331 | } 332 | }, 333 | 334 | gotoNextProblem: function() { 335 | var visiblility = app.chart.extentRange(); 336 | var next = this.nextProblem(); 337 | if (next) { 338 | var stop = app.chart.constrainToDomainMax( 339 | next.get('start').getTime()+visiblility/2 340 | ); 341 | app.chart.scrollTo(stop-visiblility,stop); 342 | } 343 | } 344 | }; 345 | 346 | })()); 347 | 348 | })(); -------------------------------------------------------------------------------- /app/chart.js: -------------------------------------------------------------------------------- 1 | var app = app || Base.extend(); 2 | 3 | // readyState: 0=initial, 1=loading, 2=loaded 4 | 5 | (function() { 6 | 7 | app.chart = Base.extend((function() { 8 | 9 | var thIndicator, 10 | thLine; 11 | 12 | var margin = {top: 20, right: 20, bottom: 100, left: 50}, 13 | margin2 = {top: 330, right: 20, bottom: 20, left: 50}, 14 | width = 940 - margin.left - margin.right, 15 | height = 400 - margin.top - margin.bottom, 16 | height2 = 400 - margin2.top - margin2.bottom; 17 | 18 | var x = d3.time.scale().range([0, width]), 19 | x2 = d3.time.scale().range([0, width]), 20 | y = d3.scale.linear().range([height, 0]), 21 | y2 = d3.scale.linear().range([height2, 0]); 22 | 23 | var xAxis = d3.svg.axis().scale(x).orient("bottom"), 24 | xAxis2 = d3.svg.axis().scale(x2).orient("bottom"), 25 | yAxis = d3.svg.axis().scale(y).orient("left"); 26 | 27 | var brush = d3.svg.brush() 28 | .x(x2) 29 | .on("brush", brushdrag); 30 | 31 | // functions 32 | 33 | var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse; 34 | var parseUTCDate = d3.time.format.iso.parse; 35 | 36 | // draws the primary view 37 | 38 | var line = d3.svg.line() 39 | .x(function(d) { return x(d.datetime); }) 40 | .y(function(d) { return y(d.light); }); 41 | 42 | // draws the brush overview 43 | 44 | var line2 = d3.svg.line() 45 | .x(function(d) { return x2(d.datetime); }) 46 | .y(function(d) { return y2(d.light); }); 47 | 48 | // build the chart 49 | 50 | var svg = d3.select("#chart").append("svg") 51 | .attr("width", width + margin.left + margin.right) 52 | .attr("height", height + margin.top + margin.bottom); 53 | 54 | svg.append("defs").append("clipPath") 55 | .attr("id", "clip") 56 | .append("rect") 57 | .attr("width", width) 58 | .attr("height", height); 59 | 60 | // primary viewing area 61 | 62 | var focus = svg.append("g") 63 | .attr('class','focus') 64 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 65 | 66 | // with background 67 | 68 | focus.append('g').attr('class','background'); 69 | 70 | // data grouping, comparator in background, points in foreground 71 | 72 | var datagroup = focus.append('g').attr('class','datagroup'); 73 | var comparator = datagroup.append('g').attr('class','comparator'); 74 | var datapointer = focus.append('g').attr('class','datapointer'); 75 | 76 | // and info overlay 77 | 78 | focus.append('g').attr('class','info-hud') 79 | .attr('transform', "translate(" + (width-240) + "," + 0 + ")") 80 | .append('rect') 81 | .attr('x',0) 82 | .attr('y',0) 83 | .attr('width',240) 84 | .attr('height',48); 85 | 86 | // overview area 87 | 88 | var context = svg.append("g") 89 | .attr('class','context') 90 | .attr("transform", "translate(" + margin2.left + "," + margin2.top + ")"); 91 | 92 | // with background 93 | 94 | context.append('g').attr('class', 'background'); 95 | 96 | function brushdrag(evt) { 97 | // should not need to duplicate the plotting functionality 98 | x.domain(brush.empty() ? x2.domain() : brush.extent()); 99 | app.set('extent',x.domain()); 100 | focus.select(".x.axis").call(xAxis); 101 | redrawPaths(); 102 | } 103 | 104 | function redrawThreshold(t) { 105 | thLine 106 | .attr("y1", y(t.toFixed(1))) 107 | .attr("y2", y(t.toFixed(1))); 108 | thIndicator 109 | .attr('y', y(t.toFixed(1))-8); 110 | } 111 | 112 | function scrollTo(start,stop) { 113 | svg.select('.brush').call(brush.extent([start,stop])); 114 | brushdrag(); 115 | } 116 | 117 | function shiftData(data, delta) { 118 | return _.map(data, function(d) { 119 | var event = _.extend({},d); 120 | event.datetime = new Date(d.datetime.valueOf()+delta); 121 | return event; 122 | }); 123 | } 124 | 125 | function zonedDateString(datestring) { 126 | // firefox requires the Z, mongo remove it 127 | return datestring.charAt(datestring.length-1) == 'Z' ? 128 | datestring : 129 | datestring+"Z" 130 | } 131 | 132 | function redrawPaths() { 133 | 134 | // DUPLICATED 135 | var start = x.domain()[0].getTime(); 136 | var stop = x.domain()[1].getTime(); 137 | 138 | var data = _.filterUntil(undefined,app.get('data'),function(d) { 139 | if ( d.datetime.getTime() > stop+OneDay ) { 140 | return undefined; 141 | } 142 | return ( d.datetime.getTime() >= start-OneDay && 143 | d.datetime.getTime() <= stop+OneDay ); 144 | }); 145 | 146 | d3.select('.line.dayof').remove(); 147 | datagroup.append("path") 148 | .datum(data) 149 | .attr("clip-path", "url(#clip)") 150 | .attr("class", "line dayof") 151 | .attr("d", line); 152 | 153 | // DUPLICATED 154 | if ( app.get('showSurroundingDays') ) { 155 | 156 | var daybefore = shiftData(data,-OneDay); 157 | d3.select('.line.daybefore').remove(); 158 | comparator.append("path") 159 | .datum(daybefore) 160 | .attr("clip-path", "url(#clip)") 161 | .attr("class", "line daybefore") 162 | .attr("d", line); 163 | 164 | var dayafter = shiftData(data,OneDay); 165 | d3.select('.line.dayafter').remove(); 166 | comparator.append("path") 167 | .datum(dayafter) 168 | .attr("clip-path", "url(#clip)") 169 | .attr("class", "line dayafter") 170 | .attr("d", line); 171 | } 172 | } 173 | 174 | app.addObserver('threshold', redrawThreshold); 175 | 176 | return { 177 | 178 | load: function(filepath) { 179 | 180 | var me = this; 181 | var url = app.get('host') + '/geologger/lightlogs/tagname/' + filepath; 182 | url += "?callback=?" 183 | 184 | me.set('readyState',1); 185 | 186 | $.ajax({ 187 | type:"GET", 188 | url: url, 189 | data: {user_id: "true"}, 190 | dataType: "json", 191 | xhrFields: {withCredentials: true}, 192 | crossDomain: false 193 | }).then(function(json) { 194 | // set metadata 195 | if (json[0].release_location) app.set('releaseLocation',json[0].release_location); 196 | if (json[0].tagname) app.set('tagname',json[0].tagname); 197 | if (json[0].notes) app.set('notes',json[0].notes); 198 | 199 | // set the data 200 | me.loadData(json[0].data); 201 | },function(error) { 202 | console.log("get failed", error); 203 | }); 204 | 205 | /* 206 | me.set('readyState',1); 207 | d3.json(url, function(error,json) { 208 | // set metadata 209 | app.set('releaseLocation',json[0].release_location); 210 | app.set('tagname',json[0].tagname); 211 | app.set('notes',json[0].notes); 212 | 213 | // set the data 214 | me.loadData(json[0].data); 215 | }); 216 | */ 217 | }, 218 | 219 | loadData: function(data) { 220 | 221 | data.forEach(function(d) { 222 | d.datetime = parseUTCDate(zonedDateString(d.datetime)); 223 | d.light = +d.light; 224 | }); 225 | 226 | app.set('data',data); 227 | 228 | x.domain(d3.extent(data, function(d) { return d.datetime; })); 229 | y.domain(d3.extent(data, function(d) { return d.light; })); 230 | 231 | x2.domain(x.domain()); 232 | y2.domain(y.domain()); 233 | 234 | // data, day before, day after 235 | // drawing deferred to redrawPaths for optimization 236 | 237 | // downsample the data we're drawing for the context: take every fifth element 238 | var contextData = _.filter(data, function(el, index) { 239 | return index % 20 == 0; 240 | }); 241 | 242 | console.log("data size", data.length, "context size", contextData.length); 243 | 244 | context.append("path") 245 | .datum(contextData) 246 | .attr("class", "line") 247 | .attr("d", line2); 248 | 249 | // axes 250 | 251 | focus.append("g") 252 | .attr("class", "x axis") 253 | .attr("transform", "translate(0," + height + ")") 254 | .call(xAxis); 255 | 256 | context.append("g") 257 | .attr("class", "x axis") 258 | .attr("transform", "translate(0," + height2 + ")") 259 | .call(xAxis2); 260 | 261 | focus.append("g") 262 | .attr("class", "y axis") 263 | .call(yAxis) 264 | .append("text") 265 | .attr("transform", "rotate(-90)") 266 | .attr("y", 6) 267 | .attr("dy", "-3.5em") 268 | .attr("dx","-14em") 269 | .style("text-anchor", "end") 270 | .text("Light"); 271 | 272 | // brush 273 | 274 | context.append("g") 275 | .attr("class", "x brush") 276 | .call(brush) 277 | .selectAll("rect") 278 | .attr("y", -6) 279 | .attr("height", height2 + 7); 280 | 281 | // threshold 282 | 283 | var thresholdDrag = d3.behavior.drag() 284 | .on("drag", function(d) { 285 | var t = y.invert(d3.event.y); 286 | if ( t >= y.domain()[0] && t <= y.domain()[1] ) { 287 | app.set('threshold',t); 288 | } 289 | }); 290 | 291 | thLine = focus.append("line") 292 | .style("stroke-dasharray", ("2, 2")) 293 | .attr("class","threshold") 294 | .attr("x1", 6) 295 | .attr("y1", y(app.get('threshold'))) 296 | .attr("x2", 870) 297 | .attr("y2", y(app.get('threshold'))) 298 | .call(thresholdDrag); 299 | 300 | thIndicator = focus.append('image') 301 | .attr('class','threshold-indicator') 302 | .attr('xlink:href','img/threshold-triangle.png') 303 | .attr('height','16px') 304 | .attr('width','16px') 305 | .attr('x', -40) 306 | .attr('y', y(app.get('threshold'))-8) 307 | .call(thresholdDrag); 308 | 309 | // make the scale publicly available ~ do not set from outside 310 | 311 | this.set('x',x); 312 | this.set('y',y); 313 | 314 | this.set('x2',x2); 315 | this.set('y2',y2); 316 | 317 | // update the threshold to force redraw and recalibration 318 | // set an initial brush extent and calibration range 319 | 320 | app.set('threshold',app.get('threshold')); 321 | 322 | var start = data[0].datetime, 323 | focusstop = new Date(data[0].datetime.getTime()+OneDay*7), 324 | calibstop = new Date(data[0].datetime.getTime()+OneDay*14); 325 | 326 | app.set('calibrationPeriod',[start,calibstop]); 327 | scrollTo(start,focusstop); 328 | this.set('readyState',2); 329 | }, 330 | 331 | // composed utilities: make our methods pretty 332 | 333 | domainMin: function() { 334 | return x2.domain()[0].getTime(); 335 | }, 336 | 337 | domainMax: function() { 338 | return x2.domain()[1].getTime(); 339 | }, 340 | 341 | yMin: function() { 342 | return y.domain()[0]; 343 | }, 344 | 345 | yMax: function() { 346 | return y.domain()[1]; 347 | }, 348 | 349 | extentMin: function() { 350 | return x.domain()[0].getTime(); 351 | }, 352 | 353 | extentMax: function() { 354 | return x.domain()[1].getTime(); 355 | }, 356 | 357 | extentRange: function() { 358 | return this.extentMax() - this.extentMin(); 359 | }, 360 | 361 | extentCenter: function() { 362 | return this.extentMin() + this.extentRange() / 2; 363 | }, 364 | 365 | scrollTo: function(start,stop) { 366 | scrollTo(start,stop); 367 | }, 368 | 369 | constrainToDomainMax: function(time) { 370 | return ( time > this.domainMax() ? 371 | this.domainMax() : 372 | time ); 373 | }, 374 | 375 | constrainToDomainMin: function(time) { 376 | return ( time < this.domainMin() ? 377 | this.domainMin() : 378 | time ); 379 | }, 380 | 381 | constrainToLightRange: function(light) { 382 | if (light < this.yMin() ) light = this.yMin(); 383 | if (light > this.yMax() ) light = this.yMax(); 384 | return light; 385 | }, 386 | 387 | // zoomIn, zoomOut: 388 | // adjust the brush extent, ensuring we do not extend 389 | // beyong the edge or zoom in less than one quarter day 390 | 391 | zoomIn: function() { 392 | var start = this.constrainToDomainMin(this.extentMin()+OneQuarterDay); 393 | var stop = this.constrainToDomainMax(this.extentMax()-OneQuarterDay); 394 | if ( stop - start >= OneHalfDay ) { 395 | this.scrollTo(start,stop); 396 | } 397 | }, 398 | 399 | zoomOut: function() { 400 | var start = this.constrainToDomainMin(this.extentMin()-OneQuarterDay); 401 | var stop = this.constrainToDomainMax(this.extentMax()+OneQuarterDay); 402 | this.scrollTo(start,stop); 403 | }, 404 | 405 | // gotoPreviousSection, gotoNextSection: 406 | // adjust the brush extent, preserving width against the edge 407 | 408 | gotoPreviousSection: function() { 409 | var start = this.constrainToDomainMin( 410 | this.extentMin()-this.extentRange() 411 | ); 412 | this.scrollTo(start,start+this.extentRange()); 413 | }, 414 | 415 | gotoNextSection: function() { 416 | var stop = this.constrainToDomainMax( 417 | this.extentMax()+this.extentRange() 418 | ); 419 | this.scrollTo(stop-this.extentRange(),stop); 420 | }, 421 | 422 | gotoBeginning: function() { 423 | this.scrollTo(this.domainMin(),this.domainMin()+this.extentRange()); 424 | }, 425 | 426 | gotoEnd: function() { 427 | this.scrollTo(this.domainMax()-this.extentRange(),this.domainMax()); 428 | }, 429 | 430 | toggleSurroundingDays: function() { 431 | //$('.line.daybefore, .line.dayafter').toggle(); 432 | app.set('showSurroundingDays', !app.get('showSurroundingDays')); 433 | if ( !app.get('showSurroundingDays') ) { 434 | d3.select('.line.daybefore').remove(); 435 | d3.select('.line.dayafter').remove(); 436 | } else { 437 | redrawPaths(); 438 | } 439 | }, 440 | 441 | redrawPoints: function() { 442 | var start = this.extentMin(); 443 | var stop = this.extentMax(); 444 | var data = _.filter(app.get('data'), function(d) { 445 | return d.datetime.getTime() >= start && 446 | d.datetime.getTime() <= stop; 447 | }); 448 | 449 | datapointer.selectAll('.datapoint').remove(); 450 | datapointer.selectAll('.datapoint') 451 | .data(data).enter() 452 | .append('circle') 453 | .attr("clip-path", "url(#clip)") 454 | .attr('class','datapoint') 455 | .attr('cx',function(d) { return x(d.datetime); }) 456 | .attr('cy',function(d) { return y(d.light); }) 457 | .attr('r',1); 458 | }, 459 | 460 | togglePoints: function() { 461 | if ( this.pointsVisible() ) { 462 | datapointer.selectAll('.datapoint').remove(); 463 | } else { 464 | this.redrawPoints(); 465 | } 466 | }, 467 | 468 | pointsVisible: function() { 469 | return !datapointer.select('.datapoint').empty(); 470 | } 471 | }; 472 | })()); 473 | 474 | })(); 475 | -------------------------------------------------------------------------------- /bootstrap/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.2.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */@-ms-viewport{width:device-width}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TAGS: Datasets 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 40 | 41 |
42 |
43 |
44 | 68 | 69 |
70 |
71 | 72 |
73 | 74 |
75 | 78 | 79 |

80 | Select a dataset from the left, or 81 | upload a new datastet 82 |

83 |

84 | If you would like to upload private datasets,
85 | login or 86 | create an account 87 | with Cybercommons. 88 |

89 | 100 |
101 | 102 | 103 |
104 | 258 |
259 |
260 |
261 |
262 | 263 |
264 |

In order to upload files you must first be logged in.

265 |
266 | 267 | 270 | 271 | 275 | 276 | 279 | 280 | 318 | 319 | 506 | 507 | 508 | 509 | -------------------------------------------------------------------------------- /calibrate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TAGS: GeoLight Interface 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 50 | 51 |
52 | 53 |
54 |
55 | 56 |
57 |
58 | 61 | 64 | 67 | 70 |
71 |
72 | 75 | 78 |
79 |
80 | 83 | 86 |
87 | 88 |
89 | 92 | 106 | 110 |
111 | 112 |
113 | 114 |

PABU222150714_curtailed GeoLight

115 | 116 |
117 |
118 | 119 | 139 | 140 |
141 |
142 | 143 |
144 |
145 |
146 |

Loading Dataset

147 |
148 | 149 |
150 | 151 |
152 |
153 | 154 |
155 |
156 | 157 | 161 | 162 |
163 |
164 | 165 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 |
Excluded Events: Date & TimeEvent Type
No data
185 |
186 | 187 |
188 | 189 |
190 |
191 |
192 | 193 |
194 |
195 | 196 |
197 | 198 |
199 |
200 | 201 |
202 |
203 | 204 |
207 |
208 |
209 |
210 | 211 |
212 | 213 |
Calibration Coordinates
214 |
215 |
216 | 217 |
218 | 219 |
220 |
221 |
222 | 223 |
224 | 225 |
226 |
227 |
228 | 229 |
230 | 231 |
Calibration Period for sun angle
232 | 233 |
234 |
235 | 236 |
237 | 238 |
239 |
240 |
241 | 242 |
243 | 244 |
245 |
246 |
247 | 250 |
251 | Compute sun angle
252 | OR SUPPLY 253 |
254 |
255 |
256 | 257 |
258 |
259 | 260 | ° 261 |
262 |
263 |
264 |
265 | 266 |
267 | 268 | 272 |
273 |

274 | Downloads
275 | Light-Twilight | 276 | Lat-Lng | 277 | All Data 278 |

279 | 280 | 293 | 294 | 299 | 300 |
301 |
302 |
303 | 304 |
305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 545 | 546 | 547 | -------------------------------------------------------------------------------- /tests/lib/jasmine-1.3.1/jasmine-html.js: -------------------------------------------------------------------------------- 1 | jasmine.HtmlReporterHelpers = {}; 2 | 3 | jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { 4 | var el = document.createElement(type); 5 | 6 | for (var i = 2; i < arguments.length; i++) { 7 | var child = arguments[i]; 8 | 9 | if (typeof child === 'string') { 10 | el.appendChild(document.createTextNode(child)); 11 | } else { 12 | if (child) { 13 | el.appendChild(child); 14 | } 15 | } 16 | } 17 | 18 | for (var attr in attrs) { 19 | if (attr == "className") { 20 | el[attr] = attrs[attr]; 21 | } else { 22 | el.setAttribute(attr, attrs[attr]); 23 | } 24 | } 25 | 26 | return el; 27 | }; 28 | 29 | jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { 30 | var results = child.results(); 31 | var status = results.passed() ? 'passed' : 'failed'; 32 | if (results.skipped) { 33 | status = 'skipped'; 34 | } 35 | 36 | return status; 37 | }; 38 | 39 | jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { 40 | var parentDiv = this.dom.summary; 41 | var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; 42 | var parent = child[parentSuite]; 43 | 44 | if (parent) { 45 | if (typeof this.views.suites[parent.id] == 'undefined') { 46 | this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); 47 | } 48 | parentDiv = this.views.suites[parent.id].element; 49 | } 50 | 51 | parentDiv.appendChild(childElement); 52 | }; 53 | 54 | 55 | jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { 56 | for(var fn in jasmine.HtmlReporterHelpers) { 57 | ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; 58 | } 59 | }; 60 | 61 | jasmine.HtmlReporter = function(_doc) { 62 | var self = this; 63 | var doc = _doc || window.document; 64 | 65 | var reporterView; 66 | 67 | var dom = {}; 68 | 69 | // Jasmine Reporter Public Interface 70 | self.logRunningSpecs = false; 71 | 72 | self.reportRunnerStarting = function(runner) { 73 | var specs = runner.specs() || []; 74 | 75 | if (specs.length == 0) { 76 | return; 77 | } 78 | 79 | createReporterDom(runner.env.versionString()); 80 | doc.body.appendChild(dom.reporter); 81 | setExceptionHandling(); 82 | 83 | reporterView = new jasmine.HtmlReporter.ReporterView(dom); 84 | reporterView.addSpecs(specs, self.specFilter); 85 | }; 86 | 87 | self.reportRunnerResults = function(runner) { 88 | reporterView && reporterView.complete(); 89 | }; 90 | 91 | self.reportSuiteResults = function(suite) { 92 | reporterView.suiteComplete(suite); 93 | }; 94 | 95 | self.reportSpecStarting = function(spec) { 96 | if (self.logRunningSpecs) { 97 | self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 98 | } 99 | }; 100 | 101 | self.reportSpecResults = function(spec) { 102 | reporterView.specComplete(spec); 103 | }; 104 | 105 | self.log = function() { 106 | var console = jasmine.getGlobal().console; 107 | if (console && console.log) { 108 | if (console.log.apply) { 109 | console.log.apply(console, arguments); 110 | } else { 111 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 112 | } 113 | } 114 | }; 115 | 116 | self.specFilter = function(spec) { 117 | if (!focusedSpecName()) { 118 | return true; 119 | } 120 | 121 | return spec.getFullName().indexOf(focusedSpecName()) === 0; 122 | }; 123 | 124 | return self; 125 | 126 | function focusedSpecName() { 127 | var specName; 128 | 129 | (function memoizeFocusedSpec() { 130 | if (specName) { 131 | return; 132 | } 133 | 134 | var paramMap = []; 135 | var params = jasmine.HtmlReporter.parameters(doc); 136 | 137 | for (var i = 0; i < params.length; i++) { 138 | var p = params[i].split('='); 139 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 140 | } 141 | 142 | specName = paramMap.spec; 143 | })(); 144 | 145 | return specName; 146 | } 147 | 148 | function createReporterDom(version) { 149 | dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, 150 | dom.banner = self.createDom('div', { className: 'banner' }, 151 | self.createDom('span', { className: 'title' }, "Jasmine "), 152 | self.createDom('span', { className: 'version' }, version)), 153 | 154 | dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), 155 | dom.alert = self.createDom('div', {className: 'alert'}, 156 | self.createDom('span', { className: 'exceptions' }, 157 | self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'), 158 | self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))), 159 | dom.results = self.createDom('div', {className: 'results'}, 160 | dom.summary = self.createDom('div', { className: 'summary' }), 161 | dom.details = self.createDom('div', { id: 'details' })) 162 | ); 163 | } 164 | 165 | function noTryCatch() { 166 | return window.location.search.match(/catch=false/); 167 | } 168 | 169 | function searchWithCatch() { 170 | var params = jasmine.HtmlReporter.parameters(window.document); 171 | var removed = false; 172 | var i = 0; 173 | 174 | while (!removed && i < params.length) { 175 | if (params[i].match(/catch=/)) { 176 | params.splice(i, 1); 177 | removed = true; 178 | } 179 | i++; 180 | } 181 | if (jasmine.CATCH_EXCEPTIONS) { 182 | params.push("catch=false"); 183 | } 184 | 185 | return params.join("&"); 186 | } 187 | 188 | function setExceptionHandling() { 189 | var chxCatch = document.getElementById('no_try_catch'); 190 | 191 | if (noTryCatch()) { 192 | chxCatch.setAttribute('checked', true); 193 | jasmine.CATCH_EXCEPTIONS = false; 194 | } 195 | chxCatch.onclick = function() { 196 | window.location.search = searchWithCatch(); 197 | }; 198 | } 199 | }; 200 | jasmine.HtmlReporter.parameters = function(doc) { 201 | var paramStr = doc.location.search.substring(1); 202 | var params = []; 203 | 204 | if (paramStr.length > 0) { 205 | params = paramStr.split('&'); 206 | } 207 | return params; 208 | } 209 | jasmine.HtmlReporter.sectionLink = function(sectionName) { 210 | var link = '?'; 211 | var params = []; 212 | 213 | if (sectionName) { 214 | params.push('spec=' + encodeURIComponent(sectionName)); 215 | } 216 | if (!jasmine.CATCH_EXCEPTIONS) { 217 | params.push("catch=false"); 218 | } 219 | if (params.length > 0) { 220 | link += params.join("&"); 221 | } 222 | 223 | return link; 224 | }; 225 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter); 226 | jasmine.HtmlReporter.ReporterView = function(dom) { 227 | this.startedAt = new Date(); 228 | this.runningSpecCount = 0; 229 | this.completeSpecCount = 0; 230 | this.passedCount = 0; 231 | this.failedCount = 0; 232 | this.skippedCount = 0; 233 | 234 | this.createResultsMenu = function() { 235 | this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, 236 | this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'), 237 | ' | ', 238 | this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing')); 239 | 240 | this.summaryMenuItem.onclick = function() { 241 | dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, ''); 242 | }; 243 | 244 | this.detailsMenuItem.onclick = function() { 245 | showDetails(); 246 | }; 247 | }; 248 | 249 | this.addSpecs = function(specs, specFilter) { 250 | this.totalSpecCount = specs.length; 251 | 252 | this.views = { 253 | specs: {}, 254 | suites: {} 255 | }; 256 | 257 | for (var i = 0; i < specs.length; i++) { 258 | var spec = specs[i]; 259 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views); 260 | if (specFilter(spec)) { 261 | this.runningSpecCount++; 262 | } 263 | } 264 | }; 265 | 266 | this.specComplete = function(spec) { 267 | this.completeSpecCount++; 268 | 269 | if (isUndefined(this.views.specs[spec.id])) { 270 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom); 271 | } 272 | 273 | var specView = this.views.specs[spec.id]; 274 | 275 | switch (specView.status()) { 276 | case 'passed': 277 | this.passedCount++; 278 | break; 279 | 280 | case 'failed': 281 | this.failedCount++; 282 | break; 283 | 284 | case 'skipped': 285 | this.skippedCount++; 286 | break; 287 | } 288 | 289 | specView.refresh(); 290 | this.refresh(); 291 | }; 292 | 293 | this.suiteComplete = function(suite) { 294 | var suiteView = this.views.suites[suite.id]; 295 | if (isUndefined(suiteView)) { 296 | return; 297 | } 298 | suiteView.refresh(); 299 | }; 300 | 301 | this.refresh = function() { 302 | 303 | if (isUndefined(this.resultsMenu)) { 304 | this.createResultsMenu(); 305 | } 306 | 307 | // currently running UI 308 | if (isUndefined(this.runningAlert)) { 309 | this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" }); 310 | dom.alert.appendChild(this.runningAlert); 311 | } 312 | this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); 313 | 314 | // skipped specs UI 315 | if (isUndefined(this.skippedAlert)) { 316 | this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" }); 317 | } 318 | 319 | this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 320 | 321 | if (this.skippedCount === 1 && isDefined(dom.alert)) { 322 | dom.alert.appendChild(this.skippedAlert); 323 | } 324 | 325 | // passing specs UI 326 | if (isUndefined(this.passedAlert)) { 327 | this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" }); 328 | } 329 | this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); 330 | 331 | // failing specs UI 332 | if (isUndefined(this.failedAlert)) { 333 | this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"}); 334 | } 335 | this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount); 336 | 337 | if (this.failedCount === 1 && isDefined(dom.alert)) { 338 | dom.alert.appendChild(this.failedAlert); 339 | dom.alert.appendChild(this.resultsMenu); 340 | } 341 | 342 | // summary info 343 | this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); 344 | this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; 345 | }; 346 | 347 | this.complete = function() { 348 | dom.alert.removeChild(this.runningAlert); 349 | 350 | this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 351 | 352 | if (this.failedCount === 0) { 353 | dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount))); 354 | } else { 355 | showDetails(); 356 | } 357 | 358 | dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); 359 | }; 360 | 361 | return this; 362 | 363 | function showDetails() { 364 | if (dom.reporter.className.search(/showDetails/) === -1) { 365 | dom.reporter.className += " showDetails"; 366 | } 367 | } 368 | 369 | function isUndefined(obj) { 370 | return typeof obj === 'undefined'; 371 | } 372 | 373 | function isDefined(obj) { 374 | return !isUndefined(obj); 375 | } 376 | 377 | function specPluralizedFor(count) { 378 | var str = count + " spec"; 379 | if (count > 1) { 380 | str += "s" 381 | } 382 | return str; 383 | } 384 | 385 | }; 386 | 387 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView); 388 | 389 | 390 | jasmine.HtmlReporter.SpecView = function(spec, dom, views) { 391 | this.spec = spec; 392 | this.dom = dom; 393 | this.views = views; 394 | 395 | this.symbol = this.createDom('li', { className: 'pending' }); 396 | this.dom.symbolSummary.appendChild(this.symbol); 397 | 398 | this.summary = this.createDom('div', { className: 'specSummary' }, 399 | this.createDom('a', { 400 | className: 'description', 401 | href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()), 402 | title: this.spec.getFullName() 403 | }, this.spec.description) 404 | ); 405 | 406 | this.detail = this.createDom('div', { className: 'specDetail' }, 407 | this.createDom('a', { 408 | className: 'description', 409 | href: '?spec=' + encodeURIComponent(this.spec.getFullName()), 410 | title: this.spec.getFullName() 411 | }, this.spec.getFullName()) 412 | ); 413 | }; 414 | 415 | jasmine.HtmlReporter.SpecView.prototype.status = function() { 416 | return this.getSpecStatus(this.spec); 417 | }; 418 | 419 | jasmine.HtmlReporter.SpecView.prototype.refresh = function() { 420 | this.symbol.className = this.status(); 421 | 422 | switch (this.status()) { 423 | case 'skipped': 424 | break; 425 | 426 | case 'passed': 427 | this.appendSummaryToSuiteDiv(); 428 | break; 429 | 430 | case 'failed': 431 | this.appendSummaryToSuiteDiv(); 432 | this.appendFailureDetail(); 433 | break; 434 | } 435 | }; 436 | 437 | jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { 438 | this.summary.className += ' ' + this.status(); 439 | this.appendToSummary(this.spec, this.summary); 440 | }; 441 | 442 | jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() { 443 | this.detail.className += ' ' + this.status(); 444 | 445 | var resultItems = this.spec.results().getItems(); 446 | var messagesDiv = this.createDom('div', { className: 'messages' }); 447 | 448 | for (var i = 0; i < resultItems.length; i++) { 449 | var result = resultItems[i]; 450 | 451 | if (result.type == 'log') { 452 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 453 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 454 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 455 | 456 | if (result.trace.stack) { 457 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 458 | } 459 | } 460 | } 461 | 462 | if (messagesDiv.childNodes.length > 0) { 463 | this.detail.appendChild(messagesDiv); 464 | this.dom.details.appendChild(this.detail); 465 | } 466 | }; 467 | 468 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) { 469 | this.suite = suite; 470 | this.dom = dom; 471 | this.views = views; 472 | 473 | this.element = this.createDom('div', { className: 'suite' }, 474 | this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) 475 | ); 476 | 477 | this.appendToSummary(this.suite, this.element); 478 | }; 479 | 480 | jasmine.HtmlReporter.SuiteView.prototype.status = function() { 481 | return this.getSpecStatus(this.suite); 482 | }; 483 | 484 | jasmine.HtmlReporter.SuiteView.prototype.refresh = function() { 485 | this.element.className += " " + this.status(); 486 | }; 487 | 488 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView); 489 | 490 | /* @deprecated Use jasmine.HtmlReporter instead 491 | */ 492 | jasmine.TrivialReporter = function(doc) { 493 | this.document = doc || document; 494 | this.suiteDivs = {}; 495 | this.logRunningSpecs = false; 496 | }; 497 | 498 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { 499 | var el = document.createElement(type); 500 | 501 | for (var i = 2; i < arguments.length; i++) { 502 | var child = arguments[i]; 503 | 504 | if (typeof child === 'string') { 505 | el.appendChild(document.createTextNode(child)); 506 | } else { 507 | if (child) { el.appendChild(child); } 508 | } 509 | } 510 | 511 | for (var attr in attrs) { 512 | if (attr == "className") { 513 | el[attr] = attrs[attr]; 514 | } else { 515 | el.setAttribute(attr, attrs[attr]); 516 | } 517 | } 518 | 519 | return el; 520 | }; 521 | 522 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { 523 | var showPassed, showSkipped; 524 | 525 | this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' }, 526 | this.createDom('div', { className: 'banner' }, 527 | this.createDom('div', { className: 'logo' }, 528 | this.createDom('span', { className: 'title' }, "Jasmine"), 529 | this.createDom('span', { className: 'version' }, runner.env.versionString())), 530 | this.createDom('div', { className: 'options' }, 531 | "Show ", 532 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), 533 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), 534 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), 535 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") 536 | ) 537 | ), 538 | 539 | this.runnerDiv = this.createDom('div', { className: 'runner running' }, 540 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), 541 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."), 542 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) 543 | ); 544 | 545 | this.document.body.appendChild(this.outerDiv); 546 | 547 | var suites = runner.suites(); 548 | for (var i = 0; i < suites.length; i++) { 549 | var suite = suites[i]; 550 | var suiteDiv = this.createDom('div', { className: 'suite' }, 551 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), 552 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); 553 | this.suiteDivs[suite.id] = suiteDiv; 554 | var parentDiv = this.outerDiv; 555 | if (suite.parentSuite) { 556 | parentDiv = this.suiteDivs[suite.parentSuite.id]; 557 | } 558 | parentDiv.appendChild(suiteDiv); 559 | } 560 | 561 | this.startedAt = new Date(); 562 | 563 | var self = this; 564 | showPassed.onclick = function(evt) { 565 | if (showPassed.checked) { 566 | self.outerDiv.className += ' show-passed'; 567 | } else { 568 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); 569 | } 570 | }; 571 | 572 | showSkipped.onclick = function(evt) { 573 | if (showSkipped.checked) { 574 | self.outerDiv.className += ' show-skipped'; 575 | } else { 576 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); 577 | } 578 | }; 579 | }; 580 | 581 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { 582 | var results = runner.results(); 583 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; 584 | this.runnerDiv.setAttribute("class", className); 585 | //do it twice for IE 586 | this.runnerDiv.setAttribute("className", className); 587 | var specs = runner.specs(); 588 | var specCount = 0; 589 | for (var i = 0; i < specs.length; i++) { 590 | if (this.specFilter(specs[i])) { 591 | specCount++; 592 | } 593 | } 594 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); 595 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; 596 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); 597 | 598 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); 599 | }; 600 | 601 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { 602 | var results = suite.results(); 603 | var status = results.passed() ? 'passed' : 'failed'; 604 | if (results.totalCount === 0) { // todo: change this to check results.skipped 605 | status = 'skipped'; 606 | } 607 | this.suiteDivs[suite.id].className += " " + status; 608 | }; 609 | 610 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { 611 | if (this.logRunningSpecs) { 612 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 613 | } 614 | }; 615 | 616 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { 617 | var results = spec.results(); 618 | var status = results.passed() ? 'passed' : 'failed'; 619 | if (results.skipped) { 620 | status = 'skipped'; 621 | } 622 | var specDiv = this.createDom('div', { className: 'spec ' + status }, 623 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), 624 | this.createDom('a', { 625 | className: 'description', 626 | href: '?spec=' + encodeURIComponent(spec.getFullName()), 627 | title: spec.getFullName() 628 | }, spec.description)); 629 | 630 | 631 | var resultItems = results.getItems(); 632 | var messagesDiv = this.createDom('div', { className: 'messages' }); 633 | for (var i = 0; i < resultItems.length; i++) { 634 | var result = resultItems[i]; 635 | 636 | if (result.type == 'log') { 637 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 638 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 639 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 640 | 641 | if (result.trace.stack) { 642 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 643 | } 644 | } 645 | } 646 | 647 | if (messagesDiv.childNodes.length > 0) { 648 | specDiv.appendChild(messagesDiv); 649 | } 650 | 651 | this.suiteDivs[spec.suite.id].appendChild(specDiv); 652 | }; 653 | 654 | jasmine.TrivialReporter.prototype.log = function() { 655 | var console = jasmine.getGlobal().console; 656 | if (console && console.log) { 657 | if (console.log.apply) { 658 | console.log.apply(console, arguments); 659 | } else { 660 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 661 | } 662 | } 663 | }; 664 | 665 | jasmine.TrivialReporter.prototype.getLocation = function() { 666 | return this.document.location; 667 | }; 668 | 669 | jasmine.TrivialReporter.prototype.specFilter = function(spec) { 670 | var paramMap = {}; 671 | var params = this.getLocation().search.substring(1).split('&'); 672 | for (var i = 0; i < params.length; i++) { 673 | var p = params[i].split('='); 674 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 675 | } 676 | 677 | if (!paramMap.spec) { 678 | return true; 679 | } 680 | return spec.getFullName().indexOf(paramMap.spec) === 0; 681 | }; 682 | --------------------------------------------------------------------------------