Source: paferalib.js

"use strict";

/**********************************************************************
 * Common utilities that don't involve use in a webpage.
 * 
 * Most functions should be self-explanatory via their names.
 **********************************************************************/

/** Retrieves the global scope
 */
var GLOBAL  = (function()
{
  return this || (1, eval)('this');
}());

/** Character constants
 */
var NUMBERS = '0123456789';
var LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
var UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
var ALPHA     = LOWERCASE + UPPERCASE;
var ALNUM     = LOWERCASE + UPPERCASE + NUMBERS;

/** For use in the timezone functions
 */
var TIMEZONES = {};
  
for (let i = -12; i < 13; i++)
{
  TIMEZONES['UTC ' + (i < 0 ? i : '+' + i)]  = i;
}  

// ====================================================================
/** Returns true if the string contains only the allowed characters.
 * 
 * @param {string} stringtotest 
 * @param {string} allowedcharacters 
 */
function ValidString(stringtotest, allowedcharacters)
{
  if (stringtotest == '' || !IsString(stringtotest))
  {
    return 0;
  }

  for (let i = 0; i < stringtotest.length; i++)
  {
    if (allowedcharacters.indexOf(stringtotest.charAt(i), 0) == -1)
    {
      return 0;
    }
  }

  return 1;
}

// ====================================================================
/** Returns true if the string contains only numbers.
 * 
 * @param {string} stringtotest 
 */
function IsNumber(stringtotest) {return ValidString(stringtotest, NUMBERS);}

// ====================================================================
/** Returns true if the string contains only lower case ASCII letters.
 * 
 * @param {string} stringtotest 
 */
function IsLower(stringtotest) {return ValidString(stringtotest, LOWERCASE);}

// ====================================================================
/** Returns true if the string contains only upper case ASCII letters.
 * 
 * @param {string} stringtotest 
 */
function IsUpper(stringtotest) {return ValidString(stringtotest, UPPERCASE);}

// ====================================================================
/** Returns true if the string contains only ASCII alphabetical characters.
 * 
 * @param {string} stringtotest 
 */
function IsAlpha(stringtotest) {return ValidString(stringtotest, ALPHA);}

// ====================================================================
/** Returns true if the string contains only ASCII alphanumeric characters.
 * 
 * @param {string} stringtotest 
 */
function IsAlphaNum(stringtotest) {return ValidString(stringtotest, ALNUM);}

// ====================================================================
/** Returns true if the string appears to be a valid email address.
 * 
 * Thanks to https://codeforgeek.com/how-to-validate-email-address-javascript
 * 
 * @param {string} stringtotest 
 */
