index.js

const EPSILON = 0.0001;

/**
 *
 * @param {number} x - x coor
 * @param {number} y - y coord
 * @param {number} angle - rotation angle in radian
 * @returns {{x:number, y:number}} - x y rotated by angle
 */
const rotate2DCoord = (x, y, angle) => {
  const cos = Math.cos(angle);
  const sin = Math.sin(angle);

  return { x: x * cos - y * sin, y: y * cos + x * sin };
};

/**
 *
 * @param {Array<{x:number, y:number}>} points - points of your polygon not closed
 * @returns {number} the area of your polygon
 */
const polygon2DArea = (points) => {
  let area = 0;
  for (let i = 0; i < points.length; i += 2) {
    area +=
      points[(i + 1) % points.length].x *
        (points[(i + 2) % points.length].y - points[i % points.length].y) +
      points[(i + 1) % points.length].y *
        (points[i % points.length].x - points[(i + 2) % points.length].x);
  }
  area /= 2;
  return area;
};

/**
 * Limit the execution of a function every delay ms
 *
 * @param {Function} fn - function to be throttled
 * @param {number} delay - delay in ms
 * @returns {*} - return what the function should return every delay ms
 */
const throttle = (fn, delay) => {
  let lastCalled = 0;
  return (...args) => {
    const now = new Date().getTime();
    if (now - lastCalled < delay) {
      return;
    }
    lastCalled = now;
    return fn(...args);
  };
};

/**
 * Check if a string is a valid number
 * inspired of https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
 *
 * @param {string} str - string to check
 * @returns {boolean} true if it's a valid number
 */
function isNumeric(str) {
  if (str === 0) return true;
  if (str instanceof Object) return false;
  if (typeof str == 'boolean') return false;

  return (
    !isNaN(str) && // Use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
    !isNaN(parseFloat(str))
  ); // ...and ensure strings of whitespace fail
}

/**
 * Take an array of string and check if it is in vector3 format
 *
 * @param {Array<string>} subString - array of string
 * @returns {boolean} - true if it is vector3 format
 */
function checkIfSubStringIsVector3(subString) {
  if (subString.length != 3) {
    // Need three string
    return false;
  }

  for (let index = 0; index < subString.length; index++) {
    const element = subString[index];
    if (!isNumeric(element)) {
      // All string should be numerics
      return false;
    }
  }

  return true;
}

/**
 * Take an array of string and check if it is in euler format
 *
 * @param {Array<string>} subString - array of string
 * @returns {boolean} - true if it is euler format
 */
function checkIfSubStringIsEuler(subString) {
  if (subString.length != 4) {
    // Need four string
    return false;
  }

  // Three first string have to be numerics
  if (!isNumeric(subString[0])) return false;
  if (!isNumeric(subString[1])) return false;
  if (!isNumeric(subString[2])) return false;

  // The last one has to be an euler order
  if (
    subString[3] == 'XYZ' ||
    subString[3] == 'XZY' ||
    subString[3] == 'ZYX' ||
    subString[3] == 'ZXY' ||
    subString[3] == 'YZX' ||
    subString[3] == 'YXZ'
  ) {
    return true;
  }
  return false;
}

/**
 * Taking a string from the unpacking URI and splitting it into an array of strings.
 *
 * @param {string} uriComp - The string from the unpacking URI
 * @returns {Array<string>} - returns the array of strings if it is in vector3 format, otherwise returns null
 */
function vector3ArrayFromURIComponent(uriComp) {
  const subString = uriComp.split(',');

  if (checkIfSubStringIsVector3(subString)) {
    return subString;
  }
  return null;
}

/**
 * Taking a string from the unpacking URI and splitting it into an array of strings.
 *
 * @param {string} uriComp - The string from the unpacking URI
 * @returns {Array<string>} - returns the array of strings if it is in euler format, otherwise returns null
 */
function eulerArrayFromURIComponent(uriComp) {
  const subString = uriComp.split(',');

  if (checkIfSubStringIsEuler(subString)) {
    return subString;
  }
  return null;
}

/**
 * Take an array of string and check if it is in matrix4 format
 *
 * @param {Array<string>} subString - array of string
 * @returns {boolean} - true if it is matrix4 format
 */
