1// KiwiSDR utilities 2// 3// Copyright (c) 2014-2026 John Seamons, ZL4VO/KF6VO 4 5 6// isUndeclared(v) => use inline "if (window.v) ..." (i.e. can't pass undeclared v as func arg) 7function isUndefined(v) { return (typeof(v) === 'undefined'); } 8function isDefined(v) { return (typeof(v) !== 'undefined'); } 9function isNull(v) { return (v === null); } 10function isNumber(v) { return (typeof(v) === 'number' && !isNaN(v)); } 11function isNumberElse(v,e) { return (isNumber(v)? v:e); } 12function isBoolean(v) { return (typeof(v) === 'boolean'); } 13function isNumOrBool(v) { return (isNumber(v) || isBoolean(v)); } 14function isNumOrBoolElse(v,e) { return (isNumOrBool(v)? v:e); } 15function isString(v) { return (typeof(v) === 'string'); } 16function isNonEmptyString(v) { return (isString(v) && v !== ''); } 17function isEmptyString(v) { return (!isString(v) || v == ''); } // NB: !isString() || 18function isArray(v) { return (Array.isArray(v)); } 19function isNonEmptyArray(v) { return (isArray(v) && v.length > 0); } 20function isEmptyArray(v) { return (!isArray(v) || v.length == 0); } // NB: !isString() || 21function isFunction(v) { return (typeof(v) === 'function'); } 22function isObject(v) { return (typeof(v) === 'object'); } 23function isArg(v) { return (isUndefined(v) || isNull(v))? false:true; } 24function isArgElse(v,e) { return (isArg(v)? v:e); } 25function isNoArg(v) { return (isUndefined(v) || isNull(v))? true:false; } 26function kiwi_typeof(v) { return isNull(v)? 'null' : (isArray(v)? 'array' : typeof(v)); } 27 28function ifString(s, alt) { return (isString(s)? s : alt); } 29 30function isalnum(c) { return c.length === 1 && /[a-zA-Z0-9]/.test(c); } 31function isalpha(c) { return c.length === 1 && /[a-zA-Z]/.test(c); } 32function iscntrl(c) { return c.length === 1 && ord(c) < 32; } 33function isdigit(c) { return c.length === 1 && c >= '0' && c <= '9'; } 34function isspace(c) { return c.length === 1 && /\s/.test(c); } 35function islower(c) { return c.length === 1 && c >= 'a' && c <= 'z'; } 36function isupper(c) { return c.length === 1 && c >= 'A' && c <= 'Z'; } 37function isprint(c) { return c.length === 1 && ord(c) >= 32 && ord(c) <= 126; } 38function isxdigit(c) { return c.length === 1 && /[0-9a-fA-F]/.test(c); } 39 40// browsers have added includes() only relatively recently 41try { 42 if (!String.prototype.includes) { 43 String.prototype.includes = function(str) { 44 return (this.indexOf(str) >= 0); 45 }; 46 } 47} catch(ex) { 48 console.log("kiwi_util: String.prototype.includes"); 49} 50 51// stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string 52try { 53 if (!String.prototype.startsWith) { 54 String.prototype.startsWith = function(str) { 55 return this.indexOf(str) == 0; 56 }; 57 } 58} catch(ex) { 59 console.log("kiwi_util: String.prototype.startsWith"); 60} 61 62// developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith 63try { 64 if (!String.prototype.endsWith) { 65 String.prototype.endsWith = function(search, this_len) { 66 if (this_len === undefined || this_len > this.length) { 67 this_len = this.length; 68 } 69 return this.substring(this_len - search.length, this_len) === search; 70 }; 71 } 72} catch(ex) { 73 console.log("kiwi_util: String.prototype.endsWith"); 74} 75 76var kiwi_util = { 77 last_time: '', 78 mobile_laptop_test: false, 79 80 _last_: null 81}; 82 83var kiwi_iOS, kiwi_iPhone, kiwi_MacOS, kiwi_linux, kiwi_Windows, kiwi_android; 84var kiwi_safari, kiwi_firefox, kiwi_chrome, kiwi_opera, kiwi_smartTV, kiwi_browserVersion; 85var kiwi_touch; 86 87// wait until DOM has loaded before proceeding (browser has loaded HTML, but not necessarily images) 88document.onreadystatechange = function() { 89 //console.log("onreadystatechange="+document.readyState); 90 //if (document.readyState == "interactive") { 91 if (document.readyState == "complete") { 92 var website = false; 93 var el = w3_el('id-kiwi-container'); 94 if (el && el.getAttribute('data-type') == 'website') website = true; 95 var s = navigator.userAgent; 96 if (!website) console.log(s); 97 //alert(s); 98 kiwi_iOS = (s.includes('iPhone') || s.includes('iPad')); 99 kiwi_iPhone = s.includes('iPhone'); 100 kiwi_MacOS = s.includes('OS X'); 101 kiwi_linux = s.includes('Linux'); 102 kiwi_Windows = s.includes('Win'); 103 kiwi_android = s.includes('Android'); 104 105 kiwi_safari = /safari\/([0-9]+)/i.exec(s); 106 kiwi_browserVersion = /version\/([0-9]+)/i.exec(s); 107 kiwi_firefox = /firefox\/([0-9]+)/i.exec(s); 108 kiwi_chrome = /chrome\/([0-9]+)/i.exec(s); 109 110 kiwi_opera = /opera\/([0-9]+)/i.exec(s); 111 if (!kiwi_opera) kiwi_opera = /OPR\/([0-9]+)/i.exec(s); 112 113 kiwi_smartTV = s.includes('SmartTV'); 114 if (!kiwi_smartTV) kiwi_smartTV = s.includes('SMART-TV'); 115 if (kiwi_smartTV) { 116 if (s.includes('Samsung')) kiwi_smartTV = 'Samsung'; 117 else 118 if (s.includes('LG')) kiwi_smartTV = 'LG'; 119 } 120 121 // It seems iPads no longer return "iPad" in the user agent (iPhones still return "iPhone"). 122 // So use more generic (and recommended) way of determining touch/mobile devices. 123 // see: developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_device_detection 124 // NB: don't use "'maxTouchPoints' in navigator" because very old iPads don't support "in" keyword. 125 if (navigator.hasOwnProperty('maxTouchPoints')) { 126 kiwi_touch = (navigator.maxTouchPoints > 0); 127 } else 128 if (navigator.hasOwnProperty('msMaxTouchPoints')) { 129 kiwi_touch = (navigator.msMaxTouchPoints > 0); 130 } else 131 if (window.hasOwnProperty('matchMedia')) { 132 var mQ = matchMedia('(pointer: coarse)'); 133 if (mQ.media === '(pointer: coarse)') { 134 kiwi_touch = !!mQ.matches; 135 } else { 136 kiwi_touch = false;
137 } 138 } else { 139 if (window.hasOwnProperty('orientation')) { 140 kiwi_touch = true; // deprecated, but good fallback 141 } else { 142 kiwi_touch = false; 143 } 144 } 145 //alert('kiwi_touch='+ kiwi_touch); 146 147 if (!website) console.log( 148 'MacOS='+ kiwi_MacOS + 149 ' Linux='+ kiwi_linux + 150 ' Windows='+ kiwi_Windows + 151 ' iOS='+ kiwi_iOS + 152 ' iPhone='+ kiwi_iPhone + 153 ' Android='+ kiwi_android + 154 ' SmartTV='+ kiwi_smartTV); 155 156 // madness.. 157 if (kiwi_opera) { 158 kiwi_chrome = kiwi_safari = null; 159 } else 160 if (kiwi_chrome) { 161 kiwi_safari = null; 162 } 163 164 if (!website) console.log('Safari='+ kiwi_isSafari() + ' Firefox='+ kiwi_isFirefox() + ' Chrome='+ kiwi_isChrome() + ' Opera='+ kiwi_isOpera()); 165 166 // packages to load early on 167 kiwi.noLocalStorage = (isUndefined(localStorage) || localStorage == null)? true:false; 168 if (!website) { 169 migrateCookies(); 170 171 kiwi_load_js_dir('pkgs/js/', ['sprintf/sprintf.js', 'SHA256.js', 'coloris/coloris.js'], 172 function() { 173 //console.log(sprintf('%s', 'sprintf loaded')); 174 kiwi_load2(); 175 } 176 ); 177 } else { 178 kiwi_load2(); 179 } 180 } 181}; 182 183function kiwi_load2() 184{ 185 if (typeof(kiwi_check_js_version) !== 'undefined' /* declared */) { 186 // done as an AJAX because needed long before any websocket available 187 kiwi_ajax("/VER", 'kiwi_version_cb'); 188 } else { 189 kiwi.conn_tstamp = (new Date()).getTime(); // fallback 190 if (typeof(kiwi_bodyonload) !== 'undefined' /* declared */) { 191 kiwi_bodyonload(''); 192 } 193 } 194} 195 196function kiwi_is_iOS() { return kiwi_iOS; } 197 198// works even for non-Safari browsers 199function kiwi_isOld_iOS() { return (kiwi_isSafari() <= 601); } 200 201function kiwi_is_iPhone() { return kiwi_iPhone; } 202 203function kiwi_isMacOS() { return kiwi_MacOS; } 204 205function kiwi_isWindows() { return kiwi_Windows; } 206 207function kiwi_isLinux() { return kiwi_linux; } 208 209function kiwi_isAndroid() { return kiwi_android; } 210 211function kiwi_isSmartTV() { return kiwi_smartTV; } 212 213function kiwi_isTouch() { return kiwi_touch; } 214 215function kiwi_isMobile() { return (kiwi.force_mobile || kiwi_isTouch() || kiwi_is_iOS() || kiwi_isAndroid() || (kiwi_isSmartTV() == 'Samsung') || kiwi_util.mobile_laptop_test); } 216function kiwi_isPhone() { return (kiwi.force_mobile || kiwi_isTouch() || kiwi_is_iOS() || kiwi_isAndroid() || (kiwi_isSmartTV() == 'Samsung') || kiwi_util.mobile_laptop_test); } 217function kiwi_forceMobile() { return kiwi.force_mobile; } 218 219// returns the version number or NaN (later of which will evaluate false in numeric comparisons) 220function kiwi_isSafari() { return (kiwi_safari? kiwi_safari[1] : NaN); } 221 222function kiwi_isFirefox() { return (kiwi_firefox? kiwi_firefox[1] : NaN); } 223 224function kiwi_isChrome() { return (kiwi_chrome? kiwi_chrome[1] : NaN); } 225 226function kiwi_isOpera() { return (kiwi_opera? kiwi_opera[1] : NaN); } 227 228function isStandalone() { 229 return ( 230 window.matchMedia('(display-mode: standalone)').matches || 231 window.navigator.standalone === true || // iOS Safari specific 232 document.referrer.includes('ios-app') // fallback 233 ); 234} 235//setTimeout(function() { if (isStandalone()) alert('isStandalone'); else alert('not'); }, 5000); 236 237function kiwi_version_cb(response_obj) 238{ 239 // Guard against multiple AJAX replies to /VER request that have been seen when using 240 // MacOS "private relay" mode with Safari. 241 if (kiwi.kiwi_version_check_done) { 242 console.log('CAUTION: /VER multiple callback?'); 243 return; 244 } 245 kiwi.kiwi_version_check_done = true; 246 247 if (!isArg(response_obj) || !isNumber(response_obj.maj) || !isNumber(response_obj.min)) { 248 console.log('CAUTION: /VER bad response?'); 249 if (isArg(response_obj)) 250 console.log(response_obj); 251 else 252 console.log('typeof response_obj = '+ typeof(response_obj)); 253 // version_{maj,min} set later from a received send_msg() 254 kiwi.conn_tstamp = (new Date()).getTime(); // fallback 255 kiwi_bodyonload(''); 256 return; 257 } 258 259 version_maj = response_obj.maj; version_min = response_obj.min; 260 kiwi.admin_save_pwd = response_obj.sp; 261 console.log('---- KiwiSDR server v'+ version_maj +'.'+ version_min); 262 //console.log(response_obj); 263 var s=''; 264 265 kiwi_check_js_version.forEach(function(el) {
266 if (el.VERSION_MAJ != version_maj || el.VERSION_MIN != version_min) { 267 if (kiwi.kiwi_version_fail == false) { 268 s = 'Your browser is trying to use incorrect versions of the KiwiSDR Javascript files.<br>' + 269 'Please clear your browser cache and try again.<br>' + 270 'Or click the button below to continue anyway.<br><br>' + 271 'v'+ version_maj +'.'+ version_min +': KiwiSDR server<br>'; 272 kiwi.kiwi_version_fail = true; 273 } 274 s += 'v'+ el.VERSION_MAJ +'.'+ el.VERSION_MIN +': '+ el.file +'<br>'; 275 } 276 //console.log('v'+ el.VERSION_MAJ +'.'+ el.VERSION_MIN +': '+ el.file); 277 }); 278 279 if (kiwi.kiwi_version_fail) 280 s += '<br>'+ w3_button('w3-css-yellow', 'Continue anyway', 'kiwi_version_continue_cb'); 281 282 kiwi.conn_tstamp = isDefined(response_obj.ts)? response_obj.ts : (new Date()).getTime(); 283 //console.log(response_obj); 284 //console.log('conn_tstamp='+ kiwi.conn_tstamp); 285 286 kiwi_bodyonload(s); 287 288 // Workaround for Safari 26.2 SND web socket hang problem. 289 // Doing this /status XHR transaction clears the web socket hang. 290 // However things still do not work in Private Relay mode. 291 // Discovered by Philippe (Tremolat) on the Kiwi Forum. 292 if (kiwi_isSafari()) { 293 setTimeout(function() { 294 kiwi_ajax("/status", function(o) { 295 console.log(o); 296 }); 297 }, 100); 298 } 299} 300 301function kiwi_version_continue_cb() 302{ 303 seriousError = false; 304 kiwi_bodyonload(''); 305} 306 307function ord(c, base) 308{ 309 if (isString(base)) 310 return c.charCodeAt(0) - base.charCodeAt(0); 311 else 312 return c.charCodeAt(0); 313} 314 315function chr(i) 316{ 317 return String.fromCharCode(i); 318} 319 320function sq(s) 321{ 322 return '\''+ s +'\''; 323} 324 325function dq(s) 326{ 327 return '\"'+ s +'\"'; 328} 329 330// "xyz": -- found frequently in JSON 331function dqc(s) 332{ 333 return '"'+ s +'":'; 334} 335 336function paren(s) 337{ 338 return '('+ s +')'; 339} 340 341function pct(s) 342{ 343 if (!isString(s)) s = s.toString(); 344 return s.includes('%')? s : (s +'%'); 345} 346 347function TF(cond) 348{ 349 return (cond? 'T':'F'); 350} 351 352function plural(num, word) 353{ 354 if (num == 1) return word; else return word +'s'; 355} 356 357function arrayBufferToString(buf) { 358 // stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers 359 var s; 360 try { 361 // with Safari, the following gets a "RangeError: Maximum call stack size exceeded" 362 // for large transfers like "MSG dx_json=..." 363 if (0) { 364 s = String.fromCharCode.apply(null, new Uint8Array(buf)); 365 } else { 366 var u8buf = new Uint8Array(buf); 367 s = ''; 368 for (var i = 0; i < u8buf.length; i++) s += String.fromCharCode(u8buf[i]); 369 } 370 } catch (ex) { 371 console.log(buf); 372 console.log(ex); 373 kiwi_trace('arrayBufferToString'); 374 s = null; 375 } 376 return s; 377} 378 379function arrayBufferToStringLen(buf, len) 380{ 381 var u8buf = new Uint8Array(buf); 382 var output = String(); 383 len = Math.min(len, u8buf.length); 384 for (var i = 0; i < len; i++) output += String.fromCharCode(u8buf[i]); 385 return output; 386} 387 388function kiwi_dup_array(a) 389{ 390 return a.slice(); 391} 392 393function kiwi_dedup_array(a, func) 394{ 395 var ra = []; 396 a.forEach(function(v, i) { 397 if (func) { 398 var rv = func(v); 399 if (!rv || rv.err) return; 400 v = rv.v; 401 } 402 if (!ra.includes(v)) ra.push(v); 403 }); 404 return ra; 405} 406 407// turn something iterable with forEach, like a nodeList, into a proper array (which a nodeList is not) 408function iterable_to_array(iter) 409{ 410 var a = []; 411 iter.forEach(function(e) { a.push(e); }); 412 return a; 413} 414 415function kiwi_char_iter(s, func) 416{ 417 var ra = []; 418 s.split('').forEach( 419 function(c,i) { 420 ra[i] = func(c,i); 421 } 422 ); 423 return ra; 424} 425 426function kiwi_delim_iter(s, delim, func) 427{ 428 var ra = []; 429 s.split(delim).forEach( 430 function(ss,i) { 431 ra[i] = func(ss,i); 432 } 433 ); 434 return ra; 435} 436 437// if func() returns undefined then no array entry is made 438function kiwi_array_iter_dup(a, func) 439{ 440 var ra = []; 441 var i = 0; 442 a.forEach( 443 function(el,j) { 444 var nel = func(el,j); 445 if (!isUndefined(nel)) ra[i++] = nel; 446 } 447 ); 448 return ra; 449} 450 451function kiwi_array_iter_rv(a, func) 452{ 453 var rv = 0; 454 a.forEach( 455 function(el,i) { 456 rv |= func(el,i); 457 } 458 ); 459 return rv; 460} 461 462function kiwi_array_join_cond(a, join_char, cond_func) 463{ 464 var j = 0, s = ''; 465 a.forEach( 466 function(el,i) { 467 if (cond_func(el,i)) { 468 s += (j? join_char : '') + el; 469 j++; 470 } 471 } 472 ); 473 return s; 474} 475 476function kiwi_array_remove_undefined(ar) 477{ 478 var ra = []; 479 var j = 0; 480 ar.forEach( 481 function(ae,i) { 482 if (isDefined(ae)) { 483 ra[j] = ae; 484 j++; 485 } 486 } 487 ); 488 return ra; 489} 490 491function kiwi_string_to_hex(s, sep) 492{ 493 var sa = kiwi_char_iter(s, 494 function (c,i) {
495 return c.charCodeAt(0).toHex(-2); 496 } 497 ); 498 return sa.join(sep || ''); 499} 500 501// NB: don't use on an array (will return object, not array) 502function kiwi_shallow_copy(obj) 503{ 504 return Object.assign({}, obj); 505} 506 507function kiwi_deep_copy(obj) 508{ 509 return JSON.parse(JSON.stringify(obj)); 510} 511 512function kiwi_sort_numeric(a, b) { 513 return parseFloat(a) - parseFloat(b); 514} 515 516function kiwi_sort_numeric_reverse(a, b) { 517 return parseFloat(b) - parseFloat(a); 518} 519 520function kiwi_sort_ignore_case(a, b) { 521 a = a.toLowerCase(); 522 b = b.toLowerCase(); 523 if (a === b) return 0; 524 if (a < b) return -1; 525 return 1; 526} 527 528function kiwi_bitReverse(v, len) { 529 if (v == 0) return 0; 530 var rv = 0; 531 for (i = 0; i < len; i++) { 532 rv |= ((v >> i) & 1) << (len - 1 - i); 533 } 534 return rv >>> 0; // force unsigned result if b31 set 535} 536 537function kiwi_bitCount(n) { 538 if (n == 0) return 0; 539 return n.toString(2).match(/1/g).length; 540} 541 542// external API compatibility 543function getFirstChars(buf, len) 544{ 545 arrayBufferToStringLen(buf, len); 546} 547 548function removeEnding(str, ending) 549{ 550 if (str.endsWith(ending)) 551 return str.slice(0, -ending.length); 552 else 553 return str; 554} 555 556function kiwi_inet4_d2h(inet4_str, opts) 557{ 558 opts = opts || {}; 559 var s = inet4_str.split('.'); 560 //console.log('kiwi_inet4_d2h:'); 561 //console.log(s); 562 if (s.length != 4) return null; 563 var check = function(v) { 564 var n = parseInt(v); // includes "" => NaN 565 if (!isNumber(n) || n < 0 || n > 255) return null; 566 return n; 567 }; 568 var a, b, c, d; 569 if ((a = check(s[0])) == null) return null; 570 if ((b = check(s[1])) == null) return null; 571 if ((c = check(s[2])) == null) return null; 572 if ((d = check(s[3])) == null) return null; 573 var ip = (a<<24) | (b<<16) | (c<<8) | d; 574 575 if (opts.no_local_ip) { 576 //console.log('no_local_ip '+ kiwi_ip_str(ip)); 577 if ( 578 (ip >= kiwi_ip_10_lo && ip <= kiwi_ip_10_hi) || 579 (ip >= kiwi_ip_172_16_lo && ip <= kiwi_ip_172_16_hi) || 580 (ip >= kiwi_ip_192_168_lo && ip <= kiwi_ip_192_168_hi) || 581 (ip == kiwi_ip_loopback)) { 582 console.log('EXCLUDE LOCAL RANGE '+ kiwi_ip_str(ip)); 583 return null; 584 } 585 } 586 587 // overlap check with kiwisdr.com IP done on server because it's easier 588 if (opts.no_local_overlap) { 589 var s2 = s[3].split('/'); 590 var nmd = (s2.length >= 2)? parseInt(s2[1]) : 32; 591 if (nmd < 1 || nm > 32) return null; 592 var nm = (~((1<<(32-nmd))-1)) & 0xffffffff; 593 var ip1 = ip & nm; 594 //console.log('no_local_overlap '+ kiwi_ip_str(ip) +' ip1='+ ip1.toHex(8) +' nmd='+ nmd +' nm='+ nm.toHex(8)); 595 if ( 596 (ip1 == (kiwi_ip_loopback & nm)) || 597 (ip1 == (kiwi_ip_10_lo & nm)) || 598 (ip1 == (kiwi_ip_172_16_lo & nm)) || 599 (ip1 == (kiwi_ip_192_168_lo & nm))) { 600 console.log('EXCLUDE LOCAL OVERLAP '+ kiwi_ip_str(ip)); 601 return null; 602 } 603 } 604 return ip; 605} 606 607var kiwi_ip_10_lo = kiwi_inet4_d2h('10.0.0.0'); 608var kiwi_ip_10_hi = kiwi_inet4_d2h('10.255.255.255'); 609var kiwi_ip_172_16_lo = kiwi_inet4_d2h('172.16.0.0'); 610var kiwi_ip_172_16_hi = kiwi_inet4_d2h('172.31.255.255'); 611var kiwi_ip_192_168_lo = kiwi_inet4_d2h('192.168.0.0'); 612var kiwi_ip_192_168_hi = kiwi_inet4_d2h('192.168.255.255'); 613var kiwi_ip_loopback = kiwi_inet4_d2h('127.0.0.1'); 614 615function kiwi_h2n_32(ip, o) 616{ 617 var n; 618 switch (o) { 619 case 0: n = ip>>24; break; 620 case 1: n = ip>>16; break; 621 case 2: n = ip>>8; break; 622 case 3: n = ip; break; 623 default: n = 0; break; 624 } 625 return n & 0xff; 626} 627 628function kiwi_ip_str(ip) 629{ 630 var s=''; 631 632 if (isNumber(ip)) 633 s = kiwi_h2n_32(ip,0)+'.'+kiwi_h2n_32(ip,1)+'.'+kiwi_h2n_32(ip,2)+'.'+kiwi_h2n_32(ip,3); 634 else 635 if (isArray(ip)) 636 s = ip[3] +'.'+ ip[2] +'.'+ ip[1] +'.'+ ip[0]; 637 else 638 if (isObject(ip)) 639 s = ip.a +'.'+ ip.b +'.'+ ip.c +'.'+ ip.d; 640 641 return s; 642} 643 644// http://stackoverflow.com/questions/2998784/how-to-output-integers-with-leading-zeros-in-javascript 645Number.prototype.leadingZeros = function(size) 646{ 647 var s = String(this); 648 if (!isNumber(size)) size = 2; 649 while (s.length < size) s = '0'+ s; 650 return s; 651}; 652 653// size is total number of digits, padded to the left with zeros 654String.prototype.leadingZeros = function(size) 655{ 656 var s = String(this); 657 if (!isNumber(size)) size = 2; 658 while (s.length < size) s = '0'+ s; 659 return s; 660}; 661 662// size is total number of characters, padded to the left with spaces 663Number.prototype.fieldWidth = function(size) 664{ 665 var s = String(this); 666 if (!isNumber(size)) return s; 667 while (s.length < size) s = ' '+ s; 668 return s; 669}; 670 671String.prototype.fieldWidth = function(size) 672{ 673 var s = String(this); 674 if (!isNumber(size)) return s; 675 while (s.length < size) s = ' '+ s; 676 return s; 677}; 678
679// unlike parseInt() considers the entire string 680String.prototype.filterInt = function() { 681 var s = String(this); 682 if (/^(\-|\+)?([0-9]+|Infinity)$/.test(s)) 683 return Number(s); 684 return NaN; 685}; 686 687String.prototype.parseIntEnd = function() { 688 var s = String(this); 689 var a = s.match(/[^-\d]+([-\d]+$)/); 690 //console.log('parseIntEnd <'+ s +'>'); 691 //console.log(a); 692 if (a.length == 2 && isNumber(+a[1])) 693 return Number(a[1]); 694 return NaN; 695}; 696 697String.prototype.withSign = function() 698{ 699 var s = this; 700 var n = Number(s); 701 return (n < 0)? s : ('+'+ s); 702}; 703 704String.prototype.positiveWithSign = function() 705{ 706 var s = this; 707 var n = Number(s); 708 return (n <= 0)? s : ('+'+ s); 709}; 710 711// pad with left zeros to 'digits' length 712// +digits: add leading '0x' 713// -digits: no leading '0x' 714Number.prototype.toHex = function(digits) 715{ 716 if (!isNumber(digits)) digits = 0; 717 var add_0x = (digits < 0)? 0:1; 718 digits = Math.abs(digits); 719 var n = Number(this); 720 n = n >>> 0; // make unsigned 721 var s = n.toString(16); 722 while (s.length < digits) s = '0'+ s; 723 if (add_0x) s = '0x'+ s; 724 return s; 725}; 726 727// minimum number of digits: remove trailing zeros and/or decimal point 728Number.prototype.toFixedNZ = function(d) 729{ 730 var n = Number(this); 731 var s = n.toFixed(d); 732 while (s.endsWith('0')) 733 s = s.slice(0, -1); 734 if (s.endsWith('.')) s = s.slice(0, -1); // nnn.0 => nnn. => nnn 735 return s; 736}; 737 738Number.prototype.toUnits = function(opts) 739{ 740 opts = opts || {}; 741 var prec = isNumberElse(opts.precision, 1); 742 var suffix = function(s) { return (opts.include_space? ' ':'') + s; }; 743 var n = Number(this); 744 if (n < 1000) { 745 return n.toString() + suffix(''); // nnn 746 } else 747 if (n < 1e6) { 748 return (n/1e3).toFixed(prec) + suffix('k'); // nnn.fk 749 } else 750 if (n < 1e9) { 751 return (n/1e6).toFixed(prec) + suffix('M'); // nnn.fM 752 } else { 753 return (n/1e9).toFixed(prec) + suffix('G'); // nnn.fG 754 } 755}; 756 757// allow 'k' (1e3) and 'M' (1e6) suffix 758// use "adj" param to convert returned result in kHz (=1e-3), MHz (=1e-6) etc. 759String.prototype.parseFloatWithUnits = function(allowed_suffixes, adj, frac_digits) { 760 var s = String(this); 761 var v = parseFloat(s); 762 if (isNaN(v)) return NaN; 763 if (!allowed_suffixes || allowed_suffixes.includes('k')) 764 if (new RegExp('([-0-9.]*k)').test(s)) { v *= 1e3; if (isNumber(adj)) v *= adj; } 765 if (!allowed_suffixes || allowed_suffixes.includes('M')) 766 if (new RegExp('([-0-9.]*M)').test(s)) { v *= 1e6; if (isNumber(adj)) v *= adj; } 767 return kiwi_round(v, frac_digits); 768}; 769 770function kiwi_round(v, frac_digits) 771{ 772 if (!isNumber(frac_digits) || frac_digits < 0) return v; 773 var factor = Math.pow(10, frac_digits); 774 return Math.round(v * factor) / factor; 775} 776 777Number.prototype.withSign = function() 778{ 779 var n = Number(this); 780 var s = n.toString(); 781 return (n < 0)? s : ('+'+ s); 782}; 783 784Number.prototype.inRange = function(min, max) 785{ 786 var n = Number(this); 787 return (n >= min && n <= max); 788}; 789 790var kHz_s = function(Hz) { return (Hz / 1e3).toFixed(3) +'k'; }; 791 792// need symmetry for negative f in passband calcs 793function _10Hz(f) { 794 var step = 10; 795 if (f >= 0) { 796 return Math.trunc(f/step) * step; 797 } else { 798 return -(Math.ceil(-f/step) * step); 799 } 800} 801 802function _10Hz_round(f) { 803 var step = 10; 804 return Math.round(f/step) * step; 805} 806 807// like setTimeout() except also calls once immediately 808function kiwi_setTimeout(func, msec, param) 809{ 810 func(param); 811 return setTimeout(func, msec, param); 812} 813 814function kiwi_clearTimeout(timeout) 815{ 816 try { clearTimeout(timeout); } catch(e) {} 817} 818 819// like setInterval() except also calls once immediately 820function kiwi_setInterval(func, msec, param) 821{ 822 func(param); 823 return setInterval(func, msec, param); 824} 825 826function kiwi_clearInterval(interval) 827{ 828 try { clearInterval(interval); } catch(e) {} 829} 830 831var littleEndian = (function() { 832 var buffer = new ArrayBuffer(2); 833 new DataView(buffer).setInt16(0, 256, true /* littleEndian */); 834 // Int16Array uses the platform's endianness. 835 return new Int16Array(buffer)[0] === 256; 836})(); 837 838//console.log('littleEndian='+ littleEndian); 839 840function _nochange(v) 841{ 842 if (arguments.length == 0) return undefined; 843 return (v === undefined); 844} 845 846function _default(v) 847{
848 if (arguments.length == 0) return NaN; 849 return isNaN(v); 850} 851 852function _change(v) 853{ 854 return (!_nochange(v) && !_default(v)); 855} 856 857 858// NB: Have come to understand that console.log() can essentially do all of what console_nv() does. 859 860/* 861var console_nv_help = 862` 863// usage: console_nv( 864// 'id', leading identifier to print 865// 866// local vars wrapped in an object: 867// {arg0}, simple local arg types 868// {'arg1':arg1}, more complex local args 869// {'arr[i]', arr[i]}, array ref 870// {'o.el', o.el}, object ref 871// 872// global vars ref by name wrapped in a string: 873// 's', global simple vars 874// 'a.b.c', global object el FQN 875// ) 876// 877// prints: "<id>: 'actual_arg0_name'=<arg0_value> 'actual_arg1_name'=<arg1_value> ..." 878// 879// e.g. console_nv('screen_char', {r}, {c}, 'kiwi.d.p.nrows') 880`; 881 882function console_nv() 883{ 884 if (arguments.length == 0) { 885 console.log(console_nv_help); 886 return; 887 } 888 889 var s, name, val, val2, object = null; 890 891 for (var i = 0; i < arguments.length; i++) { 892 var arg = arguments[i]; 893 if (i == 0) { 894 s = arg +': '; 895 } else { 896 if (isObject(arg)) { // local variables 897 name = Object.keys(arg)[0]; 898 val = arg[name]; 899 val2 = JSON.stringify(val); 900 //console.log(val2); 901 902 // if JSON.stringify() gives undecoded object use console.log(obj) trick to get full object decoding 903 if (val2 == '[object Object]') object = val; 904 s += name +'='+ (object? '[see next]' : val2) +' '; 905 } else 906 if (isString(arg)) { // global variables 907 try { 908 //val = getVarFromString(arg); 909 val = eval(arg); 910 911 // if eval() gives undecoded object use console.log(obj) trick to get full object decoding 912 if (val == '[object Object]') object = val; 913 val = JSON.stringify(val); 914 } catch(ex) { 915 //console.log('$'+ arg +': '+ ex); 916 val = '[not defined]'; 917 } 918 //var lio = arg.lastIndexOf('.'); 919 //name = (lio == -1)? arg : arg.substr(lio+1); 920 name = arg; 921 s += name +'='+ (object? '[see next]' : val) +' '; 922 } else { 923 s += '[arg'+ (i-1) +' '+ dq(arg) +' invalid type '+ dq(typeof(arg)) +'] '; 924 } 925 } 926 } 927 928 //console.log('CONSOLE_NV'); 929 var caller = kiwi_caller(); 930 //console.log(caller); 931 console.log(s +' ('+ caller.file +':'+ caller.line +')'); 932 if (object) console.log(object); 933} 934*/ 935 936function console_log_dbgUs() 937{ 938 if (!dbgUs) return; 939 s = ''; 940 for (var i = 0; i < arguments.length; i++) 941 s += arguments[i].toString(); 942 console.log(s); 943} 944 945// console log via a timeout for routines that are realtime critical (e.g. audio on_process() routines) 946function kiwi_log(s) 947{ 948 setTimeout(function(s) { 949 console.log(s); 950 }, 1, s); 951} 952 953function kiwi_trace_log(s) 954{ 955 setTimeout(function(s) { 956 kiwi_trace(s); 957 }, 1, s); 958} 959 960 961//////////////////////////////// 962// canvas log 963// use mute button / space bar to clear log 964//////////////////////////////// 965 966function canvas_log_int(s) 967{ 968 var el; 969 if (!kiwi.canvas_log_init) { 970 //kiwi_trace(); 971 el = w3_el('id-news'); 972 el.style.top = ''; 973 el.style.bottom = ''; 974 el.style.left = ''; 975 el.style.right = ''; 976 if (kiwi_isMobile()) { 977 978 // see ext.js for table of device w/h values 979 // these assume portrait orientation 980 var w = screen.width; 981 var h = screen.height; 982 if (w >= 600 && h >= 950) { // iPad & tablets 983 //el.style.top = '36px'; // top 984 //el.style.top = '200px'; // mid 985 el.style.bottom = 0; // bottom 986 el.style.right = 0; 987 //el.style.left = 0; 988 el.style.width = '350px'; 989 //el.style.height = '300px'; 990 el.style.height = '400px'; 991 } else { // phones 992 el.style.top = 0; // top 993 //el.style.top = '36px'; // top 994 //el.style.top = '300px'; // bottom 995 el.style.right = 0; // right 996 //el.style.left = 0; // left 997 //el.style.width = '350px'; 998 el.style.width = '150px'; 999 el.style.height = '150px'; 1000 } 1001 } else { // non-mobile 1002 el.style.bottom = 0; 1003 el.style.left = 0; 1004 el.style.width = '350px'; 1005 el.style.height = '300px'; 1006 } 1007 el.style.visibility = 'visible'; 1008 el.style.zIndex = 9999; 1009 kiwi.canvas_log_init = true; 1010 } 1011 el = w3_el('id-news-inner'); 1012 w3_innerHTML(el, s); 1013 w3_scrollDown(el); 1014} 1015 1016function canvas_log_clear() 1017{ 1018 if (kiwi.canvas_log_init == true) 1019 canvas_log('\f'); 1020} 1021 1022function canvas_log(s) 1023{ 1024 if (!isString(s)) s = s.toString(); 1025 if (isUndefined(owrx.news_acc_s)) owrx.news_acc_s = '<br>'; 1026 var d = new Date(), ts = '';
1027 var time = d.toTimeString().slice(4,8); 1028 if (time != kiwi_util.last_time) { 1029 ts = time +' '; 1030 kiwi_util.last_time = time; 1031 } 1032 1033 if (s == '\f') { 1034 owrx.news_acc_s = '<br>'; 1035 } else 1036 if (s.charAt(0) == '$') { 1037 owrx.news_acc_s += '<br>'+ ts + s; 1038 } else { 1039 owrx.news_acc_s += ((owrx.news_acc_s != '<br>')? ' | ' : '') + ts + s; 1040 } 1041 1042 canvas_log_int(owrx.news_acc_s); 1043} 1044 1045function canvas_log2(s) 1046{ 1047 if (kiwi_isMobile()) 1048 toggle_or_set_hide_bars(owrx.HIDE_BANDBAR); 1049 w3_innerHTML('id-rx-title', kiwi.log2_seq +': '+ s); 1050 kiwi.log2_seq++; 1051} 1052 1053function canvas_catch_log(ex) 1054{ 1055 canvas_log(ex.toString()); 1056 canvas_log(ex.stack); 1057} 1058 1059 1060// from: stackoverflow.com/questions/11547672/how-to-stringify-event-object 1061// Calculate a string representation of a node's DOM path. 1062function kiwi_pathToSelector(node) 1063{ 1064 if (!node || !node.outerHTML) { 1065 return null; 1066 } 1067 1068 var path; 1069 while (node.parentElement) { 1070 var name = node.localName; 1071 if (!name) break; 1072 name = name.toLowerCase(); 1073 var parent = node.parentElement; 1074 1075 var domSiblings = []; 1076 1077 if (parent.children && parent.children.length > 0) { 1078 for (var i = 0; i < parent.children.length; i++) { 1079 var sibling = parent.children[i]; 1080 if (sibling.localName && sibling.localName.toLowerCase) { 1081 if (sibling.localName.toLowerCase() === name) { 1082 domSiblings.push(sibling); 1083 } 1084 } 1085 } 1086 } 1087 1088 if (domSiblings.length > 1) { 1089 name += ':eq(' + domSiblings.indexOf(node) + ')'; 1090 } 1091 path = name + (path ? '>' + path : ''); 1092 node = parent; 1093 } 1094 1095 return path; 1096} 1097 1098// Generate a JSON version of the event. 1099function kiwi_serializeEvent(e) { 1100 if (e) { 1101 var o = { 1102 eventName: e.toString(), 1103 altKey: e.altKey, 1104 bubbles: e.bubbles, 1105 button: e.button, 1106 buttons: e.buttons, 1107 cancelBubble: e.cancelBubble, 1108 cancelable: e.cancelable, 1109 charCode: e.charCode, 1110 code: e.code, 1111 clientX: e.clientX, 1112 clientY: e.clientY, 1113 composed: e.composed, 1114 ctrlKey: e.ctrlKey, 1115 currentTarget: e.currentTarget ? e.currentTarget.outerHTML : null, 1116 defaultPrevented: e.defaultPrevented, 1117 detail: e.detail, 1118 eventPhase: e.eventPhase, 1119 fromElement: e.fromElement ? e.fromElement.outerHTML : null, 1120 isTrusted: e.isTrusted, 1121 key: e.key, 1122 keyCode: e.keyCode, 1123 layerX: e.layerX, 1124 layerY: e.layerY, 1125 metaKey: e.metaKey, 1126 movementX: e.movementX, 1127 movementY: e.movementY, 1128 offsetX: e.offsetX, 1129 offsetY: e.offsetY, 1130 pageX: e.pageX, 1131 pageY: e.pageY, 1132 path: kiwi_pathToSelector(e.path && e.path.length ? e.path[0] : null), 1133 relatedTarget: e.relatedTarget ? e.relatedTarget.outerHTML : null, 1134 returnValue: e.returnValue, 1135 screenX: e.screenX, 1136 screenY: e.screenY, 1137 shiftKey: e.shiftKey, 1138 sourceCapabilities: e.sourceCapabilities ? e.sourceCapabilities.toString() : null, 1139 target: e.target ? e.target.outerHTML : null, 1140 timeStamp: e.timeStamp, 1141 toElement: e.toElement ? e.toElement.outerHTML : null, 1142 type: e.type, 1143 view: e.view ? e.view.toString() : null, 1144 which: e.which, 1145 x: e.x, 1146 y: e.y 1147 }; 1148 1149 var s = JSON.stringify(o, null, 2); 1150 //console.log(s); 1151 return s; 1152 } 1153 1154 return '(null)'; 1155} 1156 1157function key_stringify(evt) 1158{ 1159 if (!evt) return '(no evt?)'; 1160 var s = ''; 1161 if (evt.shiftKey) s += 'shift-'; 1162 if (evt.ctrlKey) s += 'ctrl-'; 1163 if (evt.altKey) s += 'alt-'; 1164 if (evt.metaKey) s += 'meta-'; 1165 s += evt.key? evt.key : '(no key?)'; 1166 return s; 1167} 1168 1169function event_dump(evt, id, oneline) 1170{ 1171 var k = evt.key || '(no key)'; 1172 var tgt_id, ct_id; 1173 1174 var button = "LMR"[evt.button]; 1175 var buttons = ''; 1176 if (evt.buttons & 1) buttons += 'L'; 1177 if (evt.buttons & 4) buttons += 'M'; // yes, 4 is middle 1178 if (evt.buttons & 2) buttons += 'R'; 1179 1180 if (oneline) { 1181 var trel = (isDefined(evt.relatedTarget) && evt.relatedTarget)? (' Trel='+ evt.relatedTarget.id) : ''; 1182 var key = key_stringify(evt); 1183 tgt_id = evt.target? evt.target.id : '(null)'; 1184 ct_id = evt.currentTarget? evt.currentTarget.id : '(null)';
1185 console.log('event_dump '+ id +' |'+ evt.type +'| k='+ key +' b='+ button +' bs='+ buttons +' T='+ tgt_id +' Tcur='+ ct_id + trel); 1186 } else { 1187 console.log('================================'); 1188 if (isNoArg(evt)) { 1189 console.log('EVENT_DUMP: '+ id +' bad evt'); 1190 kiwi_trace(); 1191 return; 1192 } 1193 console.log('EVENT_DUMP: '+ id +' type='+ evt.type); 1194 if (evt.target) { 1195 console.log('key: '+ key_stringify(evt)); 1196 ct_id = evt.currentTarget? evt.currentTarget.id : '(null)'; 1197 console.log('this.id='+ this.id +' tgt.name='+ evt.target.nodeName +' tgt.id='+ evt.target.id +' ctgt.id='+ ct_id); 1198 console.log('button='+ button +' buttons='+ buttons +' detail='+ evt.detail); 1199 1200 if (evt.type.startsWith('touch')) { 1201 console.log('pageX='+ evt.pageX +' clientX='+ evt.clientX +' screenX='+ evt.screenX); 1202 console.log('pageY='+ evt.pageY +' clientY='+ evt.clientY +' screenY='+ evt.screenY); 1203 } else { 1204 console.log('pageX='+ evt.pageX +' clientX='+ evt.clientX +' screenX='+ evt.screenX +' offX(EXP)='+ evt.offsetX +' layerX(DEPR)='+ evt.layerX); 1205 console.log('pageY='+ evt.pageY +' clientY='+ evt.clientY +' screenY='+ evt.screenY +' offY(EXP)='+ evt.offsetY +' layerY(DEPR)='+ evt.layerY); 1206 } 1207 1208 console.log('evt, evt.target, evt.currentTarget, evt.relatedTarget, elementFromPoint:'); 1209 console.log(evt); 1210 console.log(evt.target); 1211 console.log(evt.currentTarget); 1212 console.log(evt.relatedTarget); 1213 var el_s = w3_elementAtPointer(evt); 1214 if (el_s) 1215 console.log(el_s); 1216 else 1217 console.log('(no x,y)'); 1218 } 1219 console.log('----'); 1220 } 1221} 1222 1223function any_alternate_click_event(evt) 1224{ 1225 return (evt && (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.button == mouse.MIDDLE || evt.button == mouse.RIGHT)); 1226} 1227 1228function any_alternate_click_event_except_shift(evt) 1229{ 1230 return (evt && (evt.ctrlKey || evt.altKey || evt.button == mouse.MIDDLE || evt.button == mouse.RIGHT)); 1231} 1232 1233function any_modifier_key(evt) 1234{ 1235 return (evt && (evt.shiftKey || evt.ctrlKey || evt.altKey)); 1236} 1237 1238function any_modifier_key_except_shift(evt) 1239{ 1240 return (evt && (evt.ctrlKey || evt.altKey || evt.metaKey)); 1241} 1242 1243// NB: returns a function reference 1244function kiwi_rateLimit(cb, msec) 1245{ 1246 var waiting = false; 1247 var rtn = function() { 1248 if (waiting) return; 1249 waiting = true; 1250 var args = arguments; 1251 setTimeout(function() { 1252 waiting = false; 1253 if (isString(cb)) 1254 w3_call(cb); 1255 else 1256 cb.apply(this, args); 1257 }, msec); 1258 }; 1259 return rtn; 1260} 1261 1262function kiwi_UTC_minutes() 1263{ 1264 var d = new Date(); 1265 return (d.getUTCHours()*60 + d.getUTCMinutes()); 1266} 1267 1268function kiwi_dhms(secs) 1269{ 1270 var t = +secs; 1271 var sec = Math.trunc(t % 60); t = Math.trunc(t/60); 1272 var min = Math.trunc(t % 60); t = Math.trunc(t/60); 1273 var hr = Math.trunc(t % 24); t = Math.trunc(t/24); 1274 var days = t; 1275 var hms = hr +':'+ min.leadingZeros(2) +':'+ sec.leadingZeros(2); 1276 var d = ''; 1277 if (days) d = days +'d:'; 1278 var dhms = d + hms; 1279 return { dhms:dhms, hms:hms, days:days, hr:hr, min:min, sec:sec, secs:secs }; 1280} 1281 1282function kiwi_hh_mm(hh_mm) 1283{ 1284 if (isNumber(hh_mm)) return hh_mm; 1285 1286 if (isString(hh_mm)) { 1287 var t = hh_mm.split(':'); 1288 var hr = +t[0]; 1289 if (t.length > 1) { 1290 var min = (+t[1]) / 60; 1291 hr = (hr < 0)? (hr - min) : (hr + min); 1292 } 1293 if (t[0] == '-0' || t[0] == '-00') 1294 hr = -hr; 1295 return hr; 1296 } 1297 1298 return null; 1299} 1300//console.log('# '+ kiwi_hh_mm(-11) +' '+ + kiwi_hh_mm('-10') +' '+ kiwi_hh_mm('10:55') +' '+ kiwi_hh_mm('-10:55')); 1301 1302// stackoverflow.com/questions/8619879/javascript-calculate-the-day-of-the-year-1-366 1303function kiwi_UTCdoyToDate(doy, year, hour, min, sec) 1304{ 1305 return new Date(Date.UTC(year, 0, doy, hour, min, sec)); // yes, doy = 1..366 really works! 1306} 1307 1308function kiwi_decodeURIComponent(id, uri) 1309{ 1310 var i, j, k; 1311 var obj = null, double_fail = false; 1312 1313 if (dbgUs && arguments.length != 2) {
1314 alert('kiwi_decodeURIComponent: requires exactly two args'); 1315 kiwi_trace(); 1316 return null; 1317 } 1318 1319 while (obj == null) { 1320 try { 1321 obj = decodeURIComponent(uri); 1322 } catch(ex) { 1323 console.log('kiwi_decodeURIComponent('+ id +'): decode fail'); 1324 console.log(uri); 1325 console.log(ex); 1326 1327 if (double_fail) { 1328 console.log('kiwi_decodeURIComponent('+ id +'): DOUBLE DECODE FAIL'); 1329 console.log(uri); 1330 console.log(ex); 1331 return null; 1332 } 1333 double_fail = true; 1334 1335 // v1.464 1336 // Recover from broken UTF-8 sequences stored in cfg. 1337 // Remove all "%xx" sequences, for xx >= 0x80, whenever decodeURIComponent() fails. 1338 // User will have to manually repair UTF-8 sequences since information has been lost. 1339 // 1340 // NB: If a cfg field contains only valid UTF-8 sequences then the decodeURIComponent() will not fail 1341 // and this code will not be triggered. That way corrections made to broken fields will persist in the cfg. 1342 // This is why bulk removal of >= %80 sequences cannot be done in _cfg_load_json() on the server side. 1343 // Doing that would always eliminate *any* UTF-8 sequence. Even valid ones. 1344 1345 for (i = 0; i < uri.length - 2; i++) { 1346 if (uri.charAt(i) == '%') { 1347 var c1 = uri.charAt(i+1); 1348 var c2 = uri.charAt(i+2); 1349 if (isxdigit(c1) && isxdigit(c2)) { 1350 //console.log(c1 +' '+ TF(c1 >= '8')); 1351 if (c1 >= '8') { 1352 //var x0 = uri.charAt(i-1); 1353 //x0 = x0.charCodeAt(0); 1354 j = Math.max(i-5, 0); k = Math.min(i+6, uri.length); 1355 console.log('BAD @'+ i +' <'+ uri.slice(j,k) +'>'); 1356 uri = uri.substr(0,i) + uri.substr(i+3); 1357 i = 0; // NB: this makes the for loop start over! 1358 //console.log('FIX <'+ uri +'>'); 1359 } 1360 } else { 1361 // % => %25 1362 uri = uri.substr(0,i) +'%25'+ uri.substr(i+1); 1363 // NB: Okay to just let the loop proceed here since index i++ now 1364 // points to the '2' of the '%25' we just inserted. 1365 } 1366 } 1367 } 1368 1369 // Must check for two cases of '%' in the last two locations in the string (not caught by above code). 1370 i = uri.length - 2; 1371 if (uri.charAt(i) == '%') uri = uri.substr(0,i) +'%25'+ uri.substr(i+1); 1372 i = uri.length - 1; 1373 if (uri.charAt(i) == '%') uri = uri.substr(0,i) +'%25'; 1374 1375 console.log('kiwi_decodeURIComponent FINAL FIX <'+ uri +'>'); 1376 obj = null; 1377 } 1378 } 1379 1380 return obj; 1381} 1382 1383kiwi_util.str_recode_lookup = []; 1384var e1 = function(c) { kiwi_util.str_recode_lookup[ord(c)] = 1; }; 1385var e2 = function(c) { kiwi_util.str_recode_lookup[ord(c)] = 2; }; 1386var er = function(r1, r2, v) { 1387 for (var i = r1; i <= r2; i++) 1388 kiwi_util.str_recode_lookup[i] = v; 1389}; 1390er(0, 127, 0); 1391e1('&'); e1("'"); e1('+'); e1(';'); e1('<'); e1('>'); e1('`'); 1392e2('"'); e2('%'); e2('\\'); 1393er(0x00, 0x1f, 2); er(0x7f, 0x7f, 2); 1394//console.log(kiwi_util.str_recode_lookup); 1395 1396/* 1397 1398XXX This doesn't work because of UTF-8! 1399 Like server-side, needs to be done on already %-encoded UTF-8 bytes 1400 1401// Equivalent of server side kiwi_url_decode_selective() except 1402// that it works in the opposite direction, i.e. encoding un-encoded strings. 1403// Encodes a more limited set of characters than encodeURIComponent() 1404// to help with readability of the various configuration files. 1405 1406function kiwi_str_encode_selective(s, fewer_encoded) 1407{ 1408 var d = ''; 1409 1410 for (var i = 0; i < s.length; i++) { 1411 var c = s[i]; 1412 var n = ord(c); 1413 var lu = kiwi_util.str_recode_lookup[n]; 1414 if ((fewer_encoded && lu == 2) || (!fewer_encoded && lu != 0)) 1415 d += '%'+ n.toHex(-2); else d += c; 1416 } 1417 1418 return d; 1419} 1420console.log(kiwi_str_encode_selective('ABC!@#$^*()_-={}[]|:,./?~ "%&\'+;<>\\`XYZ'+ String.fromCharCode(0, 1, 0x1f, 0x7f, 0x80))); 1421*/ 1422 1423// js version of service-side kiwi_url_decode_selective() 1424function kiwi_str_decode_selective_inplace(src, fewer_encoded) 1425{ 1426 var i, dst = ''; 1427 var src_len = src.length; 1428 var dst_len = src.length + 1; 1429 var hex_digit = /[0-9a-fA-F]/; 1430 1431 for (i = 0; i < src_len; i++) { 1432 if (src[i] == '%' && i + 2 < src_len && 1433 src[i + 1].match(hex_digit) && 1434 src[i + 2].match(hex_digit)) { 1435 1436 var c = (parseInt(src[i+1], 16) << 4) | parseInt(src[i+2], 16); 1437 1438 // preserve UTF-8 encoding values >= 0x80! 1439 if (c < 0x80 && (kiwi_util.str_recode_lookup[c] == 0 || (fewer_encoded && kiwi_util.str_recode_lookup[c] == 1))) { 1440 dst += chr(c); 1441 i += 2; 1442 } else { 1443 dst += '%'; 1444 } 1445 } else { 1446 dst += src[i]; 1447 } 1448 } 1449 1450 return dst; 1451} 1452 1453function kiwi_stringify(o) 1454{ 1455 return JSON.stringify(o); 1456} 1457 1458function kiwi_JSON(json, pretty) 1459{ 1460 return JSON.stringify(json, null, pretty? 3:undefined); 1461} 1462 1463function kiwi_JSON_parse(tag, json, func) 1464{ 1465 var obj; 1466 try { 1467 obj = JSON.parse(json); 1468 } catch(ex) { 1469 console.log('kiwi_JSON_parse('+ tag +'): JSON parse fail, len='+ json.length); 1470 console.log(json); 1471 console.log(ex); 1472 w3_call(func, ex); 1473 obj = null; 1474 } 1475 return obj; 1476} 1477 1478// remove ANSI "ESC[ ... m" sequences and our HTML escape sequences 1479function kiwi_remove_escape_sequences(s) 1480{ 1481 var a1 = s.split(''); 1482 var a2 = []; 1483 var inANSI = false, inHTML = false;
1484 for (var i = 0; i < a1.length; i++) { 1485 var c = a1[i]; 1486 if (inANSI) { 1487 if (c == 'm') inANSI = false; 1488 } else 1489 if (inHTML) { 1490 if (c == kiwi.esc_gt) inHTML = false; 1491 } else { 1492 if (c == '\u001b' && (i+1) < a1.length && a1[i+1] == '[') { 1493 i++; 1494 inANSI = true; 1495 } else 1496 if (c == kiwi.esc_lt) { 1497 inHTML = true; 1498 } else { 1499 if (c != '\f') // ignore kiwi_output_msg() screen clear form feed 1500 a2.push(c); 1501 } 1502 } 1503 } 1504 1505 return a2.join(''); 1506} 1507 1508function kiwi_timestamp() 1509{ 1510 return new Date().toISOString().replace(/:/g, '_').replace(/\.[0-9]+Z$/, 'Z'); 1511} 1512 1513function kiwi_timestamp_filename(pre, post) 1514{ 1515 var el = w3_el('id-freq-input'); 1516 var freq_mode = el? ('_'+ el.value +'_'+ ext_get_mode()) : ''; 1517 return pre + kiwi_host() +'_'+ kiwi_timestamp() + freq_mode + post; 1518} 1519 1520 1521//////////////////////////////// 1522// lat/lon/grid 1523//////////////////////////////// 1524 1525// field square subsquare (extended square) 1526// A-R 0-9 a-x 0-9 1527// #18 #10 #24 #10 1528 1529var grid_sq = { 1530 SQ_LON_DEG: 2, 1531 SQ_LAT_DEG: 1, 1532 SUBSQ_PER_SQ: 24, 1533 SQ_PER_FLD: 10, 1534 1535 field: "ABCDEFGHIJKLMNOPQR", 1536 square: "0123456789", 1537 subsquare: "abcdefghijklmnopqrstuvwx" 1538}; 1539 1540grid_sq.SUBSQ_LON_DEG = grid_sq.SQ_LON_DEG / grid_sq.SUBSQ_PER_SQ; 1541grid_sq.SUBSQ_LAT_DEG = grid_sq.SQ_LAT_DEG / grid_sq.SUBSQ_PER_SQ; 1542grid_sq.FLD_DEG_LON = grid_sq.SQ_PER_FLD * grid_sq.SQ_LON_DEG; 1543grid_sq.FLD_DEG_LAT = grid_sq.SQ_PER_FLD * grid_sq.SQ_LAT_DEG; 1544 1545function grid_to_latLon(grid) 1546{ 1547 var lat, lon, c; 1548 var _a = function(c) { return ord(c) - ord('a'); }; 1549 var _0 = function(c) { return ord(c) - ord('0'); }; 1550 1551 if (isEmptyString(grid)) return false; 1552 var slen = grid.length; 1553 if (slen < 4) return false; 1554 1555 c = grid[0].toLowerCase(); 1556 if (c < 'a' || c > 'r') return false; 1557 lon = _a(c)*20 - 180; 1558 1559 c = grid[1].toLowerCase(); 1560 if (c < 'a' || c > 'r') return false; 1561 lat = _a(c)*10 - 90; 1562 1563 c = grid[2]; 1564 if (!isdigit(c)) return false; 1565 lon += _0(c) * grid_sq.SQ_LON_DEG; 1566 1567 c = grid[3]; 1568 if (!isdigit(c)) return false; 1569 lat += _0(c) * grid_sq.SQ_LAT_DEG; 1570 1571 if (slen != 6) { // assume center of square (i.e. "....ll") 1572 lon += grid_sq.SQ_LON_DEG /2.0; 1573 lat += grid_sq.SQ_LAT_DEG /2.0; 1574 } else { 1575 c = grid[4].toLowerCase(); 1576 if (c < 'a' || c > 'x') return false; 1577 lon += _a(c) * grid_sq.SUBSQ_LON_DEG; 1578 1579 c = grid[5].toLowerCase(); 1580 if (c < 'a' || c > 'x') return false; 1581 lat += _a(c) * grid_sq.SUBSQ_LAT_DEG; 1582 1583 lon += grid_sq.SUBSQ_LON_DEG /2.0; // assume center of sub-square (i.e. "......44") 1584 lat += grid_sq.SUBSQ_LAT_DEG /2.0; 1585 } 1586 1587 //console.log(sprintf("GRID %s%s = (%f, %f)\n", grid, (slen != 6)? "[ll]":"", lat, lon)); 1588 return { lat:lat, lon:lon }; 1589} 1590 1591 1592function latLon_to_grid6(lat, lon) 1593{ 1594 var i, r, grid6 = []; 1595 //console.log(sprintf("latLon_to_grid6: lat=%f lon=%f\n", lat, lon)); 1596 1597 // longitude 1598 lon += 180.0; 1599 if (lon < 0 || lon >= 360.0) lon = 0; 1600 i = Math.floor(lon / grid_sq.FLD_DEG_LON); 1601 grid6[0] = grid_sq.field[i]; 1602 r = lon - (i * grid_sq.FLD_DEG_LON); 1603 1604 i = Math.floor(r / grid_sq.SQ_LON_DEG); 1605 grid6[2] = grid_sq.square[i]; 1606 r = r - (i * grid_sq.SQ_LON_DEG); 1607 1608 i = Math.floor(r * (grid_sq.SUBSQ_PER_SQ / grid_sq.SQ_LON_DEG)); 1609 grid6[4] = grid_sq.subsquare[i]; 1610 1611 // latitude 1612 lat += 90.0; 1613 if (lat < 0 || lat >= 180.0) lat = 0; 1614 i = Math.floor(lat / grid_sq.FLD_DEG_LAT); 1615 grid6[1] = grid_sq.field[i]; 1616 r = lat - (i * grid_sq.FLD_DEG_LAT); 1617 1618 i = Math.floor(r / grid_sq.SQ_LAT_DEG); 1619 grid6[3] = grid_sq.square[i]; 1620 r = r - (i * grid_sq.SQ_LAT_DEG); 1621 1622 i = Math.floor(r * (grid_sq.SUBSQ_PER_SQ / grid_sq.SQ_LAT_DEG)); 1623 grid6[5] = grid_sq.subsquare[i]; 1624 1625 //console.log(sprintf("latLon_to_grid6: grid6=%s\n", grid6.join(''))); 1626 return grid6.join(''); 1627} 1628 1629function deg_2_rad(deg) { return deg * Math.PI / 180.0; } 1630 1631function latLon_deg_to_rad(loc) { 1632 return { lat: deg_2_rad(loc.lat), lon: deg_2_rad(loc.lon) }; 1633} 1634 1635function grid_to_distance_km(r_lat, r_lon, grid) 1636{ 1637 var loc = grid_to_latLon(grid); 1638 if (loc == false) return 0; 1639 //console.log(sprintf("grid_to_distance_km: from(%f,%f) to(%s,%f,%f)\n", r_lat, r_lon, grid, loc.lat, loc.lon)); 1640 loc = latLon_deg_to_rad(loc); 1641 1642 var delta_lat = loc.lat - r_lat; 1643 delta_lat /= 2.0; 1644 delta_lat = Math.sin(delta_lat); 1645 delta_lat *= delta_lat; 1646 var delta_lon = loc.lon - r_lon; 1647 delta_lon /= 2.0; 1648 delta_lon = Math.sin(delta_lon); 1649 delta_lon *= delta_lon; 1650 1651 var t = delta_lat + (delta_lon * Math.cos(loc.lat) * Math.cos(r_lat));
1652 var km = Math.ceil(/* EARTH_RADIUS_KM */ 6371.0 * 2.0 * Math.atan2(Math.sqrt(t), Math.sqrt(1.0-t))); 1653 //console.log(sprintf("grid_to_distance_km: km=%d\n", km)); 1654 return km; 1655} 1656 1657 1658//////////////////////////////// 1659// HTML helpers 1660//////////////////////////////// 1661 1662function kiwi_clean_html(s) 1663{ 1664 return s.replace(/\&/g, '&').replace(/</g, '<').replace(/\>/g, '>'); 1665} 1666 1667function kiwi_clean_newline(s) 1668{ 1669 return s.replace(/[\r\n\f\v]/g, '').replace(/\t/g, ' '); 1670} 1671 1672// returns "http://" or "https://" depending if connection to Kiwi is using SSL or not 1673function kiwi_SSL() 1674{ 1675 return window.location.protocol +'//'; 1676} 1677 1678// returns the entire url including port and query string 1679function kiwi_url() 1680{ 1681 return window.location.href; 1682} 1683 1684// returns http[s]://host[:port] 1685function kiwi_url_origin() 1686{ 1687 return window.location.origin; 1688} 1689 1690// returns host[:port] 1691function kiwi_host_port() 1692{ 1693 return window.location.host; 1694} 1695 1696// returns host (no :port) 1697function kiwi_host() 1698{ 1699 return window.location.hostname; 1700} 1701 1702function kiwi_remove_protocol(url) 1703{ 1704 return url.replace(/^http:\/\//, '').replace(/^https:\/\//, ''); 1705} 1706 1707function kiwi_clean_url(url) 1708{ 1709 url = kiwi_remove_protocol(url).split(':'); 1710 var h = url[0], p = url[1]; 1711 if (isEmptyString(h)) return ''; 1712 var isProxy = (h.includes('proxy') && h.includes('kiwisdr.com')); 1713 if (isProxy) p = ''; // don't include ':8073' in proxy URLs 1714 var port = ''; // allow for Kiwi's on port 80 1715 if (url.length >= 2) { 1716 var n = parseInt(p); 1717 if (isNumber(n)) port = n; 1718 } 1719 return w3_sbc(':', h, port); 1720} 1721 1722// only handles IPv4-mapped IPv6 1723function kiwi_isPublic_url(host) { 1724 var isPublic_url = true; 1725 host = host.replace(/^::ffff:/, ''); // IPv4-mapped IPv6 1726 var dotted = host.split('.').length; 1727 if (dotted <= 1) 1728 isPublic_url = false; // simple hostname 1729 if (isPublic_url && (dotted == 4) && kiwi_inet4_d2h(host)) { 1730 if (!kiwi_inet4_d2h(host, { no_local_ip:1 })) { 1731 isPublic_url = false; // local IPv4 address 1732 } 1733 } 1734 if (isPublic_url && host.endsWith('.local')) 1735 isPublic_url = false; // ending in .local 1736 return isPublic_url; 1737} 1738 1739function kiwi_open_or_reload_page(obj) // { url, hp, path, qs, tab, delay } 1740{ 1741 var url = w3_opt(obj, 'url', null); 1742 if (url == 'reload') 1743 url = kiwi_url(); 1744 else 1745 if (isNull(url)) { 1746 var host_port = w3_opt(obj, 'hp', kiwi_host_port()); 1747 var pathname = w3_opt(obj, 'path', '', '/'); 1748 var query = w3_opt(obj, 'qs', '', '/?'); 1749 url = kiwi_SSL() + host_port + pathname + query; 1750 } 1751 var delay = w3_opt(obj, 'delay', 0); 1752 //console.log('kiwi_open_or_reload_page: '+ url +' delay='+ delay); 1753 1754 var reload = function() { 1755 var rv; 1756 if (w3_opt(obj, 'tab', false)) { 1757 //console.log('kiwi_open_or_reload_page: NEW TAB url='+ url); 1758 rv = window.open(url, '_blank'); 1759 } else { 1760 //console.log('kiwi_open_or_reload_page: NEW WIN url='+ url); 1761 window.location.href = url; 1762 rv = true; 1763 } 1764 return rv; 1765 }; 1766 1767 var rval; 1768 if (delay) { 1769 setTimeout(function(url) { reload(); }, delay); 1770 rval = true; 1771 } else { 1772 rval = reload(); 1773 } 1774 return rval; 1775} 1776 1777function kiwi_add_end(s, end) 1778{ 1779 if (!s.endsWith(end)) s = s + end; 1780 return s; 1781} 1782 1783// pnames can be: 'string' or [ 'string1', 'string2', ... ] or a parameter position index 1784function kiwi_url_param(pnames, default_val, not_found_val) 1785{ 1786 var pn_isArray = isArray(pnames); 1787 var pn_isString = isString(pnames); 1788 var pn_isNumber = isNumber(pnames); 1789 if (isUndefined(default_val)) default_val = true; 1790 if (isUndefined(not_found_val)) not_found_val = null; 1791 1792 // skip initial '?' 1793 var params = (window.location && window.location.search && window.location.search.substr(1)); 1794 if (!params) return not_found_val; 1795 1796 var rv = not_found_val; 1797 params.split("&").forEach(function(pv, i) { 1798 var pv_a = pv.split("="); 1799 if (pn_isArray) { 1800 pnames.forEach(function(pn) { 1801 if (pn != pv_a[0]) return; 1802 rv = (pv_a.length >= 2)? pv_a[1] : default_val; 1803 //console.log('kiwi_url_param '+ pn +'='+ rv); 1804 }); 1805 } else 1806 if (pn_isString) { 1807 if (pnames == pv_a[0]) { 1808 rv = (pv_a.length >= 2)? pv_a[1] : default_val; 1809 //console.log('kiwi_url_param '+ pnames +'='+ rv); 1810 } 1811 } else 1812 if (pn_isNumber) { 1813 if (i == +pnames) 1814 rv = pv_a[0]; 1815 } 1816 }); 1817 1818 return rv; 1819} 1820 1821function kiwi_add_search_param(location, param) 1822{ 1823 var p; 1824 var qs = location.search; 1825 console.log('qs=<'+ qs +'>'); 1826 if (qs == '') p = '/?'; 1827 else if (qs.includes('?')) p = '&'; 1828 else p = '/?'; 1829 return p + param; 1830} 1831 1832function kiwi_remove_search_param(url, p) 1833{ 1834 url = url 1835 .replace('?'+ p +'&', '?') 1836 .replace('&'+ p +'&', '&') 1837 .replace('?'+ p +'', '') 1838 .replace('&'+ p +'', ''); 1839 return url; 1840} 1841 1842function html(el_id) 1843{ 1844 return w3_el(el_id); 1845} 1846 1847function px(s) 1848{ 1849 if (isNoArg(s) || s == '') return '0'; 1850 var num = parseFloat(s); 1851 if (isNaN(num)) { 1852 console.log('px num='+ s); 1853 kiwi_trace(); 1854 return '0'; 1855 } 1856 return num.toFixed(0) +'px'; 1857} 1858 1859function unpx(s) 1860{ 1861 return parseFloat(s); // parseFloat() because s = "nnnpx" 1862} 1863 1864function css_style(el, prop) 1865{ 1866 el = w3_el(el); 1867 if (!el) return null; 1868 var style = getComputedStyle(el, null); 1869 //console.log('css_style id='+ el.id); 1870 //console.log(style); 1871 var val = style.getPropertyValue(prop); 1872 return val; 1873} 1874 1875function css_style_num(el, prop) 1876{ 1877 el = w3_el(el); 1878 if (!el) return null; 1879 var v = parseInt(css_style(el, prop)); 1880 if (isNaN(v)) v = 0; 1881 //console.log('css_style_num '+ prop +'='+ v); 1882 return v; 1883} 1884 1885function html_LR_border_pad(el) 1886{ 1887 var bL = css_style_num(el, 'border-left-width'); 1888 var bR = css_style_num(el, 'border-right-width'); 1889 var pL = css_style_num(el, 'padding-left'); 1890 var pR = css_style_num(el, 'padding-right'); 1891 //console.log('html_LR_border_pad id='+el.id+' border: '+bL+','+bR); 1892 //console.log('html_LR_border_pad id='+el.id+' padding: '+pL+','+pR); 1893 return bL+bR + pL+pR; 1894} 1895 1896// from http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel 1897function cancelEvent(ev) 1898{ 1899 ev = ev ? ev : window.event; 1900 if (ev.stopPropagation) ev.stopPropagation(); 1901 if (ev.preventDefault) ev.preventDefault(); 1902 if (ev.stopImmediatePropagation) ev.stopImmediatePropagation(); 1903 ev.cancelBubble = true; 1904 ev.cancel = true; 1905 ev.returnValue = false;
1906 return false; 1907} 1908 1909function ignore(ev) 1910{ 1911 return cancelEvent(ev); 1912} 1913 1914function preventDefault(ev) 1915{ 1916 ev.preventDefault(); 1917 return false; 1918} 1919 1920function kiwi_rgb(r, g, b) 1921{ 1922 if (r == null) return ''; 1923 1924 if (isArray(r)) { 1925 g = r[1]; 1926 b = r[2]; 1927 r = r[0]; 1928 } 1929 1930 return 'rgb('+ Math.floor(r) +','+ Math.floor(g) +','+ Math.floor(b) +')'; 1931} 1932 1933function hsl(h, s, l) 1934{ 1935 s = Math.floor(s); 1936 if (s < 0) s = 0; 1937 if (s > 100) s = 100; 1938 l = Math.floor(l); 1939 if (l < 0) l = 0; 1940 if (l > 100) l = 100; 1941 return 'hsl('+ Math.round(h) +','+ s +'%,'+ l +'%)'; 1942} 1943 1944 1945//////////////////////////////// 1946// storage (browser) 1947//////////////////////////////// 1948 1949// NB: appinventor.mit.edu used by KiwiSDR Android app doesn't have localStorage 1950 1951function kiwi_storeRead(k, def) 1952{ 1953 var rv; 1954 if (isNoArg(k)) return null; 1955 1956 if (kiwi.noLocalStorage) { 1957 rv = readCookie(k, def); 1958 //alert('sRd k='+ k +' d='+ def +' r='+ rv); 1959 return rv; 1960 } 1961 1962 try { 1963 // requires that rv == null means item not in storage 1964 // not that the value stored is null 1965 rv = localStorage.getItem(k); 1966 if (rv == null && isArg(def)) 1967 rv = def; 1968 } catch(ex) { 1969 console.log('$>kiwi_storeRead CATCH: '+ k); 1970 console.log('$>kiwi_storeRead CATCH: '+ ex); 1971 rv = null; 1972 } 1973 1974 return rv; 1975} 1976 1977function kiwi_storeInit(k, init) 1978{ 1979 var rv; 1980 if (isNoArg(k)) return null; 1981 1982 if (kiwi.noLocalStorage) { 1983 rv = initCookie(k, init); 1984 //alert('sIn k='+ k +' i='+ init +' r='+ rv); 1985 return rv; 1986 } 1987 1988 try { 1989 rv = localStorage.getItem(k); 1990 if (rv == null) { 1991 if (!isArg(init)) init = ''; // don't allow stored values of null or undefined 1992 kiwi_storeWrite(k, init); // how it differs from kiwi_storeRead() 1993 rv = init; 1994 } 1995 } catch(ex) { 1996 console.log('$>kiwi_storeInit CATCH: '+ k); 1997 console.log('$>kiwi_storeInit CATCH: '+ ex); 1998 rv = null; 1999 } 2000 2001 return rv; 2002} 2003 2004function kiwi_storeWrite(k, v) 2005{ 2006 if (isNoArg(k)) return null; 2007 2008 if (kiwi.noLocalStorage) { 2009 var rv = writeCookie(k, v); 2010 //alert('sWr k='+ k +' v='+ v +' r='+ rv); 2011 return rv; 2012 } 2013 2014 try { 2015 if (!isArg(v)) v = ''; // don't allow stored values of null or undefined 2016 localStorage.setItem(k, v); 2017 } catch(ex) { 2018 console.log('$>kiwi_storeWrite CATCH: '+ k); 2019 console.log('$>kiwi_storeWrite CATCH: '+ ex); 2020 return null; 2021 } 2022 2023 return true; 2024} 2025 2026function kiwi_storeDelete(k) 2027{ 2028 if (isNoArg(k)) return null; 2029 2030 if (kiwi.noLocalStorage) { 2031 var rv = deleteCookie(k); 2032 //alert('sDel k='+ k +' r='+ rv); 2033 return rv; 2034 } 2035 2036 try { 2037 localStorage.removeItem(k); 2038 } catch(ex) { 2039 return null; 2040 } 2041 2042 return true; 2043} 2044 2045function migrateCookies() { 2046 if (kiwi.noLocalStorage || !isArg(document.cookie)) return; 2047 var ca = document.cookie.split(';'); 2048 var len = ca.length; 2049 if (len == 0 || (len == 1 && isEmptyString(ca))) return; 2050 for (var i = 0; i < len; i++) { 2051 var c = ca[i].trim(); 2052 var a = c.split('='); 2053 var k = a[0]; 2054 var v = a[1]; 2055 if (kiwi.noLocalStorage) { 2056 alert(k +'='+ v); 2057 } else { 2058 console.log('migrateCookies k='+ k +' v='+ sq(v) +' '+ typeof(v)); 2059 var rv = kiwi_storeRead(k); 2060 if (0) { 2061 if (isArg(rv)) { 2062 console.log('migrateCookies WARNING key already exists in new storage? k='+ k +' v='+ sq(rv)); 2063 } else { 2064 kiwi_storeWrite(k, v); 2065 } 2066 } else { 2067 // compatibility with HTML_HEAD <script> that does: document.cookie = "key = val"; 2068 kiwi_storeWrite(k, v); 2069 } 2070 deleteCookie(k); 2071 } 2072 } 2073 console.log('migrateCookies #'+ len); 2074} 2075 2076// from http://www.quirksmode.org/js/cookies.html 2077function _createCookie(name, value, days) { 2078 var expires = ""; 2079 if (days) { 2080 var date = new Date(); 2081 date.setTime(date.getTime() + (days*24*60*60*1000)); 2082 expires = "; expires="+ date.toGMTString(); 2083 } 2084 //console.log('_createCookie <'+ name +"="+ value + expires +"; path=/" +'>'); 2085 document.cookie = name +"="+ value + expires +"; path=/; SameSite=Lax;"; 2086} 2087 2088// returns: 2089// cookie exists: cookie value 2090// cookie doesn't exist: default value else null (if no default specified) 2091function readCookie(name, defaultValue) { 2092 var nameEQ = name + "="; 2093 var ca = document.cookie.split(';'); 2094 for (var i=0; i < ca.length; i++) { 2095 var c = ca[i]; 2096 while (c.charAt(0) == ' ') c = c.substring(1, c.length); 2097 //console.log('readCookie '+ name +' consider <'+ c +'>'); 2098 if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); 2099 } 2100 if (isArg(defaultValue)) { 2101 return defaultValue; 2102 } else { 2103 return null; 2104 } 2105} 2106 2107function writeCookie(cookie, value) 2108{ 2109 _createCookie(cookie, value, 42*365); 2110} 2111 2112// returns: 2113// 2114// cookie doesn't exist: write initValue; return initValue 2115// else return cookie value 2116function initCookie(cookie, initValue) 2117{ 2118 var v = readCookie(cookie); 2119 if (v == null) { 2120 writeCookie(cookie, initValue); 2121 return initValue; 2122 } else { 2123 return v; 2124 } 2125} 2126 2127// returns: 2128// cookie doesn't exist OR != initValue: write cookie with initV
2128alue; return true 2129// else return false 2130/* 2131function updateCookie(cookie, initValue) 2132{ 2133 var v = readCookie(cookie); 2134 if (v == null || v != initValue) { 2135 writeCookie(cookie, initValue); 2136 return true; 2137 } else { 2138 return false; 2139 } 2140} 2141*/ 2142 2143function deleteCookie(cookie) 2144{ 2145 var v = readCookie(cookie); 2146 if (v == null) return; 2147 _createCookie(cookie, 0, -1); 2148} 2149 2150 2151//////////////////////////////// 2152// var get/set from string 2153//////////////////////////////// 2154 2155// Get function from string, with or without scopes (by Nicolas Gauthier) 2156// stackoverflow.com/questions/912596/how-to-turn-a-string-into-a-javascript-function-call 2157// returns null if var doesn't exist in last scope, throws error if scope level missing 2158function getVarFromString(path) 2159{ 2160 var scope = window; 2161 var scopeSplit = path.split('.'); 2162 for (i = 0; i < scopeSplit.length - 1; i++) { 2163 var scope_name = scopeSplit[i]; 2164 scope = scope[scope_name]; 2165 if (isUndefined(scope)) { 2166 //console.log('getVarFromString: NO SCOPE '+ path +' scope_name='+ scope_name); 2167 throw 'no scope'; 2168 } 2169 } 2170 2171 var r = scope[scopeSplit[scopeSplit.length - 1]]; 2172 //console.log('getVarFromString: s='+ path +' r='+ r); 2173 return r; // can be null or undefined 2174} 2175 2176// And our extension for setting a var (i.e. object, prim) value. 2177// Create object at each scope level if required before final variable set. 2178function setVarFromString(string, val) 2179{ 2180 var scope = window; 2181 var scopeSplit = string.split('.'); 2182 for (i = 0; i < scopeSplit.length - 1; i++) { 2183 //console.log('setVarFromString '+ i +' '+ scopeSplit[i] +'='+ scope[scopeSplit[i]]); 2184 if (scope[scopeSplit[i]] == undefined) { 2185 scope[scopeSplit[i]] = { }; 2186 } 2187 scope = scope[scopeSplit[i]]; 2188 } 2189 2190 scope[scopeSplit[scopeSplit.length - 1]] = val; 2191} 2192 2193// from: stackoverflow.com/questions/332422/how-do-i-get-the-name-of-an-objects-type-in-javascript 2194// NB: may not work in all cases 2195function getType(o) { return o && o.constructor && o.constructor.name; } 2196 2197// see: feather.elektrum.org/book/src.html 2198function kiwi_parseQuery ( query ) { 2199 var Params = {}; 2200 if ( ! query ) return Params; // return empty object 2201 var Pairs = query.split(/[;&]/); 2202 for ( var i = 0; i < Pairs.length; i++ ) { 2203 var KeyVal = Pairs[i].split('='); 2204 if ( ! KeyVal || KeyVal.length != 2 ) continue; 2205 var key = unescape( KeyVal[0] ); 2206 var val = unescape( KeyVal[1] ); 2207 val = val.replace(/\+/g, ' '); 2208 Params[key] = val; 2209 } 2210 return Params; 2211} 2212 2213// see: stackoverflow.com/questions/118241/calculate-text-width-with-javascript 2214function getTextWidth(text, font) { 2215 // re-use canvas object for better performance 2216 var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); 2217 var context = canvas.getContext("2d"); 2218 context.font = font; 2219 var metrics = context.measureText(text); 2220 return metrics.width; 2221} 2222 2223 2224//////////////////////////////// 2225// cross-domain GET 2226// 2227// For the case where we don't control the remote server, and it isn't returning an 2228// "Access-Control-Allow-Origin: *" option (e.g. wsprnet.org). So the iframe technique must be used. 2229//////////////////////////////// 2230 2231// http://stackoverflow.com/questions/298745/how-do-i-send-a-cross-domain-post-request-via-javascript 2232 2233function kiwi_GETrequest(id, url, opt) 2234{ 2235 var debug = w3_opt(opt, 'debug', 0); 2236 2237 // Add the iframe with a unique name 2238 var iframe = document.createElement("iframe"); 2239 var uniqueString = id +'_'+ (new Date()).toUTCString().substr(17,8); 2240 document.body.appendChild(iframe); 2241 iframe.style.display = "none"; 2242 iframe.contentWindow.name = uniqueString; // link for form.target below 2243 w3_add(iframe, 'id-kiwi_GETrequest'); // for benefit of browser inspector 2244 2245 // construct a form with hidden inputs, targeting the iframe via the uniqueString 2246 var form = document.createElement("form"); 2247 form.target = uniqueString; // link to iframe.contentWindow.name above 2248 form.action = url; 2249 form.method = "GET"; 2250 w3_add(form, 'id-kiwi_GETrequest'); // for benefit of browser inspector 2251 2252 if (debug) console.log('kiwi_GETrequest: '+ uniqueString); 2253 return { iframe:iframe, form:form, debug:debug }; 2254} 2255 2256function kiwi_GETrequest_submit(request, opt) 2257{ 2258 var no_submit = w3_opt(opt, 'no_submit', 0); 2259 var gc = w3_opt(opt, 'gc', 0); 2260 2261 if (no_submit) { 2262 console.log('kiwi_GETrequest_submit: NO SUBMIT'); 2263 } else { 2264 document.body.appendChild(request.form); 2265 request.form.submit(); 2266 if (request.debug) console.log('kiwi_GETrequest_submit: SUBMITTED'); 2267 } 2268 2269 if (gc) setTimeout(function() { 2270 //console.log('kiwi_GETrequest GC'); 2271 document.body.removeChild(request.form); 2272 document.body.removeChild(request.iframe); 2273 }, 60000); 2274} 2275 2276function kiwi_GETrequest_param(request, name, value) 2277{ 2278 var input = document.createElement("input"); 2279 input.type = "hidden"; 2280 input.name = name; 2281 input.value = value; 2282 request.form.appendChild(input); 2283 2284 if (request.debug) console.log('kiwi_GETrequest_param: '+ name +'='+ value); 2285} 2286 2287 2288//////////////////////////////// 2289// AJAX 2290//////////////////////////////// 2291
2292// only works on cross-domains if server sends a CORS access wildcard 2293 2294var ajax_state = { DONE:4 }; 2295 2296function kiwi_ajax(url, callback, cb_param, timeout, debug) 2297{ 2298 kiwi_ajax_prim('GET', null, url, callback, cb_param, timeout, undefined, undefined, debug); 2299} 2300 2301function kiwi_ajax_progress(url, callback, cb_param, timeout, progress_cb, progress_cb_param, debug) 2302{ 2303 kiwi_ajax_prim('GET', null, url, callback, cb_param, timeout, progress_cb, progress_cb_param, debug); 2304} 2305 2306function kiwi_ajax_send(data, url, callback, cb_param, timeout, debug) 2307{ 2308 kiwi_ajax_prim('PUT', data, url, callback, cb_param, timeout, undefined, undefined, debug); 2309} 2310 2311var ajax_id = 0; 2312var ajax_requests = {}; 2313 2314function kiwi_ajax_prim(method, data, url, callback, cb_param, timeout, progress_cb, progress_cb_param, debug) 2315{ 2316 ajax_id++; 2317 var ajax; 2318 2319 var dbug = function(msg) { 2320 if (debug) { 2321 //kiwi_debug(msg); 2322 console.log(msg); 2323 } 2324 }; 2325 2326 //try { 2327 // ajax = new window.XDomainRequest(); 2328 //} catch(ex) { 2329 try { 2330 ajax = new XMLHttpRequest(); 2331 } catch(ex) { 2332 try { 2333 ajax = new ActiveXObject("Msxml2.XMLHTTP"); 2334 } catch(ex) { 2335 try { 2336 ajax = new ActiveXObject("Microsoft.XMLHTTP"); 2337 } catch(ex) { 2338 return false; 2339 } 2340 } 2341 } 2342 //} 2343 2344 ajax.kiwi_id = ajax_id; // ajax{} => ajax_requests[] link 2345 ajax_requests[ajax_id] = {}; 2346 2347 ajax_requests[ajax_id].didTimeout = false; 2348 ajax_requests[ajax_id].timer = -1; 2349 2350 timeout = timeout? timeout : 0; 2351 if (timeout) { 2352 ajax_requests[ajax_id].timer = setTimeout(function() { 2353 var id = ajax.kiwi_id; 2354 ajax_requests[id].didTimeout = true; 2355 ajax_requests[id].timer = -1; 2356 console.log('$AJAX TIMEOUT url='+ url); 2357 var obj = { AJAX_error:'timeout' }; 2358 w3_call(callback, obj, cb_param); 2359 ajax.abort(); 2360 delete ajax_requests[id]; 2361 }, Math.abs(timeout)); 2362 } 2363 2364 if (progress_cb) { 2365 dbug('AJAX with progress_cb='+ progress_cb); 2366 ajax.onprogress = function() { 2367 var response = ajax.responseText.toString() || ''; 2368 dbug('XHR.onprogress='+ response); 2369 w3_call(progress_cb, response, progress_cb_param); 2370 }; 2371 } 2372 2373 ajax.onerror = function(e) { 2374 dbug('XHR.onerror='+ e); 2375 if (debug) console.log(e); 2376 }; 2377 2378 ajax.onabort = function(e) { 2379 dbug('XHR.onabort='+ e); 2380 if (debug) console.log(e); 2381 }; 2382 2383 ajax.onload = function(e) { 2384 dbug('XHR.onload='+ e); 2385 if (debug) console.log(e); 2386 }; 2387 2388 ajax.onloadstart = function(e) { 2389 dbug('XHR.onloadstart='+ e); 2390 if (debug) console.log(e); 2391 }; 2392 2393 ajax.onreadystatechange = function() { 2394 dbug('XHR.onreadystatechange readyState='+ ajax.readyState); 2395 if (ajax.readyState != ajax_state.DONE) { 2396 return false; 2397 } 2398 2399 var id = ajax.kiwi_id; 2400 var timer = ajax_requests[id].timer; 2401 dbug('AJAX ORSC ENTER recovered id='+ id +' timer='+ timer +' cb='+ callback); 2402 if (timer != -1) { 2403 dbug('AJAX ORSC CLEAR_TIMER recovered id='+ id +' timer='+ timer); 2404 kiwi_clearTimeout(timer); 2405 ajax_requests[id].timer = -1; 2406 } 2407 2408 if (!ajax_requests[id].didTimeout) { 2409 2410 var obj; 2411 2412 dbug('XHR.status='+ ajax.status); 2413 dbug('XHR.statusText='+ ajax.statusText); 2414 dbug('XHR.responseType='+ ajax.responseType); 2415 dbug('XHR.response='+ ajax.response); 2416 dbug('XHR.responseText='+ ajax.responseText); 2417 dbug('XHR.responseURL='+ ajax.responseURL); 2418 if (debug) console.log(ajax); 2419 2420 if (ajax.status != 200) { 2421 console.log('$AJAX BAD STATUS='+ ajax.status +' url='+ url); 2422 obj = { AJAX_error:'status', status:ajax.status }; 2423 } else { 2424 var response = ajax.responseText.toString(); 2425 if (response == null) response = ''; 2426 2427 if (response.startsWith('<?xml')) { 2428 obj = { XML:true, text:response }; 2429 } else { 2430 var firstChar = response.charAt(0); 2431 2432 if (firstChar != '{' && firstChar != '[' && firstChar != '"' && !isdigit(firstChar)) { 2433 dbug("AJAX: response didn't begin with JSON '{' '[' '\"' or digit? "+ response); 2434 obj = { AJAX_error:'JSON prefix', response:response }; 2435 } else { 2436 try { 2437 response = kiwi_remove_cjson_comments(response); 2438 obj = JSON.parse(response); // response can be empty 2439 dbug('AJAX JSON response:'); 2440 dbug(response); 2441 dbug(obj); 2442 } catch(ex) { 2443 dbug("AJAX response JSON.parse failed: <"+ response +'>'); 2444 dbug(ex); 2445 var ex_s = ex.toString(); 2446 var line = ex_s.match(/line ([0-9]*)/); 2447 line = (isArray(line) && line.length >= 1)? +line[1] : null; 2448 obj = { AJAX_error:'JSON parse', JSON_line:line, JSON_ex:ex_s, 2449 lines:response.split('\n'), response:response }; 2450 } 2451 } 2452 } 2453 } 2454
2455 dbug('AJAX ORSC CALLBACK recovered id='+ id +' callback='+ typeof(callback)); 2456 w3_call(callback, obj, cb_param); 2457 } else { 2458 dbug('AJAX ORSC TIMED_OUT recovered id='+ id); 2459 } 2460 2461 dbug('AJAX ORSC ABORT/DELETE'); 2462 ajax.abort(); 2463 delete ajax_requests[id]; 2464 }; 2465 2466 if (timeout >= 0) { // timeout < 0 is test mode 2467 // DANGER: some URLs are relative e.g. /VER 2468 //if (!url.startsWith('http://') && !url.startsWith('https://')) 2469 // url = 'http://'+ url; 2470 ajax.open(method, url, /* async */ true); 2471 dbug('AJAX SEND id='+ ajax_id +' url='+ url); 2472 ajax.send(data); 2473 } 2474 return true; 2475} 2476 2477// remove comments from JSON consisting of line beginning with '//' in column 1 2478function kiwi_remove_cjson_comments(s) 2479{ 2480 var removed = false; 2481 var c_begin, c_end; 2482 while ((c_begin = s.indexOf('\n//')) != -1) { 2483 c_end = s.indexOf('\n', c_begin+3); 2484 s = s.slice(0, c_begin) + s.slice(c_end); 2485 removed = true; 2486 } 2487 //if (removed) console.log(s); 2488 return s; 2489} 2490 2491function kiwi_scrollbar_width() 2492{ 2493 //if (kiwi_isMacOS()) return 10; // MacOS/iOS browser scrollbars are all narrower 2494 if (kiwi_isFirefox()) return 12; 2495 if (kiwi_is_iOS()) return 10; // iOS browser scrollbars are narrower 2496 return 15; 2497} 2498 2499// https://github.com/kvz/phpjs/blob/master/functions/strings/strip_tags.js 2500function kiwi_strip_tags(input, allowed) { 2501 // discuss at: http://phpjs.org/functions/strip_tags/ 2502 // original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 2503 // improved by: Luke Godfrey 2504 // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 2505 // input by: Pul 2506 // input by: Alex 2507 // input by: Marc Palau 2508 // input by: Brett Zamir (http://brett-zamir.me) 2509 // input by: Bobby Drake 2510 // input by: Evertjan Garretsen 2511 // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 2512 // bugfixed by: Onno Marsman 2513 // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 2514 // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 2515 // bugfixed by: Eric Nagel 2516 // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 2517 // bugfixed by: Tomasz Wesolowski 2518 // revised by: RafaÅ Kukawski (http://blog.kukawski.pl/) 2519 // example 1: strip_tags('<p>Kevin</p> <br /><b>van</b> <i>Zonneveld</i>', '<i><b>'); 2520 // returns 1: 'Kevin <b>van</b> <i>Zonneveld</i>' 2521 // example 2: strip_tags('<p>Kevin <img src="someimage.png" onmouseover="someFunction()">van <i>Zonneveld</i></p>', '<p>'); 2522 // returns 2: '<p>Kevin van Zonneveld</p>' 2523 // example 3: strip_tags("<a href='http://kevin.vanzonneveld.net'>Kevin van Zonneveld</a>", "<a>"); 2524 // returns 3: "<a href='http://kevin.vanzonneveld.net'>Kevin van Zonneveld</a>" 2525 // example 4: strip_tags('1 < 5 5 > 1'); 2526 // returns 4: '1 < 5 5 > 1' 2527 // example 5: strip_tags('1 <br/> 1'); 2528 // returns 5: '1 1' 2529 // example 6: strip_tags('1 <br/> 1', '<br>'); 2530 // returns 6: '1 <br/> 1' 2531 // example 7: strip_tags('1 <br/> 1', '<br><br/>'); 2532 // returns 7: '1 <br/> 1' 2533 2534 allowed = (((allowed || '') + '') 2535 .toLowerCase() 2536 .match(/<[a-z][a-z0-9]*>/g) || []) 2537 .join(''); // making sure the allowed arg is a string containing only tags in lowercase (<a><b><c>) 2538 var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, 2539 commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi; 2540 return input.replace(commentsAndPhpTags, '') 2541 .replace(tags, function($0, $1) { 2542 return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; 2543 }); 2544} 2545 2546// from http://jsfiddle.net/gFnw9/12/ 2547function kiwi_pie(id, size, unfilled_color, filled_color) { 2548 var size2 = size*2; 2549 var s = 2550 '<svg width="'+ size2 +'" height="'+ size2 +'" viewbox="0 0 '+ size2 +' '+ size2 + '">' + 2551 '<circle cx="'+ size +'" cy="'+ size +'" r="'+ size +'" fill="'+ unfilled_color +'" />'+ 2552 '<path class="'+ id +'" style="fill:'+ filled_color +'" transform="translate('+ size +', '+ size +')" />'+ 2553 '</svg>'; 2554 return s; 2555} 2556 2557function kiwi_draw_pie(id, size, filled) { 2558 var alpha = 360 * filled; 2559 var r = alpha * Math.PI / 180, 2560 x = Math.sin(r) * size, 2561 y = Math.cos(r) * -size, 2562 mid = (alpha >= 180) ? 1:0; 2563 2564 if (alpha == 360) { mid = 1; x = -0.1;
2564 y = -size; } 2565 var animate = 'M 0 0 v '+ (-size) +' A '+ size +' '+ size +' 1 '+ mid +' 1 '+ x +' '+ y +' z'; 2566 w3_iterate_classname(id, function(el) { el.setAttribute('d', animate); }); 2567} 2568 2569function page_draw_pie(which_s) { 2570 var which = (which_s == 'admin')? admin : mfg; 2571 var id_which = function(suffix) { return ('id-'+ which_s +'-'+ suffix); }; 2572 2573 w3_el(id_which('reload-secs')).innerHTML = 'Page reload in '+ which.reload_rem + ' secs'; 2574 if (which.reload_rem > 0) { 2575 which.reload_rem--; 2576 kiwi_draw_pie(id_which('pie'), which.pie_size, (which.reload_secs - which.reload_rem) / which.reload_secs); 2577 } else { 2578 if (kiwi.reload_url) { 2579 kiwi_open_or_reload_page({ url:kiwi.reload_url }); 2580 } else { 2581 try { 2582 window.location.reload(); 2583 } catch(ex) { 2584 console.log('RELOAD FAILED?'); 2585 console.log(ex); 2586 } 2587 } 2588 } 2589} 2590 2591function wait_then_reload_page(secs, msg, which_s) 2592{ 2593 if (isUndefined(which_s)) which_s = 'admin'; 2594 var isAdmin = (which_s == 'admin'); 2595 var which = isAdmin? admin : mfg; 2596 var ael = w3_el('id-'+ which_s); 2597 var id_which = function(suffix) { return ('id-'+ which_s +'-'+ suffix); }; 2598 var reload_msg = id_which('reload-msg'); 2599 var s2; 2600 2601 if (secs) { 2602 s2 = 2603 w3_divs('w3-valign w3-margin-T-8/w3-container', 2604 w3_div('w3-show-inline-block', kiwi_pie(id_which('pie'), which.pie_size, '#eeeeee', 'deepSkyBlue')), 2605 w3_div('w3-show-inline-block', 2606 w3_div(reload_msg), 2607 w3_div(id_which('reload-secs')) 2608 ) 2609 ); 2610 } else { 2611 s2 = 2612 w3_divs('w3-valign w3-margin-T-8/w3-container', 2613 w3_div(reload_msg) 2614 ); 2615 } 2616 2617 var s = (isAdmin? '<header class="w3-container w3-teal"><h5>Admin interface</h5></header>' : '') + s2; 2618 //console.log('s='+ s); 2619 ael.innerHTML = s; 2620 2621 if (msg) w3_el(reload_msg).innerHTML = msg; 2622 2623 if (secs) { 2624 which.reload_rem = which.reload_secs = secs; 2625 setInterval(page_draw_pie, 1000, which_s); 2626 page_draw_pie(which_s); 2627 } 2628} 2629 2630function enc(s) { return s.replace(/./gi, function(c) { return String.fromCharCode(c.charCodeAt(0) ^ 3); }); } 2631 2632// encode/decodeURIComponent() to handle special characters enc() doesn't work with 2633var sendmail = function (to, subject) { 2634 var s = "mailto:"+ enc(decodeURIComponent(to)) + (isDefined(subject)? ('?subject='+subject):''); 2635 //console.log(s); 2636 var o = { url: s }; 2637 if (!(kiwi_isSafari() || kiwi_isFirefox())) o.tab = 1; 2638 kiwi_open_or_reload_page(o); 2639}; 2640 2641function line_stroke(ctx, vert, linew, color, x1,y1,x2,y2) 2642{ 2643 /* 2644 ctx.lineWidth = linew; 2645 ctx.strokeStyle = color; 2646 ctx.beginPath(); 2647 ctx.moveTo(x1, y1); 2648 ctx.lineTo(x2, y2); 2649 ctx.stroke(); 2650 */ 2651 var lineh = Math.floor(linew/2); 2652 var w = vert? linew : x2 - x1 + 1; 2653 var h = vert? y2 - y1 + 1 : linew; 2654 var x = x1 - (vert? lineh : 0); 2655 var y = y1 - (vert? 0 : lineh); 2656 ctx.fillStyle = color; 2657 ctx.fillRect(x,y,w,h); 2658} 2659 2660 2661//////////////////////////////// 2662// web sockets 2663//////////////////////////////// 2664 2665var wsockets = []; 2666 2667// used by common routines called from admin code 2668// send on any 'up' web socket assuming msg will be processed by rx_common_cmd() 2669function msg_send(s) 2670{ 2671 for (var i = 0; i < wsockets.length; i++) { 2672 var ws = wsockets[i].ws; 2673 if (!ws.up) continue; 2674 try { 2675 //console.log('msg_send <'+ s +'>'); 2676 ws.send(s); 2677 return 0; 2678 } catch(ex) { 2679 console.log("CATCH msg_send('"+s+"') ex="+ex); 2680 kiwi_trace(); 2681 return -1; 2682 } 2683 } 2684 //console.log('### msg_send: NO WS <'+ s +'>'); 2685 return -1; 2686} 2687 2688function open_websocket(stream, open_cb, open_cb_param, msg_cb, recv_cb, error_cb, close_cb, opt) 2689{ 2690 if (!("WebSocket" in window) || !("CLOSING" in WebSocket)) { 2691 console.log('WEBSOCKET TEST'); 2692 kiwi_serious_error("Your browser does not support web sockets, which is required. <br> Please use an HTML5 compatible browser."); 2693 return null; 2694 } 2695 2696 // replace http:// with ws:// on the URL that includes the port number 2697 var opt_url = w3_opt(opt, 'url'); 2698 var ws_url = isNonEmptyString(opt_url)? opt_url : kiwi_url_origin().split("://")[1]; 2699 2700 // CAUTION: follow matching change in services.cpp::proxy_frpc_setup() 2701 if (w3_opt(opt, 'proxy2') && ws_url.match(/21\d\d\d.proxy.kiwisdr.com/)) { 2702 ws_url = ws_url.replace(/proxy/, 'proxy2'); 2703 } 2704 2705 // evaluate ws protocol 2706 var ws_protocol = 'ws://'; 2707 2708 if (window.location.protocol === "https:") { 2709 ws_protocol = 'wss://'; 2710 } 2711 2712 var no_wf = kiwi_url_param('no_wf'); 2713 var tstamp = (w3_opt(opt, 'new_ts') == true)? ((new Date()).getTime()) : kiwi.conn_tstamp; 2714 ws_url = ws_protocol + ws_url +'/ws/'+ (no_wf? 'no_wf/':'kiwi/') + tstamp +'/'+ stream; 2715 var qs = w3_opt(opt, 'qs'); 2716 if (isString(qs)) { 2717 if (qs != '') ws_url += '?'+ qs; 2718 } else 2719 if (isNonEmptyString(window.location.search)) { 2720 ws_url += window.location.search;
2720 // pass query string to support "&foff=" 2721 } 2722 if (no_wf) wf.no_wf = true; 2723 2724 console.log('open_websocket '+ ws_url); 2725 var ws = new WebSocket(ws_url); 2726 wsockets.push( { 'ws':ws, 'name':stream } ); 2727 ws.up = false; 2728 ws.stream = stream; 2729 2730 ws.open_cb = open_cb; 2731 ws.open_cb_param = open_cb_param; 2732 ws.msg_cb = msg_cb; 2733 ws.all_msg_cb = w3_opt(opt, 'all_msg_cb', null); 2734 ws.recv_cb = recv_cb; 2735 ws.error_cb = error_cb; 2736 ws.close_cb = close_cb; 2737 ws.gen_cb_param = w3_opt(opt, 'gen_cb_param', null); 2738 ws.trace = w3_opt(opt, 'trace', 0); 2739 2740 // There is a delay between a "new WebSocket()" and it being able to perform a ws.send(), 2741 // so must wait for the ws.onopen() to occur before sending any init commands. 2742 ws.onopen = function() { 2743 ws.up = true; 2744 2745 if (ws.open_cb) { 2746 try { 2747 ws.open_cb(ws.open_cb_param); 2748 } catch(ex) { console.log(ex); } 2749 } 2750 }; 2751 2752 ws.onmessage = function(evt) { 2753 // We've seen a case where, if uncaught, an "undefined" error in this callback code 2754 // is never reported in the console. The callback just silently exits! 2755 // So add a try/catch to all web socket callbacks as a safety net. 2756 //try { 2757 on_ws_recv(evt, ws); 2758 //} catch(ex) { console.log(ex); } 2759 }; 2760 2761 ws.onclose = function(evt) { 2762 ws.up = false; 2763 console.log('WS-CLOSE: '+ ws.stream); 2764 2765 if (ws.close_cb) { 2766 try { 2767 ws.close_cb(ws); 2768 } catch(ex) { console.log(ex); } 2769 } 2770 }; 2771 2772 ws.binaryType = "arraybuffer"; 2773 2774 ws.onerror = function(evt) { 2775 if (ws.error_cb) { 2776 try { 2777 ws.error_cb(evt, ws); 2778 } catch(ex) { console.log(ex); } 2779 } 2780 }; 2781 2782 mdev_init(); 2783 2784 return ws; 2785} 2786 2787var kiwi_flush_recv_input = true; 2788 2789function recv_websocket(ws, recv_cb) 2790{ 2791 if (ws.stream == 'SND' || ws.stream == 'W/F') return; 2792 ws.recv_cb = recv_cb; 2793 if (recv_cb == null) 2794 kiwi_flush_recv_input = true; 2795} 2796 2797function on_ws_recv(evt, ws) 2798{ 2799 var data = evt.data; 2800 2801 if (!(data instanceof ArrayBuffer)) { 2802 console.log("on_ws_recv: not an ArrayBuffer?"); 2803 return; 2804 } 2805 2806 if (seriousError) 2807 return; // don't go any further 2808 2809 //var s = arrayBufferToString(data); 2810 //if (ws.stream == 'EXT') console.log('on_ws_recv: <'+ s +'>'); 2811 2812 var firstChars = arrayBufferToStringLen(data,3); 2813 2814 //console.log(firstChars +' '+ (new DataView(data, 0)).byteLength); 2815 2816 if (firstChars == "CLI") return; 2817 2818 var claimed = false; 2819 if (firstChars == "MSG") { 2820 var stringData = arrayBufferToString(data); 2821 params = stringData.substring(4).split(" "); // "foo=arg bar=arg" 2822 2823 //if (ws.stream == 'EXT') 2824 if (ws.trace) 2825 console.log('>>> '+ ws.stream +': MSG msg_cb='+ typeof(ws.msg_cb) +' all_msg_cb='+ typeof(ws.all_msg_cb) +' #params='+ params.length +' '+ dq(stringData)); 2826 for (var i=0; i < params.length; i++) { 2827 var msg_a = params[i].split("="); 2828 2829 if (ws.stream == 'EXT' && msg_a[0] == 'EXT-STOP-FLUSH-INPUT') { 2830 //console.log('EXT-STOP-FLUSH-INPUT'); 2831 kiwi_flush_recv_input = false; 2832 } 2833 2834 if (ws.all_msg_cb) { 2835 claimed = ws.all_msg_cb(msg_a, ws); 2836 } else { 2837 claimed = kiwi_msg(msg_a, ws); 2838 if (claimed == false) { 2839 if (ws.msg_cb) { 2840 //if (ws.stream == 'EXT') 2841 if (ws.trace) 2842 console.log('>>> '+ ws.stream + ': not kiwi_msg: msg_cb='+ typeof(ws.msg_cb) +' '+ dq(params[i])); 2843 claimed = ws.msg_cb(msg_a, ws); 2844 } 2845 } 2846 } 2847 2848 if (claimed == false) 2849 console.log('>>> '+ ws.stream + ': message not claimed: '+ params[i]); 2850 } 2851 } else { 2852 /* 2853 if (ws.stream == 'EXT' && kiwi_flush_recv_input == true) { 2854 var s = arrayBufferToString(data); 2855 console.log('>>> FLUSH '+ ws.stream + ': recv_cb='+ typeof(ws.recv_cb) +' '+ s); 2856 } 2857 */ 2858 /* 2859 if (ws.trace) { 2860 var stringData = arrayBufferToString(data); 2861 console.log('>>> '+ ws.stream + ': not MSG: firstChars='+ firstChars +' recv_cb='+ typeof(ws.msg_cb) +' '+ dq(stringData)); 2862 } 2863 */ 2864 if (ws.recv_cb && (ws.stream != 'EXT' || kiwi_flush_recv_input == false)) { 2865 ws.recv_cb(data, ws, firstChars); 2866 if (isDefined(kiwi_gc_recv) && kiwi_gc_recv) data = null; // gc 2867 } 2868 } 2869} 2870 2871// http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript 2872// FIXME: but doesn't work as expected due to problems with onbeforeunload on different browsers 2873window.onbeforeunload = function() { 2874 var i; 2875 2876 //alert('onbeforeunload '+ wsockets.length); 2877 for (i=0; i < wsockets.length; i++) { 2878 var ws = wsockets[i].ws; 2879 if (ws) { 2880 //alert('window.onbeforeunload: ws close '+ wsockets[i].name); 2881 ws.onclose = function () {}; 2882 ws.close(); 2883 } 2884 } 2885};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.