75 | It's possible to embed the mobile tracker on any page. If you are developing a HAB project, you can add the tracker
76 | to your website. You can customize the tracker to fit. There are options to limit the visible vehicles to
77 | a specific callsign. Or two, or three. Many other options to play with. It's easy. Just visit the page below and check it out.
78 |
92 | Want to track with Google Earth instead? Just click here
93 |
94 |
Contribute
95 |
96 |
97 | Did you know the tracker is open-source? Check it out on
98 | github/habitat-mobile-tracker.
99 | Bug reports, suggestions and pull requests are welcome. You can also
100 | find us on IRC in #highaltitude at irc.freenode.org.
101 |
102 |
103 |
104 |
105 |
106 |
Settings
107 |
108 |
109 | Interpolate gaps in telemetry
110 |
111 |
112 |
113 |
114 |
115 |
116 | Hide welcome on start-up
117 |
118 |
119 |
120 |
121 |
122 |
123 | Imperial units
124 |
125 |
126 |
127 |
128 |
129 |
130 | Horizontal speed in hours
131 |
132 |
133 |
134 |
135 |
136 |
137 | Hide time display
138 |
139 |
140 |
141 |
142 |
143 |
144 | Hide receivers from the map
145 |
146 |
147 |
148 |
149 |
150 |
151 | Highlight selected vehicle
152 |
153 |
154 |
155 |
156 |
157 |
Overlays
158 |
159 |
160 | Daylight overlay
161 |
162 |
163 |
164 |
165 |
166 |
167 | APRS coverage
168 |
169 |
170 |
171 |
172 |
173 |
Other
174 |
175 |
176 | Availability offline
177 |
178 |
179 |
180 |
181 |
182 |
183 | Mobile station
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 | Chase car equipped with radio receiver
192 |
193 |
194 |
195 | Force check for new version
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | Reloads appcache if necessary.
204 | The size of cache files is 1.2 MB.
205 | This does not include map tiles.
206 |
207 |
208 |
209 |
210 |
211 |
212 |
Weather
213 |
216 |
217 |
218 |
Here you can access various weather overlays. This an experimental feature. Mobile users be aware that this can quickly eat your data allowance.
219 |
220 |
221 |
222 |
223 |
Chase car mode
224 |
225 |
226 | Enable
227 |
228 |
229 |
230 |
231 |
232 |
233 | Callsign
234 |
235 |
236 |
237 | Notice: If you enable this, your location will be uploaded to habitat; making it publicly visible on the map.
238 |
239 |
240 |
241 | Last updated
242 | never
243 |
244 |
245 | Latitude
246 | 0.000000
247 |
248 |
249 | Longitude
250 | 0.000000
251 |
252 |
253 | Altitude
254 | none
255 |
256 |
257 | Accuracy
258 | none
259 |
260 |
261 | Speed
262 | none
263 |
264 |
265 |
266 |
267 |
268 |
269 |
272 | UTC: ???
273 | Local: ???
274 |
275 |
276 |
277 |
280 | Azimuth: 360.0000
281 | 0° N
282 | Elevation: 90.0000
283 | 10000 km
284 |
No position available
285 |
No vehicle selected
286 |
287 |
288 |
289 |
290 |
291 |
Telemetry Graph
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
316 |
329 |
330 |
331 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | // detect if mobile
2 | var is_mobile = false;
3 |
4 | (function(a,b){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) is_mobile = true;})(navigator.userAgent||navigator.vendor||window.opera);
5 |
6 | // hash url
7 | var history_supported = (typeof history !== 'undefined');
8 |
9 | function lhash_update(history_step) {
10 | history_step = !!history_step;
11 |
12 | var url = document.location.href.split("#",1)[0];
13 | var hash = "";
14 |
15 | // generate hash
16 | hash += "mt=" + map.getMapTypeId();
17 | hash += "&mz=" + map.getZoom();
18 |
19 | if(!/^[a-z0-9]{32}$/ig.exec(wvar.query)) {
20 | hash += "&qm=" + wvar.mode.replace(/ /g, '_');
21 | }
22 |
23 | if(follow_vehicle === null || manual_pan) {
24 | var latlng = map.getCenter();
25 | hash += "&mc=" + roundNumber(latlng.lat(), 5) +
26 | "," + roundNumber(latlng.lng(), 5);
27 | }
28 |
29 | if(follow_vehicle !== null) {
30 | hash += "&f=" + follow_vehicle;
31 | }
32 |
33 | if(wvar.query !== "") {
34 | hash += "&q=" + wvar.query;
35 | $("header .search input[type='text']").val(wvar.query);
36 | }
37 |
38 | // other vars
39 | if(wvar.nyan) {
40 | hash += "&nyan=1";
41 | }
42 |
43 | hash = encodeURI(hash);
44 | // set state
45 | if(history_supported) {
46 | if(!history_step) {
47 | history.replaceState(null, null, url + "#!" + hash);
48 | } else {
49 | history.pushState(null, null, url + "#!" + hash);
50 | }
51 | } else {
52 | document.location.hash = "!" + hash;
53 | }
54 | }
55 |
56 | // wvar detection
57 | var wvar = {
58 | enabled: false,
59 | vlist: true,
60 | graph: true,
61 | graph_exapnded: false,
62 | focus: "",
63 | mode: (is_mobile) ? modeDefaultMobile : modeDefault,
64 | zoom: true,
65 | query: "!RS_*;",
66 | nyan: false,
67 | };
68 |
69 |
70 | function load_hash(no_refresh) {
71 | no_refresh = (no_refresh === null);
72 | var hash = window.location.hash.slice(2);
73 |
74 | if(hash === "") return;
75 |
76 | var parms = hash.split('&');
77 | var refresh = false;
78 | var refocus = false;
79 |
80 | // defaults
81 | manual_pan = false;
82 |
83 | var def = {
84 | mode: wvar.mode,
85 | zoom: true,
86 | focus: "",
87 | query: "",
88 | nyan: false,
89 | };
90 |
91 | parms.forEach(function(v) {
92 | v = v.split('=');
93 | k = v[0];
94 | v = decodeURIComponent(v[1]);
95 |
96 | switch(k) {
97 | case "mt":
98 | map.setMapTypeId(v);
99 | break;
100 | case "mz":
101 | map.setZoom(parseInt(v));
102 | break;
103 | case "mc":
104 | def.zoom = false;
105 | manual_pan = true;
106 | v = v.split(',');
107 | var latlng = new google.maps.LatLng(v[0], v[1]);
108 | map.setCenter(latlng);
109 | break;
110 | case "f":
111 | refocus = (follow_vehicle != v);
112 | follow_vehicle = v;
113 | def.focus = v;
114 | break;
115 | case "qm":
116 | def.mode = v.replace(/_/g, ' ');
117 | if(modeList.indexOf(def.mode) == -1) def.mode = (is_mobile) ? modeDefaultMobile : modeDefault;
118 | break;
119 | case "q":
120 | def.query = v;
121 | $("header .search input[type='text']").val(v);
122 | break;
123 | case "nyan":
124 | def[k] = !!parseInt(v);
125 | break;
126 | }
127 | });
128 |
129 | // check if we should force refresh
130 | ['mode','query','nyan'].forEach(function(k) {
131 | if(wvar[k] != def[k]) refresh = true;
132 | });
133 |
134 | $.extend(true, wvar, def);
135 |
136 | // force refresh
137 | if(!no_refresh) {
138 | if(refresh) {
139 | zoomed_in = false;
140 | clean_refresh(wvar.mode, true);
141 | }
142 | else if(refocus) {
143 | $(".row.active").removeClass('active');
144 | $(".vehicle"+vehicles[wvar.focus].uuid).addClass('active');
145 | followVehicle(wvar.focus, manual_pan, true);
146 | }
147 | }
148 |
149 | lhash_update();
150 | }
151 | window.onhashchange = load_hash;
152 |
153 | var params = window.location.search.substring(1).split('&');
154 |
155 | for(var idx in params) {
156 | var line = params[idx].split('=');
157 | if(line.length < 2) continue;
158 |
159 | switch(line[0]) {
160 | case "embed":
161 | if(line[1] == "1") {
162 | wvar.enabled = true;
163 | if(!is_mobile) wvar.mode = 'All';
164 | }
165 | break;
166 | case "hidelist": if(line[1] == "1") wvar.vlist = false; break;
167 | case "hidegraph": if(line[1] == "1") wvar.graph = false; break;
168 | case "expandgraph": if(line[1] == "1") wvar.graph_expanded = true; break;
169 | case "filter":
170 | wvar.query = decodeURIComponent(line[1]);
171 | $("header .search input[type='text']").val(wvar.query);
172 | break;
173 | case "nyan": wvar.nyan = true; break;
174 | case "focus": wvar.focus = decodeURIComponent(line[1]); break;
175 | case "docid": wvar.docid = line[1]; break;
176 | case "mode": wvar.mode = decodeURIComponent(line[1]); break;
177 | }
178 | }
179 |
180 | if(wvar.enabled) {
181 | //analytics
182 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'Functionality', 'Embed Opts', window.location.search]);
183 | }
184 |
185 | $.ajaxSetup({ cache: true });
186 |
187 | var force_check_cache = false;
188 |
189 | // handle cachin events and display a loading bar
190 | var loadComplete = function(e) {
191 | clearTimeout(initTimer);
192 |
193 | if(e.type == 'updateready') {
194 | // swapCache may throw exception if the isn't a previous cache
195 | try {
196 | window.applicationCache.swapCache();
197 | } catch(v) {}
198 |
199 | window.location.reload();
200 | return;
201 | }
202 |
203 | $('#loading .complete').stop(true,true).animate({width: 200}, {complete: trackerInit });
204 | };
205 |
206 | var hysplit = {};
207 | var hysplit_data = {};
208 | var refresh_hysplit = function() {
209 | $.getJSON("//spacenear.us/tracker/datanew.php?type=hysplit&format=json", function(data) {
210 | var refresh = false;
211 |
212 | for(var k in data) {
213 | if(k in hysplit_data) {
214 | // if the jobid is the same, skip to next one
215 | if(hysplit_data[k].jobid == data[k].jobid) continue;
216 |
217 | // otherwise update the url
218 | hysplit_data[k] = data[k];
219 | hysplit[k].setUrl(hysplit_data[k].url_kmz);
220 | } else {
221 | hysplit_data[k] = data[k];
222 | hysplit[k] = new google.maps.KmlLayer({url: hysplit_data[k].url_kmz, preserveViewport:true });
223 | refresh = true;
224 | }
225 | }
226 |
227 | if(refresh) refreshUI();
228 | });
229 | };
230 |
231 | // loads the tracker interface
232 | function trackerInit() {
233 | $('#loading,#settingsbox,#aboutbox,#chasebox').hide(); // welcome screen
234 | $('header,#main').show(); // interface elements
235 | checkSize();
236 |
237 | if(map) return;
238 |
239 | if(is_mobile || wvar.enabled) $(".nav .wvar").hide();
240 |
241 | if(!is_mobile) {
242 | $.getScript("js/init_plot.js", function() { checkSize(); if(!map) load(); });
243 | if(wvar.graph) $('#telemetry_graph').attr('style','');
244 |
245 | // fetch hysplit jobs
246 | setInterval(refresh_hysplit, 60 * 1000);
247 | refresh_hysplit();
248 |
249 | return;
250 | }
251 | if(!map) load();
252 | }
253 |
254 | // if for some reason, applicationCache is not working, load the app after a 3s timeout
255 | var initTimer = setTimeout(trackerInit, 3000);
256 |
257 | var cache = window.applicationCache;
258 | cache.addEventListener('noupdate', loadComplete, false);
259 | cache.addEventListener('updateready', loadComplete, false);
260 | cache.addEventListener('cached', loadComplete, false);
261 | cache.addEventListener('error', loadComplete, false);
262 |
263 | // if the browser supports progress events, display a loading bar
264 | cache.addEventListener('checking', function() { if(map && !force_check_cache) return; force_check_cache = false; clearTimeout(initTimer); $('#loading .bar,#loading').show(); $('#loading .complete').css({width: 0}); }, false);
265 | cache.addEventListener('progress', function(e) { $('#loading .complete').stop(true,true).animate({width: (200/e.total)*e.loaded}); }, false);
266 |
267 | var listScroll;
268 | var GPS_ts = null;
269 | var GPS_lat = null;
270 | var GPS_lon = null;
271 | var GPS_alt = null;
272 | var GPS_speed = null;
273 | var CHASE_enabled = null;
274 | var CHASE_listenerSent = false;
275 | var CHASE_timer = 0;
276 | var callsign = "";
277 |
278 | function checkSize() {
279 | // we are in landscape mode
280 | w = window.innerWidth;
281 |
282 | // this is hacky fix for off by 1px that makes the vechicle list disappear
283 | wrect = document.body.getBoundingClientRect();
284 | // chrome seems to calculate the body bounding box differently from every other browser
285 | if (!!window.chrome) {
286 | w_fix = (w >= wrect.width) ? 1 : 0;
287 | } else {
288 | w_fix = (w === Math.floor(wrect.width)) ? 0 : 1;
289 | }
290 |
291 | w = (w < 320) ? 320 : w; // absolute minimum 320px
292 | h = window.innerHeight;
293 | //h = (h < 300) ? 300 : h; // absolute minimum 320px minus 20px for the iphone bar
294 | hh = $('header').height();
295 |
296 | ph = 0;
297 |
298 | if(w > 900 && $('.flatpage:visible').length) {
299 | $('.flatpage').addClass('topanel');
300 | ph = $('.flatpage:visible').width()+30;
301 | } else {
302 | $('.flatpage.topanel').removeClass('topanel');
303 | }
304 |
305 | $("#mapscreen,.flatpage").height(h-hh-5);
306 |
307 | sw = (wvar.vlist) ? 200 : 0;
308 |
309 | $('.container').width(w-20);
310 |
311 | if($('.landscape:visible').length) {
312 | $('#main').height(h-hh-5);
313 | if($('#telemetry_graph .graph_label').hasClass('active')) {
314 | $('#map').height(h-hh-5-200);
315 | } else {
316 | $('#map').height(h-hh-5);
317 | }
318 | $('body,#loading').height(h);
319 | $('#mapscreen,#map,#telemetry_graph,#telemetry_graph .holder').width(w-sw-ph-w_fix);
320 | $('#main').width(sw);
321 | } else { // portrait mode
322 | //if(h < 420) h = 420;
323 | var mh = (wvar.vlist) ? 150 : 0;
324 |
325 | $('body,#loading').height(h);
326 | $('#map,#mapscreen').height(h-hh-5-mh);
327 | $('#map,#mapscreen').width(w);
328 | $('#main').height(mh); // 180px is just enough to hold one expanded vehicle
329 | $('#main').width(w);
330 | }
331 |
332 | // this should hide the address bar on mobile phones, when possible
333 | window.scrollTo(0,1);
334 |
335 | if(map) google.maps.event.trigger(map, 'resize');
336 | }
337 |
338 | window.onresize = checkSize;
339 | window.onchangeorientation = checkSize;
340 |
341 |
342 | // functions
343 |
344 | function positionUpdateError(error) {
345 | switch(error.code)
346 | {
347 | case error.PERMISSION_DENIED:
348 | alert("no permission to use your location");
349 | $('#sw_chasecar').click(); // turn off chase car
350 | break;
351 | default:
352 | break;
353 | }
354 | }
355 |
356 | var positionUpdateHandle = function(position) {
357 | if(CHASE_enabled && !CHASE_listenerSent) {
358 | if(offline.get('opt_station')) {
359 | ChaseCar.putListenerInfo(callsign);
360 | CHASE_listenerSent = true;
361 | }
362 | }
363 |
364 | //navigator.geolocation.getCurrentPosition(function(position) {
365 | var lat = position.coords.latitude;
366 | var lon = position.coords.longitude;
367 | var alt = (position.coords.altitude) ? position.coords.altitude : 0;
368 | var accuracy = (position.coords.accuracy) ? position.coords.accuracy : 0;
369 | var speed = (position.coords.speed) ? position.coords.speed : 0;
370 |
371 | // constantly update 'last updated' field, and display friendly time since last update
372 | if(!GPS_ts) {
373 | GPS_ts = parseInt(position.timestamp/1000);
374 |
375 | setInterval(function() {
376 | var delta_ts = parseInt(Date.now()/1000) - GPS_ts;
377 |
378 | // generate friendly timestamp
379 | var hours = Math.floor(delta_ts / 3600);
380 | var minutes = Math.floor(delta_ts / 60) % 60;
381 | var ts_str = (delta_ts >= 60) ?
382 | ((hours)?hours+'h ':'') +
383 | ((minutes)?minutes+'m':'') +
384 | ' ago'
385 | : 'just now';
386 | $('#cc_timestamp').text(ts_str);
387 | }, 30000);
388 |
389 | $('#cc_timestamp').text('just now');
390 | }
391 |
392 | // save position and update only if different is available
393 | if(CHASE_timer < (new Date()).getTime() &&
394 | (
395 | GPS_lat != lat ||
396 | GPS_lon != lon ||
397 | GPS_alt != alt ||
398 | GPS_speed != speed
399 | )
400 | )
401 | {
402 | GPS_lat = lat;
403 | GPS_lon = lon;
404 | GPS_alt = alt;
405 | GPS_speed = speed;
406 | GPS_ts = parseInt(position.timestamp/1000);
407 | $('#cc_timestamp').text('just now');
408 |
409 | // update look angles once we get position
410 | if(follow_vehicle !== null && vehicles[follow_vehicle] !== undefined) {
411 | update_lookangles(follow_vehicle);
412 | }
413 |
414 | if(CHASE_enabled) {
415 | ChaseCar.updatePosition(callsign, position);
416 | CHASE_timer = (new Date()).getTime() + 15000;
417 |
418 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'upload', 'chase car position']);
419 | }
420 | }
421 | else { return; }
422 |
423 | // add/update marker on the map (tracker.js)
424 | updateCurrentPosition(lat, lon);
425 |
426 | // round the coordinates
427 | lat = parseInt(lat * 10000)/10000; // 4 decimal places (11m accuracy at equator)
428 | lon = parseInt(lon * 10000)/10000; // 4 decimal places
429 | speed = parseInt(speed * 10)/10; // 1 decimal place
430 | accuracy = parseInt(accuracy);
431 | alt = parseInt(alt);
432 |
433 | // dispaly them in the top right corner
434 | $('#app_name b').html(lat + ' ' + lon);
435 |
436 | // update chase car interface
437 | $('#cc_lat').text(lat);
438 | $('#cc_lon').text(lon);
439 | $('#cc_alt').text(alt + " m");
440 | $('#cc_accuracy').text(accuracy + " m");
441 | $('#cc_speed').text(speed + " m/s");
442 | /*
443 | },
444 | function() {
445 | // when there is no location
446 | $('#app_name b').html('mobile tracker');
447 | });
448 | */
449 | };
450 |
451 | var twoZeroPad = function(n) {
452 | n = String(n);
453 | return (n.length<2) ? '0'+n : n;
454 | };
455 |
456 | // updates timebox
457 | var updateTimebox = function(date) {
458 | var elm = $("#timebox");
459 | var a,b,c,d,e,f,g,z;
460 |
461 | a = date.getUTCFullYear();
462 | b = twoZeroPad(date.getUTCMonth()+1); // months 0-11
463 | c = twoZeroPad(date.getUTCDate());
464 | e = twoZeroPad(date.getUTCHours());
465 | f = twoZeroPad(date.getUTCMinutes());
466 | g = twoZeroPad(date.getUTCSeconds());
467 |
468 | elm.find(".current").text("UTC: "+a+'-'+b+'-'+c+' '+e+':'+f+':'+g);
469 |
470 | a = date.getFullYear();
471 | b = twoZeroPad(date.getMonth()+1); // months 0-11
472 | c = twoZeroPad(date.getDate());
473 | e = twoZeroPad(date.getHours());
474 | f = twoZeroPad(date.getMinutes());
475 | g = twoZeroPad(date.getSeconds());
476 | z = date.getTimezoneOffset() / -60;
477 |
478 | elm.find(".local").text("Local: "+a+'-'+b+'-'+c+' '+e+':'+f+':'+g+" "+((z<0)?"-":"+")+z);
479 | };
480 |
481 | var format_time_friendly = function(start, end) {
482 | var dt = Math.floor((end - start) / 1000);
483 | if(dt < 0) return null;
484 |
485 | if(dt < 60) return dt + 's';
486 | else if(dt < 3600) return Math.floor(dt/60)+'m';
487 | else if(dt < 86400) {
488 | dt = Math.floor(dt/60);
489 | return Math.floor(dt/60)+'h '+(dt % 60)+'m';
490 | } else {
491 | dt = Math.floor(dt/3600);
492 | return Math.floor(dt/24)+'d '+(dt % 24)+'h';
493 | }
494 | };
495 |
496 | // runs every second
497 | var updateTime = function(date) {
498 | // update timebox
499 | var elm = $("#timebox.present");
500 | if(elm.length > 0) updateTimebox(date);
501 |
502 | // update friendly delta time fields
503 | elm = $(".friendly-dtime");
504 | if(elm.length > 0) {
505 | var now = new Date().getTime();
506 |
507 | elm.each(function(k,v) {
508 | var e = $(v);
509 | if(e.attr('data-timestamp') === undefined) return;
510 | var ts = e.attr('data-timestamp');
511 | var str = format_time_friendly(ts, now);
512 | if(str) e.text(str + ' ago');
513 | });
514 | }
515 | };
516 |
517 |
518 | $(window).ready(function() {
519 | // refresh timebox
520 | setInterval(function() {
521 | updateTime(new Date());
522 | }, 1000);
523 |
524 | // resize elements if needed
525 | checkSize();
526 |
527 | // add inline scroll to vehicle list
528 | listScroll = new IScroll('#main', {
529 | hScrollbar: false,
530 | hScroll: false,
531 | snap: false,
532 | mouseWheel: true,
533 | scrollbars: true,
534 | scrollbarClass: 'scrollStyle',
535 | shrinkScrollbars: 'scale',
536 | tap: true,
537 | click: true,
538 | disableMouse: false,
539 | disableTouch: false,
540 | disablePointer: false,
541 | });
542 |
543 | $('#telemetry_graph').on('click', '.graph_label', function() {
544 | var e = $(this), h;
545 | if(e.hasClass('active')) {
546 | e.removeClass('active');
547 | h = $('#map').height() + $('#telemetry_graph').height();
548 |
549 | plot_open = false;
550 |
551 | //analytics
552 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'UI', 'Collapse', 'Telemetry Graph']);
553 | } else {
554 | e.addClass('active');
555 | h = $('#map').height() - $('#telemetry_graph').height();
556 |
557 | plot_open = true;
558 |
559 | //analytics
560 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'UI', 'Expand', 'Telemetry Graph']);
561 | }
562 | $('#map').stop(null,null).animate({'height': h}, function() {
563 | if(map) google.maps.event.trigger(map, 'resize');
564 |
565 | if(plot_open &&
566 | follow_vehicle !== null &&
567 | (follow_vehicle != graph_vehicle || vehicles[follow_vehicle].graph_data_updated)) updateGraph(follow_vehicle, true);
568 | });
569 | });
570 |
571 | // expand graph on startup, if nessary
572 | if(wvar.graph_expanded) $('#telemetry_graph .graph_label').click();
573 |
574 | // hysplit button
575 | $("#main").on('click','.row .data .vbutton.hysplit', function(event) {
576 | event.stopPropagation();
577 |
578 | var elm = $(this);
579 | var name = elm.attr('data-vcallsign');
580 |
581 | if(elm.hasClass("active")) {
582 | elm.removeClass('active');
583 | hysplit[name].setMap(null);
584 | }
585 | else {
586 | elm.addClass('active');
587 | hysplit[name].setMap(map);
588 | }
589 | });
590 |
591 | $("#main").on('click','.row .data .vbutton.path', function(event) {
592 | event.stopPropagation();
593 |
594 | var elm = $(this);
595 | var name = elm.attr('data-vcallsign');
596 |
597 | if(elm.hasClass("active")) {
598 | elm.removeClass('active');
599 | set_polyline_visibility(name, false);
600 | }
601 | else {
602 | elm.addClass('active');
603 | set_polyline_visibility(name, true);
604 | }
605 | });
606 |
607 | // reset nite-overlay and timebox when mouse goes out of the graph box
608 | $("#telemetry_graph").on('mouseout','.holder', function() {
609 | if(plot_crosshair_locked) return;
610 |
611 | updateGraph(null, true);
612 | });
613 |
614 | // hand cursor for dragging the vehicle list
615 | $("#main").on("mousedown", ".row", function () {
616 | $("#main").addClass("drag");
617 | });
618 | $("body").on("mouseup", function () {
619 | $("#main").removeClass("drag");
620 | });
621 |
622 | // confirm dialog when launchnig a native map app with coordinates
623 | //$('#main').on('click', '#launch_mapapp', function() {
624 | // var answer = confirm("Launch your maps app?");
625 |
626 | // //analytics
627 | // if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'Functionality', ((answer)?"Yes":"No"), 'Coord Click']);
628 |
629 | // return answer;
630 | //});
631 |
632 | // follow vehicle by clicking on data
633 | $('#main').on('click', '.row .data', function() {
634 | var e = $(this).parent();
635 |
636 | followVehicle(e.attr('data-vcallsign'));
637 | });
638 |
639 | // expand/collapse data when header is clicked
640 | $('#main').on('click', '.row .header', function() {
641 | var e = $(this).parent();
642 | if(e.hasClass('active')) {
643 | // collapse data for selected vehicle
644 | e.removeClass('active');
645 | e.find('.data').hide();
646 |
647 | listScroll.refresh();
648 |
649 | // disable following only we are collapsing the followed vehicle
650 | if(follow_vehicle !== null && follow_vehicle == e.attr('data-vcallsign')) {
651 | stopFollow();
652 | }
653 | } else {
654 | // expand data for selected vehicle
655 | e.addClass('active');
656 | e.find('.data').show();
657 |
658 | listScroll.refresh();
659 |
660 | // auto scroll when expanding an item
661 | if($('.portrait:visible').length) {
662 | var eName = "." + e.parent().attr('class') + " ." + e.attr('class').match(/vehicle\d+/)[0];
663 | listScroll.scrollToElement(eName);
664 | }
665 |
666 | // pan to selected vehicle
667 | followVehicle(e.attr('data-vcallsign'));
668 | }
669 | });
670 |
671 | // menu interface options
672 | $('.nav')
673 | .on('click', 'li', function() {
674 | var e = $(this);
675 | var name = e.attr('class').replace(" last","");
676 |
677 | // makes the menu buttons act like a switch
678 | if($("#"+name+"box").is(':visible')) name = 'home';
679 |
680 | var box = $("#"+name+"box");
681 |
682 | if(box.is(':hidden')) {
683 | $('.flatpage, #homebox').hide();
684 | box.show().scrollTop(0);
685 |
686 | if(name == 'about' && !$('#motd').hasClass('inited')) {
687 | $('#motd').addClass('inited');
688 |
689 | $.getJSON("//spacenear.us/tracker/datanew.php?type=info", function(data) {
690 | if('html' in data) $('#motd').html(data.html.replace(/\\/g,''));
691 | });
692 |
693 | var iframe = box.find('iframe');
694 | var src = iframe.attr('data-src');
695 | iframe.attr('src', src);
696 | }
697 |
698 | // analytics
699 | var pretty_name;
700 | switch(name) {
701 | case "home": pretty_name = "Map"; break;
702 | case "chasecar": pretty_name = "Chase Car"; break;
703 | default: pretty_name = name[0].toUpperCase() + name.slice(1);
704 | }
705 |
706 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'UI Menubar', 'Open Page', pretty_name]);
707 | }
708 | checkSize();
709 | });
710 |
711 | // toggle functionality for switch button
712 | $("#sw_chasecar").click(function() {
713 | var e = $(this);
714 | var field = $('#cc_callsign');
715 |
716 | // turning the switch off
717 | if(e.hasClass('on')) {
718 | field.removeAttr('disabled');
719 | e.removeClass('on').addClass('off');
720 |
721 | if(navigator.geolocation) navigator.geolocation.clearWatch(CHASE_enabled);
722 | CHASE_enabled = null;
723 | //CHASE_enabled = false;
724 |
725 | // blue man reappers :)
726 | if(currentPosition && currentPosition.marker) currentPosition.marker.setVisible(true);
727 |
728 | // analytics
729 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'Functionality', 'Turn Off', 'Chase Car']);
730 | // turning the switch on
731 | } else {
732 | if(callsign.length < 5) { alert('Please enter a valid callsign, at least 5 characters'); return; }
733 | if(!callsign.match(/^[a-zA-Z0-9\_\-]+$/)) { alert('Invalid characters in callsign (use only a-z,0-9,-,_)'); return; }
734 |
735 | field.attr('disabled','disabled');
736 | e.removeClass('off').addClass('on');
737 |
738 | // push listener doc to habitat
739 | // this gets a station on the map, under the car marker
740 | // im still not sure its nessesary
741 | if(!CHASE_listenerSent) {
742 | if(offline.get('opt_station')) {
743 | ChaseCar.putListenerInfo(callsign);
744 | CHASE_listenerSent = true;
745 | }
746 | }
747 | // if already have a position push it to habitat
748 | if(GPS_ts) {
749 | ChaseCar.updatePosition(callsign, { coords: { latitude: GPS_lat, longitude: GPS_lon, altitude: GPS_alt, speed: GPS_speed }});
750 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'upload', 'chase car position']);
751 | }
752 |
753 | if(navigator.geolocation) CHASE_enabled = navigator.geolocation.watchPosition(positionUpdateHandle, positionUpdateError);
754 | //CHASE_enabled = true;
755 |
756 | // hide the blue man
757 | if(currentPosition && currentPosition.marker) currentPosition.marker.setVisible(false);
758 |
759 | // analytics
760 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'Functionality', 'Turn On', 'Chase Car']);
761 | }
762 | });
763 |
764 | // remember callsign as a cookie
765 | $("#cc_callsign").on('change keyup', function() {
766 | callsign = $(this).val().trim();
767 | offline.set('callsign', callsign); // put in localStorage
768 | CHASE_listenerSent = false;
769 | });
770 |
771 | // load value from localStorage
772 | callsign = offline.get('callsign');
773 | $('#cc_callsign').val(callsign);
774 |
775 | // settings page
776 |
777 | // list of all switches
778 | var opts = [
779 | "#sw_layers_aprs",
780 | "#sw_offline",
781 | "#sw_station",
782 | "#sw_imperial",
783 | "#sw_haxis_hours",
784 | "#sw_daylight",
785 | "#sw_hide_receivers",
786 | "#sw_hide_timebox",
787 | "#sw_hilight_vehicle",
788 | "#sw_nowelcome",
789 | "#sw_interpolate",
790 | ];
791 |
792 | // applies functionality when switches are toggled
793 | $(opts.join(',')).click(function() {
794 | var e = $(this);
795 | var name = e.attr('id').replace('sw', 'opt');
796 | var on;
797 |
798 | if(e.hasClass('on')) {
799 | e.removeClass('on').addClass('off');
800 | on = 0;
801 |
802 | //analytics
803 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'Functionality', 'Turn Off', name]);
804 | } else {
805 | e.removeClass('off').addClass('on');
806 | on = 1;
807 |
808 | //analytics
809 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'Functionality', 'Turn On', name]);
810 | }
811 |
812 | // remember choice
813 | offline.set(name, on);
814 |
815 | // execute functionality
816 | switch(name) {
817 | case "opt_hilight_vehicle":
818 | if(on) focusVehicle(follow_vehicle);
819 | else focusVehicle(null, true);
820 | break;
821 | case "opt_imperial":
822 | case "opt_haxis_hours":
823 | refreshUI();
824 | break;
825 | case "opt_daylight":
826 | if(on) { nite.show(); }
827 | else { nite.hide(); }
828 | break;
829 | case "opt_hide_receivers":
830 | if(on) {
831 | updateReceivers([]);
832 | clearTimeout(periodical_listeners);
833 | }
834 | else {
835 | refreshReceivers();
836 | }
837 | break;
838 | case "opt_hide_timebox":
839 | var elm = $("#timebox");
840 | if(on) {
841 | elm.removeClass('past').removeClass('present').hide();
842 | $('#lookanglesbox').css({top:'7px'});
843 | } else {
844 | elm.addClass('present').show();
845 | $('#lookanglesbox').css({top:'40px'});
846 | }
847 | break;
848 | case "opt_layers_aprs":
849 | if(on) map.overlayMapTypes.setAt("1", overlayAPRS);
850 | else map.overlayMapTypes.setAt("1", null);
851 | break;
852 | case "opt_interpolate":
853 | if(on) { graph_gap_size = graph_gap_size_max; }
854 | else { graph_gap_size = graph_gap_size_default; }
855 | clean_refresh(wvar.mode, true, false);
856 | break;
857 | }
858 | });
859 |
860 | // set the switch, based on the remembered choice
861 | for(var k in opts) {
862 | var switch_id = opts[k];
863 | var opt_name = switch_id.replace("#sw_", "opt_");
864 |
865 | if(offline.get(opt_name)) $(switch_id).removeClass('off').addClass('on');
866 | }
867 |
868 | // force re-cache
869 | $('#sw_cache').click(function() {
870 | var e = $(this).removeClass('off').addClass('on');
871 | if(confirm("The app will automatically reload, if new version is available.")) {
872 | force_check_cache = true;
873 |
874 | try {
875 | applicationCache.update();
876 | } catch (v) {
877 | force_check_cache = false;
878 | alert("There is no applicationCache available");
879 | }
880 | }
881 | e.removeClass('on').addClass('off');
882 | });
883 |
884 | // We are able to get GPS position on idevices, if the user allows
885 | // The position is displayed in top right corner of the screen
886 | // This should be very handly for in the field tracking
887 | //setTimeout(function() {updateCurrentPosition(50.27533, 3.335166);}, 5000);
888 | if(navigator.geolocation) {
889 | // if we have geolocation services, show the locate me button
890 | // the button pants the map to the user current location
891 | if(is_mobile && !wvar.enabled) $(".chasecar").show();
892 | $("#locate-me,#app_name").attr('style','').click(function() {
893 | if(map && currentPosition) {
894 | // disable following of vehicles
895 | stopFollow();
896 | // open map
897 | $('.nav .home').click();
898 | // pan map to our current location
899 | map.panTo(new google.maps.LatLng(currentPosition.lat, currentPosition.lon));
900 |
901 | //analytics
902 | if(typeof _gaq == 'object') _gaq.push(['_trackEvent', 'Functionality', 'Locate me']);
903 | } else {
904 | alert("No position available");
905 | }
906 | });
907 |
908 | navigator.geolocation.getCurrentPosition(positionUpdateHandle);
909 | // check for location update every 30sec
910 | //setInterval(positionUpdateHandle, 30000);
911 | // immediatelly check for position
912 | //positionUpdateHandle();
913 | }
914 |
915 | // weather feature
916 |
917 | // list of overlays
918 | var overlayList = [
919 | ['Global', [
920 | ['google-radar','Google Earth Radar'],
921 | ['nrl-global-cloudtop','NRL Monterey Cloudtop'],
922 | ['nrl-global-ir','NRL Monterey IR'],
923 | ['nrl-global-vapor','NRL Monterey Vapor']
924 | ]],
925 | ['Europe/Africa', [
926 | ['meteosat-Odeg-MPE', 'METEOSAT Precip. Estimate']
927 | ]],
928 | ['Indian Ocean', [
929 | ['meteosat-iodc-MPE', 'METEOSAT IODC Precip. Est.']
930 | ]],
931 | ['North America', [
932 | ['nexrad-n0q-900913', 'NEXRAD Base Reflectivity'],
933 | ['goes-ir-4km-900913', 'GOES NA Infrared ~4km'],
934 | ['goes-wv-4km-900913', 'GOES NA Water Vapor ~4km'],
935 | ['goes-vis-1km-900913', 'GOES NA Visible ~1km'],
936 | ['goes-east-ir-4km-900913', 'GOES East CONUS Infrared'],
937 | ['goes-east-wv-4km-900913', 'GOES East CONUS Water Vapor'],
938 | ['goes-east-vis-1km-900913', 'GOES East CONUS Visible'],
939 | ['goes-west-ir-4km-900913', 'GOES West CONUS Infrared'],
940 | ['goes-west-wv-4km-900913', 'GOES West CONUS Water Vapor'],
941 | ['goes-west-vis-1km-900913', 'GOES West CONUS Visible'],
942 | ['hawaii-vis-900913', 'GOES West Hawaii Visible'],
943 | ['alaska-vis-900913', 'GOES West Alaska Visible'],
944 | ['alaska-ir-900913', 'GOES West Alaska IR'],
945 | ['alaska-wv-900913', 'GOES West Alaska Water Vapor'],
946 | ['q2-n1p-900913', 'Q2 1 Hour Precipitation'],
947 | ['q2-p24h-900913', 'Q2 24 Hour Precipitation'],
948 | ['q2-p48h-900913', 'Q2 48 Hour Precipitation'],
949 | ['q2-p72h-900913', 'Q2 72 Hour Precipitation'],
950 | ['q2-hsr-900913', 'MRMS Hybrid-Scan Reflectivity Composite.']
951 | ]]
952 | ];
953 |
954 | // generate the list of switches for each overlay
955 | var elm = $("#weatherbox .slimContainer");
956 | var j;
957 | for(j in overlayList) {
958 | var region = overlayList[j][0];
959 | var switches = overlayList[j][1];
960 |
961 | elm.append("
"+region+"
");
962 |
963 | var i;
964 | for(i in switches) {
965 | var id = switches[i][0];
966 | var name = switches[i][1];
967 |
968 | var html = '
' +
969 | ''+name+'' +
970 | '
' +
971 | '' +
972 | '' +
973 | '
' +
974 | '
';
975 |
976 | elm.append(html);
977 | }
978 | }
979 |
980 | // the magic that makes the switches do things
981 | elm.find(".switch").click(function() {
982 | var e = $(this);
983 | var name = e.attr('id').replace('sw', 'opt');
984 | var id = name.replace("opt_weather_","");
985 | var on;
986 |
987 | if(e.hasClass('on')) {
988 | e.removeClass('on').addClass('off');
989 | on = 0;
990 | } else {
991 | // only one overlay at a time
992 | $("#weatherbox .switch").removeClass('on').addClass('off');
993 | e.removeClass('off').addClass('on');
994 | on = 1;
995 | }
996 |
997 | weatherImageOverlay.setMap(null);
998 | weatherGoogleRadar.setMap(null);
999 | map.overlayMapTypes.setAt("0", null);
1000 |
1001 | if(on) {
1002 | if(id == "google-radar") {
1003 | weatherGoogleRadar.setMap(map);
1004 | return;
1005 | } else if(id in weatherImageOverlayList) {
1006 | var o = weatherImageOverlayList[id];
1007 | var sw = new google.maps.LatLng(o[1][0][0], o[1][0][1]);
1008 | var ne = new google.maps.LatLng(o[1][1][0], o[1][1][1]);
1009 | var bounds = new google.maps.LatLngBounds(sw, ne);
1010 | weatherImageOverlay = new google.maps.GroundOverlay(o[0], bounds, {opacity: 0.7});
1011 | weatherImageOverlay.setMap(map);
1012 | return;
1013 | }
1014 |
1015 | weatherOverlayId = id;
1016 | map.overlayMapTypes.setAt("0", weatherOverlay);
1017 | }
1018 | });
1019 |
1020 | $("header .search form").on('submit', function(e) {
1021 | e.preventDefault();
1022 |
1023 | var text = $("header .search input[type='text']").val();
1024 |
1025 | if(text === wvar.query) return;
1026 |
1027 | // when running an empty search, it's probably best to reset the query mode
1028 |
1029 | wvar.query = text;
1030 | stopFollow();
1031 | zoomed_in = false;
1032 | wvar.zoom = true;
1033 |
1034 | if(text === "") { wvar.mode = null; }
1035 | clean_refresh(wvar.mode, true, true);
1036 | });
1037 | });
1038 |
--------------------------------------------------------------------------------