function checkIfSubStringIsMatrix4(subString) {
  if (subString.length != 16) {
    // Need 16 string
    return false;
  }

  for (let index = 0; index < subString.length; index++) {
    const element = subString[index];
    if (!isNumeric(element)) {
      // All string should be numerics
      return false;
    }
  }

  return true;
}

/**
 * Taking a string from the unpacking URI and splitting it into an array of strings.
 *
 * @param {string} uriComp - The string from the unpacking URI
 * @returns {Array<string>} - returns the array of strings if it is in matrix4 format, otherwise returns null
 */
function matrix4ArrayFromURIComponent(uriComp) {
  const subString = uriComp.split(',');

  if (checkIfSubStringIsMatrix4(subString)) {
    return subString;
  }
  return null;
}

/**
 * Convert an Object into a Int32Array
 *
 * @param {object} obj - object to convert
 * @returns {Int32Array} - array converted
 */
function objectToInt32Array(obj) {
  const OString = JSON.stringify(obj);
  const SABuffer = new SharedArrayBuffer(
    Int32Array.BYTES_PER_ELEMENT * OString.length
  );
  const sArray = new Int32Array(SABuffer);

  for (let i = 0; i < OString.length; i++) {
    sArray[i] = OString.charCodeAt(i);
  }

  return sArray;
}

/**
 * Convert a Int32Array into an Object
 *
 * @param {Int32Array} array - array to convert
 * @returns {object} - object converted
 */
function int32ArrayToObject(array) {
  const str = String.fromCharCode.apply(this, array);
  return JSON.parse(str);
}

/**
 * Convert a data URI into a Buffer
 *
 * @param {string} uri - data uri to convert
 * @returns {Buffer|null} - the buffer of the data uri or null if uri is not a data uri
 */
function dataUriToBuffer(uri) {
  if (!/^data:/i.test(uri)) {
    return null; // Its not a Data URI
  }
  // Strip newlines
  uri = uri.replace(/\r?\n/g, '');
  // Split the URI up into the "metadata" and the "data" portions
  const firstComma = uri.indexOf(',');
  if (firstComma === -1 || firstComma <= 4) {
    throw new TypeError('malformed data: URI');
  }
  // Remove the "data:" scheme and parse the metadata
  const meta = uri.substring(5, firstComma).split(';');
  let charset = '';
  let base64 = false;
  const type = meta[0] || 'text/plain';
  let typeFull = type;
  for (let i = 1; i < meta.length; i++) {
    if (meta[i] === 'base64') {
      base64 = true;
    } else {
      typeFull += `;${meta[i]}`;
      if (meta[i].indexOf('charset=') === 0) {
        charset = meta[i].substring(8);
      }
    }
  }
  // Defaults to US-ASCII only if type is not provided
  if (!meta[0] && !charset.length) {
    typeFull += ';charset=US-ASCII';
    charset = 'US-ASCII';
  }
  // Get the encoded data portion and decode URI-encoded chars
  const encoding = base64 ? 'base64' : 'ascii';
  const data = unescape(uri.substring(firstComma + 1));
  const buffer = Buffer.from(data, encoding);
  // Set `.type` and `.typeFull` properties to MIME type
  buffer.type = type;
  buffer.typeFull = typeFull;
  // Set the `.charset` property
  buffer.charset = charset;
  return buffer;
}

/**
 * Removes empty fields from a FormData. Useful for update requests that
 * would update those fields to an empty string if they were sent in the
 * body. To check if a value is empty, this function just convert it into
 * a boolean.
 *
 * @param {FormData} formData The form data.
 * @returns {FormData} The same data, without the fields containing an empty value.
 */
function removeEmptyValues(formData) {
  const emptyKeys = [];
  formData.forEach((value, key) => {
    if (!value) {
      emptyKeys.push(key);
    }
  });
  emptyKeys.forEach((key) => {
    formData.delete(key);
  });
  return formData;
}

/**
 * Converts the raw content of an array buffer (as retrieved by a GET request
 * on a file) to a data URI. This is required, for example, to display images
 * fetched from the server. As we need authentication headers to retrieve some
 * protected files, we get the raw data dynamically and need to convert it to
 * a data URI do display it.
 * The basic scheme of the URI is defined in the
 * [RFC 2397](https://tools.ietf.org/html/rfc2397), with the mediaType set to
 * `mimeType` and the raw data converted to base64.
 *
 * @param {ArrayBuffer} arrayBuffer The binary data of the file.
 * @param {string} mimeType The media type. Any type supported by a data URI
 * should work. For images, use `image/png` or `image/jpeg` for instance.
 * @param {number} chunkSize The size of the chunks used to process the raw
 * data. If you get an exception saying that too many arguments were passed as
 * parameters, try reducing this value.
 * @returns {string} - data uri
 */