function IsEmail(stringtotest) 
{
	var expression = /(?!.*\.{2})^([a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+(\.[a-z\d!#$%&'*+\-\/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)*|"((([ \t]*\r\n)?[ \t]+)?([\x01-\x08\x0b\x0c\x0e-\x1f\x7f\x21\x23-\x5b\x5d-\x7e\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\\[\x01-\x09\x0b\x0c\x0d-\x7f\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))*(([ \t]*\r\n)?[ \t]+)?")@(([a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\d\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.)+([a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF][a-z\d\-._~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]*[a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])\.?$/i;
	return expression.test(stringtotest.toLowerCase());
}

// ====================================================================
/** Converts v into a valid int.
 * 
 * @param {value} v - Any value to convert to an int.
 */
function ToInt(v)
{
  v   = parseInt(v);

  if (isNaN(v) || !v)
  {
    return 0;
  }

  return v;
}

// ====================================================================
/** Returns the prototype of obj, essentially the JavaScript base class.
 * 
 * @param {object} obj - An object to get the prototype of.
 */
function GetParentObject(obj)
{
  return Object.getPrototypeOf(obj);
}

// ====================================================================
/** Limits the number to the range provided.
 *
 * If value is not a number, then return defaultvalue
 * 
 * @param {number} value - The value to limit.
 * @param {number} min - The minimum allowed value.
 * @param {number} max - The maximum allowed value.
 * @param {number} defaultvalue - The value returned if the original
 *     value was not a valid number.
 */
function Bound(value, min, max, defaultvalue = 0)
{
  if (typeof value != 'number')
  {
    value  = parseFloat(value);
  }

  if (isNaN(value))
  {
    value  = defaultvalue;
  }
  
  if (value < min)
  {
    value  = min;
  }

  if (value > max)
  {
    value  = max;
  }

  return value;
}

// ====================================================================
/** Shamelessly stolen from AngularJS to encode HTML entities.
 *
 * @param {string} str - The string to encode into HTML entities.
 */
function EncodeEntities(str)
{
  if (!str)
  {
    return '';
  }

  return str.
    replace(/&/g, '&amp;').
    replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function(value) {
      let hi = value.charCodeAt(0);
      let low = value.charCodeAt(1);
      return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
    }).
    replace(/([^\#-~| |!])/g, function(value) {
      return '&#' + value.charCodeAt(0) + ';';
    }).
    replace(/</g, '&lt;').
    replace(/>/g, '&gt;');
}


// ====================================================================
/** Quick and easy regexp to strip HTML tags inside a string.
 * 
 * @param {string} str - The string to encode into HTML entities.
 */
function StripTags(str)
{
  if (!str)
  {
    return '';
  }

  return str.replace(/(<([^>]+)>)/gi, "");
}

// ====================================================================
/** Returns the extension of a given filename, which is everything
 * after the last period.
 * 
 * @param {string} str - The string to encode into HTML entities.
 */
function GetFileExtension(filename)
{
  if (!filename)
  {
    return '';
  }

  return filename.substr(filename.lastIndexOf('.') + 1);
}

// ====================================================================
/** Merge a number of objects together, with the latter object 
 * properties overwriting the earlier object properties akin to
 * cascading style sheets.
 * 
 * @param {object} A variable number of objects to merge from left to
 *     right.
 */
function Merge()
{
  let newobj  =  {};
  
  for (let obj of arguments) 
  {
    for (let k in obj)
    {
      newobj[k]  =  obj[k];
    }
  }
  
  return newobj;
}

// ====================================================================
/** Deep clones any object. Handy when you want to use a mutable data type 
 * without changing the original
 * 
 * Thanks to https://github.com/davidmarkclements/rfdc
 * 
 * @param {object} o - The object to deep clone.
 */
function Clone(o) 
{
  var refs = []
  var refsNew = []

  return cloneProto(o);

  function CloneCopyBuffer (cur) {
    if (cur instanceof Buffer) {
      return Buffer.from(cur)
    }

    return new cur.constructor(cur.buffer.slice(), cur.byteOffset, cur.length)
  }

  function cloneArray (a, fn) {
    var keys = Object.keys(a)
    var a2 = new Array(keys.length)
    for (var i = 0; i < keys.length; i++) {
      var k = keys[i]
      var cur = a[k]
      if (typeof cur !== 'object' || cur === null) {
        a2[k] = cur
      } else if (cur instanceof Date) {
        a2[k] = new Date(cur)
      } else if (ArrayBuffer.isView(cur)) {
        a2[k] = CloneCopyBuffer(cur)
      } else {
        var index = refs.indexOf(cur)
        if (index !== -1) {
          a2[k] = refsNew[index]
        } else {
          a2[k] = fn(cur)
        }
      }
    }
    return a2
  }

  function cloneProto (o) {
    if (typeof o !== 'object' || o === null) return o
    if (o instanceof Date) return new Date(o)
    if (Array.isArray(o)) return cloneArray(o, cloneProto)
    if (o instanceof Map) return new Map(cloneArray(Array.from(o), cloneProto))
    if (o instanceof Set) return new Set(cloneArray(Array.from(o), cloneProto))
    var o2 = {}
    refs.push(o)
    refsNew.push(o2)
    for (var k in o) {
      var cur = o[k]
      if (typeof cur !== 'object' || cur === null) {
        o2[k] = cur
      } else if (cur instanceof Date) {
        o2[k] = new Date(cur)
      } else if (cur instanceof Map) {
        o2[k] = new Map(cloneArray(Array.from(cur), cloneProto))
      } else if (cur instanceof Set) {
        o2[k] = new Set(cloneArray(Array.from(cur), cloneProto))
      } else if (ArrayBuffer.isView(cur)) {
        o2[k] = CloneCopyBuffer(cur)
      } else {
        var i = refs.indexOf(cur)
        if (i !== -1) {
          o2[k] = refsNew[i]
        } else {
          o2[k] = cloneProto(cur)
        }
      }
    }
    refs.pop()
    refsNew.pop()
    return o2
  }
}

// ====================================================================
/** Search for any keys containing searchterm inside obj, printing
 * the key and value to the console log.
 * 
 * @param {object} obj - The object to search within.
 * @param {string} searchterm - A string that the keyname must contain.
 */
function DebugObject(obj, searchterm)
{
  let refs  = [];
  
  return DebugObjectHelper(obj, searchterm, refs)
  
  function DebugObjectHelper(obj, searchterm, refs)
  {
    for (let k in obj) 
    {
      let v = obj[k];
      
      if (IsObject(v))
      {
        if (refs.indexOf(v) != -1)
        {
          continue;
        }
        
        refs.push(v)
        
        DebugObjectHelper(v);
      } else if (k.indexOf(search) != -1)
      {
        L(k, v);
      }
    }
  }
}

// ====================================================================
/** Copy of the python time.time() function, returns the number of 
 * seconds since the Unix epoch.
 */
function time()
{
  return Date.now() / 1000;
}
  
// ====================================================================
/** Returns a datetime string from a timestamp.
 * 
 * @param {int}  timestamp - The timestamp in seconds instead of milliseconds, so
 *     if you use JavaScript's Date.now(), divided by 1000 first.
 * @param {bool} dateonly - Returns only the date string and not a whole
 *     datetime string.
 */
function PrintTimestamp(timestamp, dateonly = 0)
{
  let d   = timestamp 
    ? new Date(timestamp * 1000)
    : new Date();
    
  let year  = d.getFullYear();

  let month  =  d.getMonth() + 1;
  month      = month < 10 ? '0' + month : month;

  let day    =  d.getDate();
  day        = day < 10 ? '0' + day : day;

  if (dateonly)
  {
    return year + '-' + month + '-' + day;
  }

  let hour  =  d.getHours();
  hour      = hour < 10 ? '0' + hour : hour;

  let minute  =  d.getMinutes();
  minute      = minute < 10 ? '0' + minute : minute;

  let second  =  d.getSeconds();
  second      = second < 10 ? '0' + second : second;

  return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
}

// ====================================================================
/** Turns a number of seconds into a hours, minutes, seconds string.
 * Handy for countdowns and timers.
 * 
 * @param {number} numseconds - The total number of seconds
 * @param {bool} nohours - Set to true to print only minutes and seconds
 * @param {bool} precision - The number of digits for fractional seconds.
 */
function SecondsToTime(numseconds, nohours = 0, precision = 0)
{
  precision  = precision || 0;
  
  if (nohours)
  {
    if (numseconds <= 0)
    {
      return '00:00';
    }
    
    let  minutes  =  Math.floor(numseconds / 60);
    minutes      = minutes < 10 ? '0' + minutes : minutes;

    let  seconds  =  numseconds % 60;
    seconds      = seconds < 10 ? '0' + seconds.toFixed(precision) : seconds.toFixed(precision);

    return minutes + ':' + seconds;
  }
  
  if (numseconds <= 0)
  {
    return '00:00:00';
  }
    
  let  hours    =  Math.floor(numseconds / 3600);
  hours        = hours < 10 ? '0' + hours : hours;

  let  minutes  =  Math.floor((numseconds % 3600) / 60);
  minutes      = minutes < 10 ? '0' + minutes : minutes;

  let  seconds  =  numseconds % 60;
  seconds      = seconds < 10 ? '0' + seconds.toFixed(precision) : seconds.toFixed(precision);

  return hours + ':' + minutes + ':' + seconds;
}

// ====================================================================
/** Converts an UTC ISO date string to local time.
 * 
 * @param {string} isostring - The date to convert in ISO 8601 format
 * 		as an date object or ISO string. If blank, then returns the 
 * 		current local time. If it's an integer, then interprets it as 
 * 		a timestamp.
 */
function UTCToLocal(isostring)
{
  if (typeof isostring == 'object')
  {
    isostring	= MakeDateReadable(isostring)
  }
  
  if (typeof isostring == 'number')
	{
		let d	= new Date(parseInt(isostring));
    isostring	= d.toISOString();
	}
  
  if (!isostring)
  {
    let d = new Date();
    d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
    return MakeDateReadable(d);
  }

  isostring  =  isostring.replace(' ', 'T');

  let d  =  new Date();
  let e  =  new Date(isostring);

  if (isNaN(e.getTime()))
	{
    throw new Error('Invalid date: ' + isostring);
	}

  e.setMinutes(e.getMinutes() - d.getTimezoneOffset());
  return MakeDateReadable(e);
}

// ====================================================================
/** Converts a local date string to UTC time.
 * 
 * @param {string} isostring - The local datetime string to convert 
 *    as a Date object or ISO string. If blank, then returns the 
 * 		current UTC time.
 */
function LocalToUTC(isostring)
{
  let d  =  new Date();

  if (typeof isostring == 'object')
  {
    isostring	= MakeDateReadable(isostring)
  }
  
  if (typeof isostring == 'number')
	{
		let d	= new Date(parseInt(isostring));
    return MakeDateReadable(d.toISOString());
	}
	
  if (!isostring)
  {
    return MakeDateReadable();
  }

  isostring  =  isostring.replace(' ', 'T');
  let e  =  new Date(isostring + 'Z');

  if (isNaN(e.getTime()))
  {
    throw new Error('Invalid date: ' + isostring);
  }

  e.setMinutes(e.getMinutes() + d.getTimezoneOffset());
  return MakeDateReadable(e);
}

// ====================================================================
/** Makes a ISO timestring more readable by getting rid of the 
 * characters after seconds and the T between the date and time.
 * 
 * @param {string} timestring - An ISO string, Date object, or 
 *    integer timestamp. If blank, returns the current local time.
 */
function MakeDateReadable(timestring)
{
	if (!timestring)
	{
		timestring	= new Date().toISOString();
	}
	
	if (typeof timestring == 'number')
  {
    timestring	= new Date(timestring).toISOString();
  }
	
  if (!IsString(timestring))
  {
    timestring  = timestring.toISOString();
  }
  
  return timestring.substr(0, 19).replace('T', ' ');
}

// ====================================================================
/** Returns a list of the key names for an object
 * 
 * @param {object} obj - The object to list keys for.
 */
function Keys(obj)
{
  let keys = [];

  for (let k in obj)
  {
    if (obj.hasOwnProperty(k))
    {
      keys.push(k);
    }
  }

  return keys;
}

// ====================================================================
/** Returns a list of the values for an object excluding functions.
 * 
 * @param {object} obj - The object to list values for.
 */
function Values(obj)
{
  let values = [];

  for (let p in obj)
  {
    switch (typeof obj[p])
    {
      case 'function':
        continue;
    };

    values.push(obj[p]);
  }

  return values;
}

// ====================================================================
/** Returns true if the object has a key with the given name.
 * 
 * @param {object} obj - The object to search within
 * @param {string} keyname - The key to search for
 */
function HasKey(obj, keyname)
{
  return Keys(obj).indexOf(keyname) != -1;
}

// ====================================================================
/** Returns the first property inside an object.
 * 
 * @param {object} obj - The object to look in
 */
function First(obj)
{
  for (let k in obj)
  {
    if (obj.hasOwnProperty(k))
    {
      return obj[k];
    }
  }

  return '';
}

// ====================================================================
/** Returns the last element of an array or 0 otherwise.
 * 
 * @param {list} a - The array to examine.
 */
function Last(a)
{
  if (a.length)
  {
    return a[a.length - 1];
  }
  
  return 0;
}

// ====================================================================
/** Returns true if value is a function.
 * 
 * @param {value} value - The value to examine.
 */
function IsFunc(value)
{
  return typeof value === 'function';
}

// ====================================================================
/** Returns true if value is an async function.
 * 
 * @param {value} value - The value to examine.
 */
function IsAsyncFunc(value)
{
  return typeof value === 'function' && value.constructor.name == 'AsyncFunction';
}

// ====================================================================
/** Returns true if value is undefined.
 * 
 * @param {value} value - The value to examine.
 */
function IsUndef(value)
{
  return typeof value === 'undefined';
}

// ====================================================================
/** Returns true if the value is boolean false, undefined, or is an 
 * empty array or object.
 * 
 * @param {value} value - The value to examine.
 */
function IsEmpty(v)
{
  if (!v || typeof v === 'undefined')
    return true;

  if (Array.isArray(v))
  {
    if (v.length == 0)
      return true;
  } else
  {
    for (let k in v)
    {
      if (v.hasOwnProperty(k))
        return false;
    }
    return true;
  }

  return false;
}


// ====================================================================
/** Returns true if a and b are equal, comparing even lists and objects
 * 
 * @param {value} a
 * @param {value} b
 */
function IsEqual(a, b)
{
  if ((!a && !b)
    || (typeof a === 'undefined' && typeof b === 'undefined')
    || (IsEmpty(a) && IsEmpty(b))
  )
  {
    return true;
  }

  if (Array.isArray(a) && Array.isArray(b))
  {
    if (a.length == b.length)
    {
      for (let i = 0, l = a.length; i < l; i++)
      {
        if (a[i] != b[i])
        {
          return false;
        }
      }
      
      return true;
    } else
    {
      return false;
    }
  } else if (typeof a === 'object' && typeof b === 'object')
  {
    let akeys = Keys(a);
    let bkeys = Keys(b);
    
    if (!IsEqual(akeys, bkeys))
    {
      return false;
    }
    
    for (let i = 0, l = a.length; i < l; i++)
    {
      if (!IsEqual(a[akeys[i]], b[akeys[i]]))
      {
        return false;
      }
    }
    
    return true;
  }

  return a == b;
}

// ====================================================================
/** Converts a dot notation such as animals.felines.cats into
 * animals['felines']  = 'cats'
 * 
 * @param {object} baseobj - The object to store the values in
 * @param {string} dottedname - The 
 */
function SaveNestedObject(baseobj, dottedname)
{
  let parts  =  dottedname.split('.');

  for (let i = 0, l = parts.length; i < l; i++)
  {
    baseobj   = baseobj[parts[i]];
  }

  return baseobj;
}

// =====================================================================
/** Sorts a JavaScript object based upon its keys and returns a sorted
 * array of [value, key, obj] pairs. level1 and level2 allow sorting based
 * upon a key such as obj['animals']['birds']['parrots']
 * 
 * @param {object}  obj - The object to sort
 * @param {bool} ignorecase - Set to true to enable case insensitive sorting
 * @param {string} level1 - The first key to sort, or blank to do top-level sorting.
 * @param {string} level2 - The first key to sort, or blank to do level 1 sorting.
 */
function SortArray(obj, ignorecase, level1 = 0, level2 = 0)
{
  let sorted  =  [];
  var  key      =  null;

  for (let k in obj)
  {
    if (typeof obj[k] == "undefined")
      continue;

    if (level2)
    {
      if (typeof obj[k][level1] == 'undefined')
        continue;

      key  =  obj[k][level1][level2];
    } else if (level1)
    {
      key  =  obj[k][level1];
    } else
    {
      key  =  obj[k];
    }
  
    if (key == undefined)
    {
      continue;
    }
  
    if (key && ignorecase)
    {
      key  =  key.toString().toLowerCase();
    }

    sorted.push([k, key, obj[k]]);
  }

  sorted.sort(
    function(a, b)
    {
      if (a[1] == b[1])
      {
        return 0;
      }

      return (a[1] > b[1]) ? 1 : -1;
    }
  );

  return sorted;
}

// ====================================================================
/** Copies all keys and values from newvalues to obj.
 *
 * Useful for storing multiple CSS styles in a DOM element.
 *
 * @param {object} objfrom
 * @param {object} objto
 */
function CopyValues(copyfrom, copyto)
{
  for (let k in copyfrom)
  {
    if (!k)
    {
      continue;
    }
    
    copyto[k]   = copyfrom[k];
  }
}

// ====================================================================
/** Simple regex to get the filename portion of a full path.
 * 
 *  Thanks to http://planetozh.com/blog/2008/04/javascript-basename-and-dirname/
 *
 * @param {string} path - The full path of the file.
 */ 
function Basename(path)
{
  return path.replace(/\\/g,'/').replace( /.*\//, '' );
}

// ====================================================================
/** Simple regex to get the directory name portion of a full path.
 * 
 *  Thanks to http://planetozh.com/blog/2008/04/javascript-basename-and-dirname/
 *
 * @param {string} path - The full path of the file.
 */ 
function Dirname(path)
{
  return path.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');;
}

// ====================================================================
/** Returns true if the string starts with the prefix.
 * 
 * @param {string} prefix - The prefix to test with.
 */
String.prototype.startswith = function(prefix) 
{
  return this.indexOf(prefix) == 0;
};

// ====================================================================
/** Returns true if the string ends with the suffix
 * 
 * @param {string} suffix - The suffix to test with.
 */
String.prototype.endswith = function(suffix) 
{
  return this.indexOf(suffix, this.length - suffix.length) !== -1;
};

// ====================================================================
/** Returns a random character from this string.
 */
String.prototype.random = function() 
{
  if (!this.length)
  {
    return '';
  }

  return this.charAt(RandInt(0, this.length));
};

// ====================================================================
/** Replaces all instances of search with replace inside of this string.
 *
 * Thanks to http://dumpsite.com/forum/?topic=4.msg29#msg29
 *
 * @param {string} search - The original string
 * @param {string} replace - The replacement string
 * @param {bool} ignorecase - Set to true to use case insensitive replacing
 */
String.prototype.replaceAll = function(search, replace, ignorecase = 0)
{
  return this.replace(new RegExp(search.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g,"\\$&"),(ignorecase?"gi":"g")),(typeof(replace)=="string")?replace.replace(/\$/g,"$$$$"):replace);
};

// ====================================================================
/** Capitalizes every beginning letter in the string. Note that this
 * is a simple function and doesn't skip smaller words like the, in, and
 * so forth, so it's not quite the same as title case.
 * 
 * Thanks to http://stackoverflow.com/questions/2332811/capitalize-words-in-string/7592235#7592235
 */
String.prototype.capitalize = function() 
{
  return this.replace(
    /(?:^|\s)\S/g, 
    function(a) 
    { 
      return a.toUpperCase(); 
      
    }
  );
};

// ====================================================================
/** Returns true if the string contains the given value.
 * 
 * @param {value} value - The value to test for.
 */
String.prototype.has = function(value)
{
  return this.indexOf(value) != -1;
};

// ====================================================================
/** Removes any element from this array that has the same value as the 
 * given value.
 * 
 * Thanks to http://stackoverflow.com/questions/3954438/remove-item-from-array-by-value
 * 
 * @param {value} value - The value to test for equivalence
 */
Array.prototype.remove = function()
{
  let what, a = arguments, L = a.length, ax;
  
  while (L && this.length)
  {
    what = a[--L];
    
    while ((ax = this.indexOf(what)) !== -1)
    {
      this.splice(ax, 1);
    }
  }
  return this;
};

// ====================================================================
/** Returns true if the array contains the given value.
 * 
 * @param {value} value - The value to test for.
 */
Array.prototype.has = function(value)
{
  return this.indexOf(value) != -1;
};

// ====================================================================
/** A copy of the python extend function, this appends the given array
 * to the current aray.
 * 
 * @param {array} a - The array to append
 */
Array.prototype.extend = function (a) 
{
  a.forEach(function(v) {this.push(v)}, this);    
}

// ====================================================================
/** Moves an array element from one position to another. Great for
 * rearranging tables or lists.
 * 
 * Thanks to http://stackoverflow.com/questions/5306680/move-an-array-element-from-one-array-position-to-another
 * 
 * @param {int} frompos - The position to move from
 * @param {int} topos - The position to move to
 */
Array.prototype.move = function(frompos, topos) 
{
  while (frompos < 0) 
  {
    frompos += this.length;
  }
  
  while (topos < 0) 
  {
    topos += this.length;
  }
  
  if (topos >= this.length) 
  {
    let k = topos - this.length;
    
    while ((k--) + 1) 
    {
        this.push(undefined);
    }
  }
  
  this.splice(topos, 0, this.splice(frompos, 1)[0]);
  
  return this; // for testing purposes
};

// ====================================================================
/** Randomly shuffles an array's elements. Be sure to clone a copy if 
 * you still want to use the original array.
 * 
 * @param {array} a - The array to shuffle.
 */
function Shuffle(a)
{
  let i = a.length, j, tempi, tempj;
  
  if (i == 0) return false;
  
  while (--i) 
  {
    j       = Math.floor(Math.random() * (i + 1));
    tempi   = a[i];
    tempj   = a[j];
    a[i]     = tempj;
    a[j]     = tempi;
  }
  return a;
}

// ====================================================================
/** A copy of the C strcmp() function.
 * 
 * @param {string} a
 * @param {string} b
 * @param {bool} ignorecase
 */
function StrCmp(a, b, ignorecase = 0)
{
  if (ignorecase)
  {
    a  =  a.toUpperCase();
    b  =  b.toUpperCase();
  }
  
  if (a > b)
  {
    return 1;
  }

  if (b > a)
  {
    return -1;
  }

  return 0;
}

// ====================================================================
/** A copy of the C strcmpi() function.
 * 
 * @param {string} a
 * @param {string} b
 */
function StrCmpI(a, b)
{
  return StrCmp(a, b, 1);
}


// ====================================================================
/** Returns a random integer from min to max.
 * 
 * @param {int} min - The minimum value
 * @param {int} max - The maximum value
 */
function RandInt(min, max)
{
  return Math.round(Math.random() * (max - min)) + min;
}

// ====================================================================
/** Returns a random element of an array.
 * 
 * @param {array} a
 */
function RandElement(a)
{
  return a[RandInt(0, a.length - 1)];
}

// ====================================================================
/** A copy of the python range() function, this returns an array of 
 * integers from min to max differing by step.
 * 
 * @param {int} min
 * @param {int} max
 * @param {int} step
 */
function Range(min, max, step)
{
  let seq  =  Array();

  for (let i = min; i < max; i += step)
  {
    seq.push(i);
  }

  return seq;
}

// ====================================================================
/** Returns true if v is a string.
 * 
 * @param {value} v
 */
function IsString(v)
{
  return (typeof v == 'string' || v instanceof String);
}

// ====================================================================
/** Returns true if v is a number.
 * 
 * @param {value} v
 */
function IsNum(v)
{
  return typeof v == 'number';
}

// ====================================================================
/** Returns true if v is an array.
 * 
 * @param {value} v
 */
function IsArray(v)
{
  return Array.isArray(v);
};

// ====================================================================
/** Returns true if v is an object.
 * 
 * @param {value} v
 */
function IsObject(obj)
{
  return obj === Object(obj);
}

// ====================================================================
/** Converts a dict into query string params. Useful if you need to 
 * construct a query string.
 * 
 * @param {object} data - The dict to convert
 */
function ToParams(data)
{
  let params = [];

  for (let p in data)
  {
    if (data.hasOwnProperty(p))
    {
      let k = p, v = data[p];

      params.push(typeof v == "object"
        ? ToParams(v, k) :
        encodeURIComponent(k) + "=" + encodeURIComponent(v));
    }
  }
  return params.join("&");
}

// ====================================================================
/** Generic function to see if the point (x, y) is inside the rect
 * (top, right, bottom, left)
 * 
 * @param {number} x
 * @param {number} y
 * @param {rect} rect
 */
function InRect(x, y, rect)
{
  if (!rect)
    return 0;

  return (rect.left <= x
    && x <= rect.right
    && rect.top <= y
    && y <= rect.bottom);
}

// ====================================================================
/** Convenience function to set a cookie in JavaScript. Pafera itself
 * uses this to set a user's time offset for converting between
 * GMT and local time on the server side. Only name and value are
 * required.
 * 
 * @param {string} name - The identifier for the cookie
 * @param {value} value - The cookie's contents
 * @param {int} numdays - How many days the cookie is valid for
 * @param {string} path - The valid path for the cookie
 * @param {string} domain - The valid domain for the cookie
 * @param {string} secure - The security for the cookie
 */
function SetCookie(name, value, numdays, path, domain, secure)
{
  path  =  path || '/';

  let now = new Date();

  if (numdays)
  {
    numdays = numdays * 1000 * 60 * 60 * 24;
  }

  let expirationdate = new Date(now.getTime() + numdays);

  document.cookie = name + '=' + escape(value)
    + (numdays ? ';expires=' + expirationdate.toGMTString() : '' )
    + ';path=' + path
    + (domain ? ';domain=' + domain : '')
    + (secure ? ';secure' : '')
    + '; SameSite=Strict';
}

// ====================================================================
/** Taken from mysql-connector for the web dbapi where we know that
 * only administrators will use this function.
 * 
 * @params {string} s - The SQL query fragment to escape
 */
function EscapeSQL(s)
{
  let ls  = [];
  
  for (let i = 0, l = s.length; i < l; i++) 
  {
    switch (s[i]) 
    {
      case '\\n':
        ls.push('\\\\n')
        break;
      case '\\r':
        ls.push('\\\\r')
        break;
      case '\\':
        ls.push('\\\\')
        break;
      case "'":
        ls.push("\\'")
        break;
      case '"': 
        ls.push('\\"')
        break;
      case '\u00a5':
      case '\u20a9':
        // escape characters interpreted as backslash by mysql
        // fall through
      default:
        ls.push(s[i])
    }
  }

  return ls.join('');
}

// ====================================================================
/** An implementation of other languages' sleep functions through
 * JavaScript promises.
 * 
 * @param {int} ms - The number of milliseconds to delay execution.
 */
function Sleep(ms) 
{
  return new Promise(resolve => setTimeout(resolve, ms));
}

// ====================================================================
// Fake function for JSDoc below
//
/** Defines a uuid() function that can be used to return unique IDs.
 *
 * Thanks to https://github.com/jchook/uuid-random/blob/master/uuid-random.min.js
 */
function uuid()
{  
}

!function(){function g(a,b){return Math.floor(Math.random()*(b-a))+a}function h(c){let f;if("undefined"!=typeof e){if("undefined"==typeof a||b+c>j.BUFFER_SIZE)if(b=0,e.getRandomValues)a=new Uint8Array(j.BUFFER_SIZE),e.getRandomValues(bytes);else{if(!e.randomBytes)throw new Error("Non-standard crypto library");a=e.randomBytes(j.BUFFER_SIZE)}return a.slice(b,b+=c)}for(f=[],d=0;d<c;d++)f.push(g(0,16));return f}function i(){let a=h(16);return a[6]=15&a[6]|64,a[8]=63&a[8]|128,a}function j(){let a=i();return c[a[0]]+c[a[1]]+c[a[2]]+c[a[3]]+"-"+c[a[4]]+c[a[5]]+"-"+c[a[6]]+c[a[7]]+"-"+c[a[8]]+c[a[9]]+"-"+c[a[10]]+c[a[11]]+c[a[12]]+c[a[13]]+c[a[14]]+c[a[15]]}let a,d,b=0,c=[];for(j.BUFFER_SIZE=512,j.bin=i,d=0;d<256;d++)c[d]=(d+256).toString(16).substr(1);if("undefined"!=typeof module&&"function"==typeof require){let e=require("crypto");module.exports=j}else"undefined"!=typeof window&&(window.uuid=j)}();


// ====================================================================
// Fake function for JSDoc below
//
/** Defines a SVGInject() function that replaces an img tag with inline
 * SVG, allowing CSS and JavaScript to be used on the SVG
 * 
 * Use like
 * &lt;img src="image.svg" onload="SVGInject(this)"&gt;
 *
 * Thanks to https://github.com/iconfu/svg-inject/
 * 
 * @params {DOMElement} imgtag - The image tag to replace with SVG
 */
function SVGInject(imgtag)
{  
}

/* MIT License - blob/master/LICENSE */
!function(r,l){var p,h=null,g="length",n="SVG_NOT_SUPPORTED",y="LOAD_FAIL",b="SVG_INVALID",i="createElement",S="__svgInject",s=["src","alt","onload","onerror"],A=l[i]("a"),o=l[i]("div"),k="undefined"==typeof SVGRect,a={cache:!0,copyAttributes:!0,makeIdsUnique:!0},w={clipPath:["clip-path"],"color-profile":h,cursor:h,filter:h,linearGradient:["fill","stroke"],marker:["marker","marker-end","marker-mid","marker-start"],mask:h,pattern:["fill","stroke"],radialGradient:["fill","stroke"]},I=1,f=2,c=3;function j(e,t,r,n,i){if(t=t||L(r,n)){var o=e.parentNode;if(o){i.copyAttributes&&function u(e,t){for(var r=e.attributes,n=0;n<r[g];++n){var i=r[n],o=i.name;if(-1==s.indexOf(o)){var a=i.value;if("title"==o){var f=l.createElementNS("http://www.w3.org/2000/svg","title");f.textContent=a;var c=t.firstElementChild;c&&"title"==c.tagName.toLowerCase()?t.replaceChild(f,c):t.insertBefore(f,c)}else t.setAttribute(o,a)}}}(e,t),i.makeIdsUnique&&function b(e){var t,r,n,i,o,a,f,c,u,l=e.querySelectorAll("defs>[id]"),s={};for(f=0;f<l[g];f++)if((r=(t=l[f]).tagName)in w)for(i=(n=t.id)+"-"+Math.random().toString(36).substr(2,10),t.id=i,o=w[r]||[r],c=0;c<o[g];c++)(s[a=o[c]]||(s[a]=[])).push([n,i]);var d=Object.keys(s);if(d[g]){var v,m,p,h,y=e.querySelectorAll("*");for(f=0;f<y[g];f++)for(v=y[f],c=0;c<d[g];c++)if(m=d[c],p=v.getAttribute(m))for(h=s[m],u=0;u<h[g];u++)if(p.replace(/"/g,"")=="url(#"+h[u][0]+")"){v.setAttribute(m,"url(#"+h[u][1]+")");break}}}(t);var a=i.beforeInject&&i.beforeInject(e,t)||t;o.replaceChild(a,e),e[S]=f,T(e),i.afterInject&&i.afterInject(e,a)}}else x(e,i)}function E(){for(var e={},t=arguments,r=0;r<t[g];++r){var n=t[r];if(n)for(var i in n)n.hasOwnProperty(i)&&(e[i]=n[i])}return e}function L(e,t){try{o.innerHTML=e}catch(n){return h}var r=o.firstElementChild;if(o.innerHTML="",function i(e){return e instanceof SVGElement}(r))return r.setAttribute("data-inject-url",t),r}function T(e){e.removeAttribute("onload")}function u(e,t,r){e[S]=c,r.onFail&&r.onFail(e,t)}function x(e,t){T(e),u(e,b,t)}function C(e,t){T(e),u(e,n,t)}function G(e,t){u(e,y,t)}function N(e){e.onload=h,e.onerror=h}function O(){throw new Error("img not set")}var e=function M(e,t){var d=E(a,t),v={};function m(o,a){if(o){var e=o[g],t=o.src;if(t&&!o[S]){if(o[S]=I,a=E(d,a),k)return void C(o,a);var f=function l(e){return A.href=e,A.href}(t),n=a.cache,c=function(e){if(n){for(var t=v[f],r=0;r<t[g];++r)t[r](e);v[f]=e}};if(N(o),n){var r=v[f],i=function(e){e===y?G(o,a):e===b?x(o,a):j(o,h,e,f,a)};if(r!==undefined)return void(Array.isArray(r)?r.push(i):i(r));v[f]=[]}!function s(e,t,r){if(e){var n=new XMLHttpRequest;n.onreadystatechange=function(){if(4==n.readyState){var e=n.status;200==e?t(n.responseXML,n.responseText.trim()):400<=e?r():0==e&&r()}},n.open("GET",e,!0),n.send()}}(f,function(e,t){if(o[S]==I){var r=e instanceof Document?e.documentElement:L(t,f);if(r){var n=a.afterLoad;n&&(n(r),t=function i(){return p=p||new XMLSerializer}().serializeToString(r)),j(o,r,t,f,a),c(t)}else x(o,a),c(b)}},function(){G(o,a),c(y)})}else if(e)for(var u=0;u<e;++u)m(o[u],a)}else O()}return function n(e){var t=l.getElementsByTagName("head")[0];if(t){var r=l[i]("style");r.type="text/css",r.styleSheet?r.styleSheet.cssText=e:r.appendChild(l.createTextNode(e)),t.appendChild(r)}}('img[onload^="'+e+'("]{visibility:hidden;}'),m.setOptions=function(e){d=E(d,e)},m.create=M,m.err=function(e,t){e?e[S]!=c&&(N(e),k?C(e,d):(T(e),G(e,d)),t&&(T(e),e.src=t)):O()},r[e]=m}("SVGInject");"object"==typeof module&&"object"==typeof module.exports&&(module.exports=e)}(window,document);

// ====================================================================
/** Convert a 32-bit integer into a six character alphanumeric code.
 * 
 * This is used extensively in Pafera to both save space in URLs and to 
 * make IDs more human readable. 
 * 
 * Since JavaScript represents all numbers internally as 64-bit doubles,
 * if you need a 64-bit integer, use two short codes together.
 * 
 * @param {int} val - The integer to convert
 * @param {string} chars - The character set to use. Make sure that
 *     every application that you write uses the same character set, or 
 *     short codes will not be portable.
 */
function ToShortCode(
  val, 
  chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
)
{
  val  = val + 2147483648
  
  let base  = chars.length;
  let code  = '';
  let mod   = 0;
  
  for (;;)
  {
    mod = parseInt(val % base);
    code = chars[mod] + code;
    val = (val - mod) / base;
    
    if (val <= 0)
    {
      break
    }
  }
  
  while (code.length < 6)
  {
    code  = '0' + code;
  }
  
  return code
}

// =====================================================================
/** The reverse of ToShortCode(), turning a six character alphanumeric
 * code into a 32-bit integer
 * 
 * @param {string} code - The code to convert
 * @param {string} chars - The character set to use. Make sure that
 *     every application that you write uses the same character set, or 
 *     short codes will not be portable.
 */
function FromShortCode(
  code, 
  chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
)
{
  let base    = chars.length;
  
  //code  = code.rstrip('0')
  
  let l    = code.length
  let val  = 0;

  chars     = chars.split('');
  let arr   = {};

  for (let i = 0; i < base; i++)
  {
    arr[chars[i]] = i
  }

  for (let i = 0; i < l; i++)
  {
    val = val + (arr[code[i]] * (base ** (l - i - 1)))
  }

  val  = val - 2147483648

  return val
}

// =====================================================================
/** For simple highlighting of values, returns the background color 
 * for a value based upon given thresholds.
 * 
 * The colors run from purple, blue, green, orange, to red, so 
 * thresholds needs to be an array of four values from highest to lowest.
 * 
 * @param {number} value - The value to test
 * @param {array} thresholds - A list of four values from highest to lowest.
 */
function ColorCode(value, thresholds)
{
  let color  = 'dredg';
  
  if (value >= thresholds[0])
  {
    color  = 'dpurpleg';
  } else if (value >= thresholds[1])
  {
    color  = 'dblueg';
  } else if (value >= thresholds[2])
  {
    color  = 'dgreeng';
  } else if (value >= thresholds[3])
  {
    color  = 'dorangeg';
  }
  
  return color;
}

// ********************************************************************
// DOM Convenience functions

// ====================================================================
/** Returns an element by ID.
 * 
 * @param {string} id - CSS ID selector
 */
var I  = function(id)
{
  return document.getElementById(id);
}

// ====================================================================
/** Returns all elements containing this selector.
 * 
 * @param {string} selector - CSS selector
 */
var Q  = function(selector)
{
  if (IsString(selector))
  {
    if (selector[0] == '#')
    {
      return [ I(selector.substr(1)) ];
    }

    return document.querySelectorAll(selector);
  }

  if (IsArray(selector) || selector instanceof NodeList)
  {
    return selector;
  }

  if (selector)
  {
    return [selector];
  }

  return [];
}

// ====================================================================
/** Returns the first element containing this selector.
 * 
 * @param {string} selector - CSS selector
 */
var E  = function(selector)
{
  return IsString(selector) 
    ? document.querySelector(selector)
    : selector;
}

// ====================================================================
/** Convenient alias for console.log
 * 
 * @param {args} args - Anything that console.log will accept.
 */
var L  = console.log;