// // // // // Ruetteln source // // // // // // // File: Ruetteln/Resources/app.js // // Author: Francesco Prelz (Francesco.Prelz@mi.infn.it) // // Revision history: // 27-Jun-2014 Initial version - base tabbed app, network communication via UDP. // 8-Sep-2014 Modifications after debugging on iOS. // // Check dependencies if (Ti.version < 1.8) { alert('Sorry - Ruetteln requires Titanium Mobile SDK 1.8 or later'); } (function() { // Render appropriate components based on the platform and form factor var osname = Ti.Platform.osname, version = Ti.Platform.version, height = Ti.Platform.displayCaps.platformHeight, width = Ti.Platform.displayCaps.platformWidth; var destip = ""; var udport_s = 18769; var udport_d = udport_s; var last_acc_data_ts = 0; var last_hdg_data_ts = 0; var last_loc_data_ts = 0; var max_update_hz = 5; // considering tablets to have width over 720px and height over 600px function checkTablet() { var platform = Ti.Platform.osname; switch (platform) { case 'ipad': return true; case 'android': var psc = Ti.Platform.Android.physicalSizeCategory; var tiAndroid = Ti.Platform.Android; return psc === tiAndroid.PHYSICAL_SIZE_CATEGORY_LARGE || psc === tiAndroid.PHYSICAL_SIZE_CATEGORY_XLARGE; default: return (Math.min( Ti.Platform.displayCaps.platformHeight, Ti.Platform.displayCaps.platformWidth ) >= 400); } } var isTablet = checkTablet(); console.log(isTablet); var tabs = Titanium.UI.createTabGroup(); tabs.addEventListener('close', function (e) { if (Ti.Platform.name === 'android') { var activity = Titanium.Android.currentActivity; if (activity) activity.finish(); } } ); // Application tab and its attributes var win_a = Ti.UI.createWindow({ backgroundColor: 'white', layout: 'vertical', title: 'Ruetteln' }); var tab_a = Titanium.UI.createTab({ icon:'app_tab_icon.png', title:'Ruetteln', window:win_a }); label_opts = { color:'black', font:{fontSize:15}, text:'-', left: 10, right:10, width: Titanium.UI.FILL }; var labelxy = Ti.UI.createLabel(label_opts); win_a.add(labelxy); var labelz = Ti.UI.createLabel(label_opts); win_a.add(labelz); var labellat = Ti.UI.createLabel(label_opts); win_a.add(labellat); var labellon = Ti.UI.createLabel(label_opts); win_a.add(labellon); var labeladdr = Ti.UI.createLabel(label_opts); win_a.add(labeladdr); var labelhsp = Ti.UI.createLabel(label_opts); win_a.add(labelhsp); var labelmgh = Ti.UI.createLabel(label_opts); win_a.add(labelmgh); var labelnet = Ti.UI.createLabel(label_opts); labelnet.color = "green"; win_a.add(labelnet); if (Ti.Platform.name === 'android') { var closebut = Ti.UI.createButton(label_opts); closebut.title = "Close App"; closebut.addEventListener('click', function(e) { tabs.close(); } ); win_a.add(closebut); } // Graph tab and its attributes var win_g = Ti.UI.createWindow({ backgroundColor: 'white', layout: 'vertical', title: 'Graph' }); var tab_g = Titanium.UI.createTab({ icon:'graph_tab_icon.png', title:'Graph', window:win_g, }); var graph_d = { active: 0, dx: 0, lvy: new Array(0,0,0), max: 1, min: -1, width: width, height: height*.7, col: new Array('red','green','blue') }; if (Ti.Platform.name === 'android') { graph_d.max = 11; graph_d.min = -11; }; var Canvaspkg; var gcanv; var graphFocusCallback = function(e) { graph_d.width = Ti.Platform.displayCaps.platformWidth; graph_d.height = Ti.Platform.displayCaps.platformHeight*.66; if (Ti.Platform.name !== 'android') gcanv.begin(); gcanv.clearRect(0,0,gcanv.rect.width,gcanv.rect.height); if (Ti.Platform.name !== 'android') gcanv.lineWidth(2); if (graph_d.max > 0 && graph_d.min < 0) { // Draw horizontal axis var axy = graph_d.height/(graph_d.max - graph_d.min)*graph_d.max; gcanv.beginPath(); gcanv.setColor('black'); gcanv.moveTo(0,axy); gcanv.lineTo(graph_d.width, axy); gcanv.stroke(); } graph_d.active = 1; graph_d.dx = 0; }; var graphBlurCallback = function(e) { graph_d.active = 0; }; var graphAddPoint = function(x,y,z) { if (graph_d.active != 0) { var nval = new Array(x,y,z); var nvy = new Array(nval.length); for (var i=0; i<nval.length; i++) { nvy[i] = graph_d.height - ((nval[i]-graph_d.min)*(graph_d.height/(graph_d.max - graph_d.min))); } if (graph_d.dx > 0) { for (var i=0; i<nval.length; i++) { if (nvy[i] >= 0 && nvy[i] <= graph_d.height && graph_d.lvy[i] >= 0 && graph_d.lvy[i] <= graph_d.height) { gcanv.setColor(graph_d.col[i]); gcanv.beginPath(); gcanv.moveTo(graph_d.dx-1,graph_d.lvy[i]); gcanv.lineTo(graph_d.dx, nvy[i]); gcanv.stroke(); } if (Ti.Platform.name !== 'android') gcanv.commit(); } } if (graph_d.dx < graph_d.width) { graph_d.dx++; } else { graphFocusCallback({}); } for (var i=0; i<nval.length; i++) { graph_d.lvy[i] = nvy[i]; } } }; if (Ti.Platform.name === 'android') { Canvaspkg = require('com.wwl.canvas'); gcanv = Canvaspkg.createCanvasView({ backgroundColor: "transparent", zIndex: 1 }); } else { Canvaspkg = require('ti.canvas'); gcanv = Canvaspkg.createView(); gcanv.begin(); } if (gcanv != null) { win_g.add(gcanv); win_g.addEventListener('focus', graphFocusCallback); win_g.addEventListener('postlayout', graphFocusCallback); win_g.addEventListener('blur', graphBlurCallback); gcanv.setColor = function(color) { if ((typeof this.strokeStyle) == 'function') { this.strokeStyle(color); } else { this.strokeStyle = color; } }; } // Settings tab and its attributes var win_s = Ti.UI.createWindow({ backgroundColor: 'white', layout: 'horizontal', title: 'Settings' }); var tab_s = Titanium.UI.createTab({ icon:'setting_tab_icon.png', title:'Settings', window:win_s }); var portip_label = Ti.UI.createLabel(label_opts); portip_label.width = '90%'; if (Ti.Platform.name !== 'android') { portip_label.height = '25%'; portip_label.bottom = '15%'; } portip_label.text = 'Destination IP:UDP port'; var hz_label = Ti.UI.createLabel(label_opts); hz_label.width = '90%'; if (Ti.Platform.name !== 'android') hz_label.height = '10%'; hz_label.text = 'Update rate (Hz):'; var hz_input = Ti.UI.createTextField({ borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED, hintText : 'Data update rate (Hz)', textAlign: Ti.UI.TEXT_ALIGNMENT_LEFT, color:'black', left: 10, right:10, width: '40%', value : max_update_hz }); if (Ti.Platform.name !== 'android') hz_input.height = '20%'; var portip_picker = Ti.UI.createPicker({ width: '90%', color:'orange', backgroundColor:'blue', left: 10, right:10 }); if (Ti.Platform.name !== 'android') portip_picker.height = '20%'; var portip_opts = []; var prop_portips = Ti.App.Properties.getList('ruettelnPortIP'); for (var portip in prop_portips) { portip_opts.push(Ti.UI.createPickerRow({title:prop_portips[portip]})); } prop_portips = []; if (portip_opts.length == 0) { portip_opts[0] = Ti.UI.createPickerRow({title:'192.84.138.153:18769'}); } if (!(portip_opts[portip_opts.length - 1].getTitle().match(/add new/i))) { portip_opts.push(Ti.UI.createPickerRow({title:'Add new IP:port'})); } portip_picker.add(portip_opts); portip_picker.setSelectedRow(0,portip_opts.length - 2,false); portip_picker.selectionIndicator = true; var eff_portip = portip_opts[portip_opts.length - 2].title.split(":"); if (eff_portip.length >= 2) { destip = eff_portip[0]; udport_d = parseInt(eff_portip[1]); } labelnet.text = 'to IP: ' + destip + ':' + udport_d + ' - ' + max_update_hz + 'Hz'; var settingsFocusCallback = function(e) { hz_input.value = max_update_hz; }; win_s.addEventListener('focus', settingsFocusCallback); var ipd_textfield; var moveAddressUp = function(ipport) { var add_row = true; var picker_els = portip_picker.columns[0].rows.length; var picker_ins = picker_els - 1; // Duplicate address in history ? for (var i=0; i<picker_els; i++) { if (portip_picker.columns[0].rows[i].getTitle() == ipport) { for (var j=i+1; j < picker_els; j++) { var title = portip_picker.columns[0].rows[j].getTitle(); portip_picker.columns[0].rows[j-1].setTitle(title); } if (!add_row) portip_picker.columns[0].rows.pop(); // More than 1 row. add_row = false; picker_ins--; } } // Add new address to picker var save; if(add_row) { save = Ti.UI.createPickerRow({title:portip_picker.columns[0].rows[picker_els - 1].getTitle()}); portip_picker.columns[0].rows[picker_ins].setTitle(ipport); portip_picker.columns[0].addRow(save); } else portip_picker.columns[0].rows[picker_ins].setTitle(ipport); if ((typeof portip_picker.reloadColumn) == "function") portip_picker.reloadColumn(0); if (picker_els > 20) // Save the last 20 addresses { portip_picker.columns[0].rows.shift(); if ((typeof portip_picker.reloadColumn) == "function") portip_picker.reloadColumn(0); } portip_picker.setSelectedRow(0,portip_picker.columns[0].rows.length - 2,false); var prop_portips = []; for (var i in portip_picker.columns[0].rows) { prop_portips.push( portip_picker.columns[0].rows[i].getTitle()); } Ti.App.Properties.setList('ruettelnPortIP', prop_portips); }; var setPortipPicker = function(portip) { if (portip.length <= 0) return false; var eff_portip = portip.split(":"); var port = parseInt(eff_portip[1]); if ((eff_portip.length < 2) || (port == null)) return false; if ((port > 0 && port < 65536) && ((eff_portip[0] != destip) || (port !== udport_d))) { destip = eff_portip[0]; udport_d = port; moveAddressUp(destip+":"+port); labelnet.text = 'to IP: ' + destip + ':' + udport_d + ' - ' + max_update_hz + 'Hz'; return true; } return false; }; var addNewAddressCallback = function(e) { var portip; if (Ti.Platform.name === 'android') { portip = ipd_textfield.value; } else portip = e.text; if (!setPortipPicker(portip)) { portip_picker.setSelectedRow(0,portip_picker.columns[0].rows.length - 2,false); } portip_picker.addEventListener('change', settingsCallback); }; var settingsCallback = function(e) { if (e.source == hz_input) { max_update_hz = parseInt(e.value); labelnet.text = 'to IP: ' + destip + ':' + udport_d + ' - ' + max_update_hz + 'Hz'; } else if (e.source == portip_picker) { var title = portip_picker.getSelectedRow(0).getTitle(); if (title.search(/add new/i) >= 0) { // No more picker events until selection is finished portip_picker.removeEventListener('change', settingsCallback); // Dialog box to enter new IP:port var dialog; if (Ti.Platform.name === 'android') { ipd_textfield = Ti.UI.createTextField({value:destip + ':' + udport_d}); dialog = Ti.UI.createAlertDialog({ title: 'Enter IP address:port', androidView: ipd_textfield, buttonNames: ['OK', 'cancel'] }); } else // TODO: This is not really a fallback case { dialog = Ti.UI.createAlertDialog({ title: 'Enter IP address:port', style: Ti.UI.iPhone.AlertDialogStyle.PLAIN_TEXT_INPUT, buttonNames: ['OK', 'cancel'] }); } dialog.addEventListener('click', addNewAddressCallback); dialog.show(); return; } setPortipPicker(title); } }; portip_picker.addEventListener('change', settingsCallback); hz_input.addEventListener('change', settingsCallback); var data_mode_picker = Ti.UI.createPicker({ width: '40%', color:'orange', backgroundColor:'blue', left: 10, right:10 }); if (Ti.Platform.name !== 'android') data_mode_picker.height='25%'; var data_modes = []; data_modes[0]=Ti.UI.createPickerRow({title:'JSON'}); data_modes[1]=Ti.UI.createPickerRow({title:'XML'}); data_modes[2]=Ti.UI.createPickerRow({title:'txt'}); data_mode_picker.add(data_modes); data_mode_picker.setSelectedRow(0,0,false); data_mode_picker.selectionIndicator = true; if (Ti.Platform.name === 'android') { win_s.add(portip_label); win_s.add(portip_picker); } win_s.add(hz_label); win_s.add(hz_input); win_s.add(data_mode_picker); if (Ti.Platform.name !== 'android') { win_s.add(portip_label); win_s.add(portip_picker); } // Pack tabs together. tabs.addTab(tab_a); tabs.addTab(tab_g); tabs.addTab(tab_s); tabs.open(); var UDP = require('ti.udp'); var udpsock = UDP.createSocket(); var addrtext = "-unknown-"; var udperrorCallback = function(e) { labelnet.color = "red"; // Pull back on update speed in case of errors. max_update_hz = 1; hz_input.value = max_update_hz; }; udpsock.addEventListener('error', udperrorCallback); udpsock.start({ port: udport_s }); var accelerometerCallback = function(e) { var now = new Date().getTime(); // ms since Unix epoch. if (now < (last_acc_data_ts + (1000/max_update_hz))) return; last_acc_data_ts = now; labelxy.text = 'x acc: ' + e.x.toPrecision(9) + ' y acc: ' + e.y.toPrecision(9); labelz.text = 'z acc: ' + e.z.toPrecision(9); graphAddPoint(e.x, e.y, e.z); var root_tag = 'accelerometer'; var send_data; if ((data_mode_picker.getSelectedRow(0) != null) && (data_mode_picker.getSelectedRow(0).getTitle() == "XML")) { // Titanium.XML.DOMImplementation.createDocument is documented but // missing. // var xmldoc = Titanium.XML.DOMImplementation.createDocument("http://orsone.mi.infn.it/ruetteln", "Accelerometer data", null); // var xmlroot = xmldoc.createElement(root_tag); // for (var attr in e) // { // // TODO: Would need to check whether the attribute names are valid // // XML tag names. // var element = xmldoc.createElement(attr); // element.setNodeValue(e[attr]); // xmlroot.appendChild(element); // } // xmldoc.appendChild(xmlroot); // send_data = xmldoc.GetData(); send_data = '<'+root_tag+'>'; for (var attr in e) { // TODO: Would need to check whether the attribute names are valid // XML tag names. if ((typeof e[attr]) != "object") { send_data += '<'+attr+'>'+e[attr]+'</'+attr+'>'; } } send_data += '</'+root_tag+'>'; } else if ((data_mode_picker.getSelectedRow(0) != null) && (data_mode_picker.getSelectedRow(0).getTitle() == "txt")) { send_data = ""; for (var attr in e) { if ((typeof e[attr]) != "object") { send_data += root_tag + '.' + attr + '==' + e[attr] + '\n'; } } } else // JSON case { send_data = JSON.stringify(e); } if (destip.length > 0) { labelnet.color = "green"; udpsock.sendString({ host: destip, port: udport_d, data: send_data }); } }; var geodecCallback = function(e) { if (e.success) { addrtext = e.places[0].street + ', ' + e.places[0].city + ', ' + e.places[0].country_code; labeladdr.text = addrtext.substr(0,120); } }; var locCallback = function(e) { var now = new Date().getTime(); // ms since Unix epoch. if (now < (last_loc_data_ts + (1000/max_update_hz))) return; last_loc_data_ts = now; if (e.coords != null) { e.coords.address = addrtext; labellat.text = "lat: " + e.coords.latitude; labellon.text = "long: " + e.coords.longitude + " +/- " + e.coords.accuracy + "m"; Titanium.Geolocation.reverseGeocoder(e.coords.latitude, e.coords.longitude, geodecCallback); labelhsp.text = "alt: " + e.coords.altitude + " +/- " + e.coords.altitudeAccuracy + " head: " + e.coords.heading.toPrecision(6) + " speed: " + e.coords.speed.toPrecision(6); var root_tag = 'coords'; var send_data; if ((data_mode_picker.getSelectedRow(0) != null) && (data_mode_picker.getSelectedRow(0).getTitle() == "XML")) { send_data = '<'+root_tag+'>'; for (var attr in e.coords) { // TODO: Would need to check whether the attribute names are valid // XML tag names. if ((typeof e.coords[attr]) != "object") { send_data += '<'+attr+'>'+e.coords[attr]+'</'+attr+'>'; } } send_data += '</'+root_tag+'>'; } else if ((data_mode_picker.getSelectedRow(0) != null) && (data_mode_picker.getSelectedRow(0).getTitle() == "txt")) { send_data = ""; for (var attr in e.coords) { if ((typeof e.coords[attr]) != "object") { send_data += root_tag + '.' + attr + '==' + e.coords[attr] + '\n'; } } } else // JSON case { send_data = JSON.stringify(e); } if (destip.length > 0) { labelnet.color = "green"; udpsock.sendString({ host: destip, port: udport_d, data: send_data }); } } }; var hdgCallback = function(e) { var now = new Date().getTime(); // ms since Unix epoch. if (now < (last_hdg_data_ts + (1000/max_update_hz))) return; last_hdg_data_ts = now; if (e.source.lastGeolocation != null) { if ((typeof e.source.lastGeolocation) == "string") { var geoobj = {}; geoobj = JSON.parse(e.source.lastGeolocation); delete e.source.lastGeolocation; e.source['lastGeolocation'] = geoobj; } if ((typeof e.source.lastGeolocation) == "object") { e.source.lastGeolocation.address = addrtext; } } if (e.heading != null) { labelmgh.text = "MAG heading: "+e.heading.magneticHeading; var root_tag = 'heading'; var send_data; if ((data_mode_picker.getSelectedRow(0) != null) && (data_mode_picker.getSelectedRow(0).getTitle() == "XML")) { send_data = '<'+root_tag+'>'; for (var attr in e.heading) { // TODO: Would need to check whether the attribute names are valid // XML tag names. if ((typeof e.heading[attr]) != "object") { send_data += '<'+attr+'>'+e.heading[attr]+'</'+attr+'>'; } } if ((e.source.lastGeolocation != null) && ((typeof e.source.lastGeolocation) == "object")) { send_data += '<lastGeolocation>'; for (var attr in e.source.lastGeolocation) { // TODO: Would need to check whether the attribute names are valid // XML tag names. if ((typeof e.source.lastGeolocation[attr]) != "object") { send_data += '<'+attr+'>'+e.source.lastGeolocation[attr]+'</'+attr+'>'; } } send_data += '</lastGeolocation>'; } send_data += '</'+root_tag+'>'; } else if ((data_mode_picker.getSelectedRow(0) != null) && (data_mode_picker.getSelectedRow(0).getTitle() == "txt")) { send_data = ""; for (var attr in e.heading) { if ((typeof e.heading[attr]) != "object") { send_data += root_tag + '.' + attr + '==' + e.heading[attr] + '\n'; } } if ((e.source.lastGeolocation != null) && ((typeof e.source.lastGeolocation) == "object")) { for (var attr in e.source.lastGeolocation) { if ((typeof e.source.lastGeolocation[attr]) != "object") { send_data += root_tag + '.lastGeolocation.' + attr + '==' + e.source.lastGeolocation[attr] + '\n'; } } } } else // JSON case { send_data = JSON.stringify(e); } if (destip.length > 0) { labelnet.color = "green"; udpsock.sendString({ host: destip, port: udport_d, data: send_data }); } } }; if (Ti.Platform.model === 'Simulator' || Ti.Platform.model.indexOf('sdk') !== -1) { alert('Warning: Accelerometer, location and compass do not work on a virtual device'); } Ti.Accelerometer.addEventListener('update', accelerometerCallback); if (Ti.Platform.name === 'android') { Ti.Android.currentActivity.addEventListener('pause', function(e) { Ti.API.info("removing accelerometer callback on pause"); Ti.Accelerometer.removeEventListener('update', accelerometerCallback); }); Ti.Android.currentActivity.addEventListener('resume', function(e) { Ti.API.info("adding accelerometer callback on resume"); Ti.Accelerometer.addEventListener('update', accelerometerCallback); }); } Ti.Geolocation.accuracy = Ti.Geolocation.ACCURACY_HIGH; Ti.Geolocation.addEventListener("heading", hdgCallback); Ti.Geolocation.addEventListener("location", locCallback); var gestureCallback = function(e) { var splay = Ti.Media.createSound({url:"brbulb.wav"}); splay.play(); }; Ti.Gesture.addEventListener('shake', gestureCallback); })();