function imageToDataURI(arrayBuffer, mimeType, chunkSize = 8 * 1024) {
  // The response is a raw file, we need to convert it to base64
  // File (ArrayBuffer) -> Byte array -> String -> Base64 string
  const responseArray = new Uint8Array(arrayBuffer);

  // Make a string from the response array. As the array can be
  // too long (each value will be passed as an argument to
  // String.fromCharCode), we need to split it into chunks
  let responseAsString = '';
  for (let i = 0; i < responseArray.length / chunkSize; i++) {
    responseAsString += String.fromCharCode.apply(
      null,
      responseArray.slice(i * chunkSize, (i + 1) * chunkSize)
    );
  }

  const b64data = 'data:' + mimeType + ';base64,' + btoa(responseAsString);
  return b64data;
}

/**
 * Gets an attribute of an object from the given path. To get nested attributes,
 * the path qualifiers must be separated by dots ('.'). If the path is not
 * nested (does not contain any dot), the function is equivalent to `obj[path]`.
 *
 * @param {object} obj - object to get attribute
 * @param {string} path - path to get the attribute
 * @returns {*} - attribute vaue
 * @example
 * const obj = {test: {msg: "Hello world !"}};
 * console.log(getAttributeByPath(obj, "test.msg")); // prints "Hello world !";
 * console.log(getAttributeByPath(obj, "other")); // undefined
 */
function getAttributeByPath(obj, path) {
  const segs = path.split('.');
  let val = obj;
  for (const seg of segs) {
    val = val[seg];
    if (val === undefined) {
      break;
    }
  }
  return val;
}

/**
 * Check if two json object are equals
 *
 * @param {object}  json1 - first json object
 * @param {object} json2 - second json object
 * @returns {boolean} - true if both json are equals, false otherwise
 */
function objectEquals(json1, json2) {
  for (const key in json1) {
    if (json1[key] instanceof Object) {
      if (json2[key] instanceof Object) {
        if (objectEquals(json1[key], json2[key])) {
          continue;
        } else {
          return false;
        }
      } else {
        return false;
      }
    } else {
      const areEquals = isNumeric(json1[key])
        ? Math.abs(json1[key] - json2[key]) < EPSILON
        : json2[key] == json1[key];
      if (areEquals) {
        continue;
      } else {
        return false;
      }
    }
  }
  return true; // All check have passed meaning is equals
}

/**
 * Overwrite identical key of jsonOverWrited with the one matching in jsonModel
 * Create key of jsonModel which are not in jsonOverWrited
 *
 * @param {object} jsonOverWrited - json object overwrited
 * @param {object} jsonModel - json object used as model to overwrite
 * @returns {object} - json object overwrited
 */
function objectOverWrite(jsonOverWrited, jsonModel) {
  if (!jsonModel) return jsonOverWrited;

  // write the ones not in jsonOverWrited
  for (const key in jsonModel) {
    if (jsonOverWrited[key] == undefined) {
      jsonOverWrited[key] = jsonModel[key];
    }
  }

  // check in jsonOverWrited the ones existing in jsonModel
  for (const key in jsonOverWrited) {
    if (jsonOverWrited[key] instanceof Array) {
      if (jsonModel[key] instanceof Array) {
        jsonOverWrited[key] = jsonModel[key]; // array are replaced
      }
    } else if (jsonOverWrited[key] instanceof Object) {
      if (jsonModel[key] instanceof Object)
        objectOverWrite(jsonOverWrited[key], jsonModel[key]);
    } else {
      if (jsonModel[key] != undefined) {
        jsonOverWrited[key] = jsonModel[key];
      }
    }
  }

  return jsonOverWrited;
}

/**
 * Apply a callback to each key value couple of an object
 *
 * @param {object} object - object to parse
 * @param {Function} cb - callback to apply (first argument is the object containing the key and second is the key)
 * @returns {object} - object parsed
 */
function objectParse(object, cb) {
  for (const key in object) {
    if (object[key] instanceof Object) {
      objectParse(object[key], cb);
    } else {
      cb(object, key);
    }
  }
  return object;
}

