PageSourceSearch

http://kiwisdr.com/kiwi/kiwi_util.js

js kiwisdr.com collected 2026-10-01 11:48:59 UTC 82,633 bytes, 2,885 lines download raw bytes

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, '&amp;').replace(/</g, '&lt;').replace(/\>/g, '&gt;');
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.