/**
 * Replace all valid number string in a json object by a float
 *
 * @param {object} json - json object to parse
 * @returns {object} - json object parsed
 */
function objectParseNumeric(json) {
  return objectParse(json, function (j, key) {
    if (isNumeric(j[key])) {
      j[key] = parseFloat(j[key]);
    }
  });
}

/**
 * Check if both array are equals
 *
 * @param {Array} a1 - array 1
 * @param {Array} a2 - array 2
 * @returns {boolean} - true if equals
 */
function arrayEquals(a1, a2) {
  if (a1.length !== a2.length) return false;
  for (let i = 0; i < a1.length; i++) {
    if (a1[i] !== a2[i]) return false;
  }
  return true;
}

/**
 * Compute the last string after the . in the filename
 *
 * @param {string} filename - file name
 * @returns {string} - file format
 */
function computeFileFormat(filename) {
  const indexLastPoint = filename.lastIndexOf('.');
  return filename.slice(indexLastPoint + 1);
}

/**
 * Compute filename from path
 *
 * @param {string} path - path
 * @returns {string} filename
 */
function computeFilenameFromPath(path) {
  const indexLastSlash = path.lastIndexOf('/');
  return path.slice(indexLastSlash + 1);
}

/**
 * Check if the element is alreeady included in the array if not push it
 *
 * @param {Array} array - array where to push the element
 * @param {*} element - element to push
 * @returns {boolean} true if pushed false otherwise
 */
function arrayPushOnce(array, element) {
  if (!array.includes(element)) {
    array.push(element);
    return true;
  }
  return false;
}

/**
 * Remove an element if it's present in an array
 *
 * @param {Array} array - array to remove element from
 * @param {*} element - element to remove
 * @returns {boolean} true if removed false otherwise
 */
function removeFromArray(array, element) {
  const index = array.indexOf(element);
  if (index >= 0) {
    array.splice(index, 1);
    return true;
  }

  return false;
}

/**
 *
 * @param {string} originalString - string to modify
 * @param {number} index - where to insert
 * @param {string} string to insert
 * @returns {string} - string injected
 */
const insert = (originalString, index, string) => {
  if (index > 0) {
    return (
      originalString.substring(0, index) +
      string +
      originalString.substring(index, originalString.length)
    );
  }

  return string + originalString;
};

/**
 *
 * @param {number} number - number to round
 * @returns {string} rounded number
 */
const round = (number) => {
  const x = Math.round(number * 10) + '';
  return insert(x, x.length - 1, ',');
};

/**
 *
 * @param {{x:number,y:number,z:number}} v - vector 3 to labelize
 * @returns {string} vector labelified
 */
const vector3ToLabel = (v) => {
  return round(v.x) + ' m; ' + round(v.y) + ' m; ' + round(v.z) + ' m;';
};

module.exports = {
  EPSILON: EPSILON,
  throttle: throttle,
  vector3ToLabel: vector3ToLabel,
  round: round,
  insert: insert,
  isNumeric: isNumeric,
  arrayEquals: arrayEquals,
  checkIfSubStringIsEuler: checkIfSubStringIsEuler,
  checkIfSubStringIsVector3: checkIfSubStringIsVector3,
  checkIfSubStringIsMatrix4: checkIfSubStringIsMatrix4,
  vector3ArrayFromURIComponent: vector3ArrayFromURIComponent,
  eulerArrayFromURIComponent: eulerArrayFromURIComponent,
  matrix4ArrayFromURIComponent: matrix4ArrayFromURIComponent,
  objectToInt32Array: objectToInt32Array,
  int32ArrayToObject: int32ArrayToObject,
  dataUriToBuffer: dataUriToBuffer,
  removeEmptyValues: removeEmptyValues,
  imageToDataURI: imageToDataURI,
  getAttributeByPath: getAttributeByPath,
  objectEquals: objectEquals,
  objectOverWrite: objectOverWrite,
  objectParse: objectParse,
  objectParseNumeric: objectParseNumeric,
  computeFileFormat: computeFileFormat,
  computeFilenameFromPath: computeFilenameFromPath,
  arrayPushOnce: arrayPushOnce,
  removeFromArray: removeFromArray,
  rotate2DCoord: rotate2DCoord,
  polygon2DArea: polygon2DArea,
  ProcessInterval: require('./ProcessInterval'),
};