From 8a410e5c8b1d4642c427354acd95f97c865e9406 Mon Sep 17 00:00:00 2001
From: Nico Vermaas <vermaas@astron.nl>
Date: Thu, 10 Dec 2020 19:20:12 +0100
Subject: [PATCH] adding initial basic SAMP functionality

---
 public/assets/js/votable.js               | 1812 +++++++++++++++++++++
 public/index.html                         |    3 +
 src/components/query/samp/ReactVOTable.js |   94 ++
 src/components/query/samp/SampGrid.js     |   43 +
 src/components/query/samp/SampPage.js     |  112 ++
 src/contexts/GlobalContext.js             |    2 +-
 src/routes/Routes.js                      |    3 +
 7 files changed, 2068 insertions(+), 1 deletion(-)
 create mode 100644 public/assets/js/votable.js
 create mode 100644 src/components/query/samp/ReactVOTable.js
 create mode 100644 src/components/query/samp/SampGrid.js
 create mode 100644 src/components/query/samp/SampPage.js

diff --git a/public/assets/js/votable.js b/public/assets/js/votable.js
new file mode 100644
index 0000000..d2e225a
--- /dev/null
+++ b/public/assets/js/votable.js
@@ -0,0 +1,1812 @@
+/**
+ * Copyright 2014 - UDS/CNRS
+ * The votables.js and its additional files (examples, etc.) are distributed
+ * under the terms of the GNU General Public License version 3.
+ *
+ * This file is part of votables.js package.
+ *
+ * votables.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * votables.js is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * The GNU General Public License is available in COPYING file
+ * along with votables.js
+ *
+ * votables.js - Javascript library for the VOTable format (IVOA
+ * standard) parsing
+ *
+ * @author Thomas Rolling
+ * @author Jérôme Desroziers
+ */
+function VOTableParser() {
+  'use strict';
+
+  var thisParser = this,
+      xmlData = {},
+      prefix = '',
+      vot = {
+        name: '',
+        loadingState: 'null',
+        nbTables: 0,
+        nbResources: 0
+      },
+      selected = {
+        resource: {
+          i: 0,
+          xml: null,
+          tables: null
+        },
+        table: {
+          i: 0,
+          xml: null
+        }
+      },
+      votableMetadata = '',
+      currentResourceMetadata = '',
+      currentTableMetadata = '',
+      currentTableFields = '',
+      tablesData = '',
+      callbackFunction,
+      errCallbackFunction,
+      debugMode = false,
+      // Following parameters are used for B64 parsing
+      dataB64 = '',
+      bufferBitArray = '',
+      ptrStream = 0,
+      endParsingB64 = false;
+  
+  this.xhr = null; //stock the request so we can kill it
+
+  //--------------------------------------------------------------------------------
+  // Functions related to the loading of VOTable files
+
+  /**
+   * Set callback functions that will be executed after the file's loading.
+   *
+   * @param {function} callback
+   * @param {function} errCallback
+   */
+  this.setCallbackFunctions = function (callback, errCallback) {
+    callbackFunction = callback;
+    errCallbackFunction = errCallback;
+  };
+
+  /**
+   * Load XML file.
+   *
+   * @param {string} url - can be either an url or a local path
+   */
+  this.loadFile = function (url) {
+    var processError = function() {
+      debug('Unable to load VOTable file. Check the path of VOTable file');
+      vot.loadingState = 'fail';
+      if (errCallbackFunction !== undefined) {
+        errCallbackFunction(thisParser);
+      }
+    };
+
+    thisParser.cleanMemory();
+    var start = new Date().getTime(),
+        data;
+    thisParser.xhr = new XMLHttpRequest();
+
+    thisParser.xhr.open('GET', url, false);
+    thisParser.xhr.onreadystatechange = function () {
+      if (thisParser.xhr.readyState === XMLHttpRequest.DONE && thisParser.xhr.status==200) {
+        data = thisParser.xhr.responseText;
+
+        thisParser.loadBufferedFile(data, false, url);
+        //initialize(data, url);
+
+        thisParser.loadingTime = new Date().getTime() - start;
+        debug('loading time : ' + thisParser.loadingTime + ' ms.');
+      }
+      else {
+        processError();
+      }
+    };
+
+    try {
+      this.xhr.send();
+    } catch (e) {
+      processError();
+    }
+  };
+  
+  /**
+   * Load XML file.
+   *
+   * @param {string} url - can be either an url or a local path
+   */
+  this.loadFileAsynchronous = function (url) {
+    var processError = function() {
+      debug('Unable to load VOTable file. Check the path of VOTable file');
+      vot.loadingState = 'fail';
+      if (errCallbackFunction !== undefined) {
+        errCallbackFunction(thisParser);
+      }
+    };
+
+    thisParser.cleanMemory();
+    var start = new Date().getTime(),
+        data;
+    thisParser.xhr = new XMLHttpRequest();
+
+    thisParser.xhr.open('GET', url, true);
+    thisParser.xhr.onload = function (e) {
+        if (thisParser.xhr.readyState === 4) {
+          if (thisParser.xhr.status === 200) {
+              data = thisParser.xhr.responseText;
+
+              thisParser.loadBufferedFile(data, false, url);
+
+              thisParser.loadingTime = new Date().getTime() - start;
+              debug('loading time : ' + thisParser.loadingTime + ' ms.');
+          } else {
+              processError();
+          }
+        }
+    };
+    thisParser.xhr.onerror = function (e) {
+        processError();
+    };
+
+    try {
+      thisParser.xhr.send();
+    } catch (e) {
+      processError();
+    }
+  };
+  
+  /****abort the query****/
+  this.abort = function(){
+      debug("query aborted");
+      thisParser.xhr.abort();
+      
+  };
+
+  /**
+   * Load buffered XML file.
+   *
+   * @param {string|xmlTree} buffer
+   * @param {boolean} isXml - true if buffer is an xmlTree
+   */
+  this.loadBufferedFile = function (buffer, isXml, filename) {
+    thisParser.cleanMemory();
+    var start = new Date().getTime(),
+        data;
+
+    if (isXml === undefined || isXml === false) {
+      // conversion String => XML
+      var parseXml;
+      if (window.DOMParser) {
+        parseXml = function (xmlStr) {
+          return (new window.DOMParser()).parseFromString(xmlStr, 'text/xml');
+        };
+      } else if (typeof window.ActiveXObject !== 'undefined'
+                 && new window.ActiveXObject('Microsoft.XMLDOM')) {
+        parseXml = function (xmlStr) {
+          var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM');
+          xmlDoc.async = 'false';
+          xmlDoc.loadXML(xmlStr);
+          return xmlDoc;
+        };
+      }
+      data = parseXml(buffer);
+    } else {
+      data = buffer;
+    }
+
+    initialize(data, filename || 'VOTableBufferedFile');
+
+    thisParser.loadingTime = new Date().getTime() - start;
+    debug('loading time : ' + thisParser.loadingTime + ' ms.');
+  };
+
+  /**
+   * Initialize the parser's attributes
+   *
+   * @param {xmlTree} data
+   * @param {string} name
+   */
+  function initialize(data, name) {
+    // error
+    if (!data || !data.documentElement || data.getElementsByTagName("parsererror").length) {
+      debug('Error: loaded file does not contains VOTable information, or is not in xml format');
+      vot.loadingState = 'fail';
+      if (errCallbackFunction !== undefined)
+        errCallbackFunction(thisParser);
+    }
+    else {
+      xmlData = data;
+
+      selected.resource.i = 0;
+      selected.resource.xml = null;
+      selected.resource.tables = null;
+      selected.table.i = 0;
+      selected.table.xml = null;
+      vot.nbTables = 0;
+
+      // checks if document is VOTable
+      var firstTag = xmlData.documentElement.tagName;
+      if ( firstTag==='VOTABLE' ) {
+        prefix = '';
+      }
+      else if ( firstTag.indexOf(':VOTABLE')===firstTag.length-8 ) {
+        prefix = firstTag.substring(0, firstTag.length-7); // prefix includes ':' as first character
+      }
+      else {
+        debug('Error: loaded file is not a VOTable');
+        vot.loadingState = 'fail';
+        if (errCallbackFunction !== undefined) {
+          errCallbackFunction(thisParser);
+        }
+        return;
+      }
+
+
+      vot.loadingState = 'success';
+      vot.name = name;
+      vot.resource = xmlData.getElementsByTagName(prefix + 'RESOURCE');
+      vot.nbResources = vot.resource.length;
+      tablesData = [];
+      for (var i = 0; i < vot.nbResources; i++)
+        tablesData[i] = [];
+
+      votableMetadata = parseXmlMetadata('votable');
+      setVOTableGroups();
+
+      if (callbackFunction !== undefined)
+        callbackFunction(thisParser);
+    }
+  };
+
+  /**
+   * Reset all data stored by the parser
+   */
+  this.cleanMemory = function () {
+    xmlData = {};
+    prefix = '';
+    vot = {
+      name: '',
+      loadingState: 'null',
+      nbTables: 0,
+      nbResources: 0
+    };
+    selected = {
+      resource: {
+        i: 0,
+        xml: null,
+        tables: null
+      },
+      table: {
+        i: 0,
+        xml: null
+      }
+    };
+    votableMetadata = '';
+    currentResourceMetadata = '';
+    currentTableMetadata = '';
+    currentTableFields = '';
+    tablesData = '';
+  };
+
+  /***
+   * Get xml table 
+   *
+   * Output  : xml
+   * 
+   *
+   * @return : xml object;
+   ***/
+
+  this.getXMLTable = function() {
+     return xmlData;
+  };
+  
+  
+  /**
+   * Return the prefix of the document.
+   *
+   * @return {string}
+   */
+  function getPrefix() {
+    if (xmlData.getElementsByTagName('VOTABLE').length)
+      return '';
+    else {
+      prefix = xmlData.getElementsByTagName('*')[0];
+      return prefix.tagName.replace('VOTABLE', '');
+    }
+  };
+
+  /**
+   * Return the type of encodage of the currently selected table.
+   *
+   * @return {string}
+   */
+  this.getCurrentTableEncoding = function () {
+    if (selected.table.xml.getElementsByTagName(prefix + 'BINARY').length)
+      return 'BASE-64';
+    return 'UTF-8';
+  };
+
+  /**
+   * Return the total number of tables in the VOTable file.
+   *
+   * @return {integer}
+   */
+  this.getNbTablesInFile = function () {
+    if (!vot.nbTables)
+      vot.nbTables = xmlData.getElementsByTagName(prefix + 'TABLE').length;
+    return vot.nbTables;
+  };
+
+  /**
+   * Return the loading state of the current VOTable file
+   *
+   * @return {boolean}
+   */
+  this.getFileLoadingState = function () {
+    if (vot.loadingState === 'success')
+      return true;
+    return false;
+  };
+
+  //--------------------------------------------------------------------------------
+  // Functions related to the selection of the VOTable file's elements
+
+  /**
+   * Select a resource within the currently loaded VOTable file.
+   *
+   * @param {integer|string} number
+   *
+   * @return {boolean}
+   */
+  this.selectResource = function (number) {
+    var nbResources;
+
+    if (typeof (number) === 'string')
+      number = parseInt(number);
+
+    nbResources = this.getNbResourcesInFile();
+    if (typeof (number) === 'number') {
+      if (number >= 0 && number < nbResources) {
+        selected.resource.i = number;
+        selected.resource.xml = vot.resource[selected.resource.i];
+        selected.resource.tables = selected.resource.xml.getElementsByTagName(prefix + 'TABLE');
+        currentResourceMetadata = parseXmlMetadata('resource');
+        setCurrentResourceGroups();
+        return true;
+      } else {
+        debug('Unable to select resource. '
+              + 'You specified the resource number "' + number + '", '
+              + 'but the ressource number should be between 0 and ' + (nbResources - 1));
+      }
+    } else
+      debug('Unable to select resource. Your argument must be an integer.');
+    return false;
+  };
+
+  /**
+   * Select a table within the currently selected resource.
+   *
+   * @param {integer|string} number
+   *
+   * @return {boolean}
+   */
+  this.selectTable = function (number) {
+    var nbTables;
+
+    if (typeof (number) === 'string')
+      number = parseInt(number);
+
+    nbTables = selected.resource.tables.length;
+
+    if (typeof (number) === 'number') {
+      if (number >= 0 && number < nbTables) {
+        selected.table.i = number;
+        if (selected.resource.xml !== undefined)
+          selected.table.xml = selected.resource.tables[selected.table.i];
+        currentTableMetadata = parseXmlMetadata('table');
+        currentTableFields = parseCurrentTableFields();
+        setCurrentTableGroups();
+        return true;
+      } else {
+        debug('Unable to select table.'
+              + 'You specified the table number "' + number + '", '
+              + 'but the table number should be between 0 and ' + (nbTables - 1));
+      }
+    } else {
+      debug('Unable to select table.'
+            + 'Your argument must be an integer (or an integer contain in a string).');
+    }
+    return false;
+  };
+
+  /**
+   * Parse the fields of the currently selected table.
+   *
+   * @return {Array}
+   */
+  function parseCurrentTableFields() {
+    var fields = [],
+        currentField = {},
+        i = 0, j, childrens, child, tag, nullValue;
+
+    Array.prototype.slice.call(
+      selected.table.xml.getElementsByTagName(prefix + 'FIELD')).forEach(function (element) {
+        //get the attribute
+        Array.prototype.slice.call(element.attributes).forEach(function (element) {
+          currentField[element.name] = element.value;
+        });
+        
+        //childrens = element.children; // --> does not work on IE
+        childrens = getChildren(element);
+        for (j = 0; j < childrens.length; j++) {
+            child = childrens[j];  
+            tag = child.tagName.toLowerCase();
+            currentField[tag] = {};
+            //get the attribute of the child
+            Array.prototype.slice.call(child.attributes).forEach(function (element) {
+                currentField[tag][element.name] = element.value;
+            });
+            //get child text content
+            if(!!child.textContent){
+                currentField[tag]["text"] = child.textContent;
+            }
+          /*if (childrens[j].tagName === 'VALUES') {
+            nullValue = childrens[j].getAttribute('null');
+            break;
+          }*/
+        }
+        /*if (nullValue !== undefined) {
+          currentField['null'] = nullValue;
+          nullValue = undefined;
+        }*/
+        fields[i] = currentField;
+        currentField = {};
+        i += 1;
+      });
+    return fields;
+  };
+
+  /**
+   * Return the fields of the currently selected table.
+   *
+   * @return {Array}
+   */
+  this.getCurrentTableFields = function () {
+    return currentTableFields;
+  };
+
+  /**
+   * Return the data of the currently selected table.
+   *
+   * @return {Array}
+   */
+  this.getCurrentTableData = function () {
+    if (!tablesData[selected.resource.i][selected.table.i]) {
+      if (this.isCurrentTableEncodedInB64())
+        parseB64CurrentTableData();
+      else
+        parseXmlCurrentTableData();
+    }
+    return tablesData[selected.resource.i][selected.table.i];
+  };
+
+  /**
+   * Return the number of resources present in the VOTable file.
+   *
+   * @return {integer}
+   */
+  this.getNbResourcesInFile = function () {
+    return vot.nbResources;
+  };
+
+  /**
+   * Return the number of tables present in the currently selected resource.
+   *
+   * @return {integer}.
+   */
+  this.getCurrentResourceNbTables = function () {
+    if (selected.resource.tables !== null)
+      return selected.resource.tables.length;
+    if (selected.resource.xml !== null)
+      return selected.resource.xml.getElementsByTagName(prefix + 'TABLE').length;
+    return 0;
+  };
+
+  /**
+   * Check if the encodage of current table's data is in base 64.
+   *
+   * @return {boolean}
+   */
+  this.isCurrentTableEncodedInB64 = function () {
+    if (this.getCurrentTableEncoding() === 'BASE-64')
+      return true;
+    return false;
+  };
+
+  /**
+   * Return a specific value of the current table.
+   *
+   * @param {integer} x
+   * @param {integer} y
+   *
+   * @return {number|string|null}
+   */
+  this.getCurrentTableSpecificData = function (x, y) {
+    var currentTableData = '';
+
+    if (typeof (x) !== 'number') {
+      if (typeof (x) === 'string')
+        x = parseInt(x);
+      else {
+        debug('Unable to get this data. '
+              + 'Your argument must be an integer (or an integer contained in a string).');
+        return null;
+      }
+    }
+    if (typeof (y) !== 'number') {
+      if (typeof (y) === 'string')
+        y = parseInt(y);
+      else {
+        debug('Unable to get this data. '
+              + 'Your argument must be an integer (or an integer contained in a string).');
+        return null;
+      }
+    }
+    currentTableData = this.getCurrentTableData();
+
+    if (x < 0 || x >= currentTableData.length) {
+      debug('Unable to get this data. '
+            + 'You specified the first argument "' + x + '" '
+            + 'but it should be between 0 and ' + tablesData.length);
+      return null;
+    }
+    if (y < 0 || y >= currentTableData[x].length) {
+      debug('Unable to get this data. '
+            + 'You specified the second argument "' + y + '" '
+            + 'but it should be between 0 and ' + tablesData[x].length);
+      return null;
+    }
+
+    return currentTableData[x][y];
+  };
+
+  /**
+   * Return basic informations about the currently loaded file.
+   *
+   * @return {Array}
+   */
+  this.whoAmI = function () {
+    var array = {};
+
+    array['xml'] = vot.name;
+    array['resource'] = selected.resource.i;
+    array['table'] = selected.table.i;
+    return array;
+  };
+
+  /**
+   * Util. function returning children nodes of node given as parameter
+   * Excludes all child nodes which are not of type ELEMENT_NODE
+   *
+   * @return {Array}
+   */
+  function getChildren(node) {
+    var children = [];
+    for (var i = 0 ; i<node.childNodes.length; i++) {
+      var child = node.childNodes[i];
+      // keep only node of type Node.ELEMENT_NODE (==1)
+      if (child.nodeType !== 1) {
+        continue;
+      }
+      
+      children.push(child);
+    }
+
+    return children;
+  };
+
+  //-----------------------------------------------------------------------------
+  // Functions related to TABLEDATA parsing.
+
+  /**
+   * Parse data of currently selected table.
+   *
+   * @return {Array}
+   */
+  function parseXmlCurrentTableData() {
+    var fields = thisParser.getCurrentTableFields(),
+        rows = [],
+        columns = [],
+        i = 0, j = 0, k = 0,
+        value,
+        isComplex,
+        // If the current column's value is an array, we need to store
+        // its structure.
+        arrayStructBuffer = [],
+        arrayStruct = [],
+        arrayStructLength = 0,
+        // When array's last dimension is variable
+        arrayStructLastDim,
+        start = new Date().getTime();
+
+    Array.prototype.slice.call(
+      selected.table.xml.getElementsByTagName(prefix + 'TR')).forEach(function (element) {
+        Array.prototype.slice.call(
+          element.getElementsByTagName(prefix + 'TD')).forEach(function (element) {
+            // For each column's value, we distinguish two cases: array of simple value
+            // complex numbers are considered as array, strings are not
+            isComplex = (fields[j].datatype.indexOf('Complex') > -1);
+            if ((fields[j].arraysize !== undefined
+                 && fields[j].datatype !== 'char'
+                 && fields[j].datatype !== 'unicodeChar')
+                || isComplex) {
+              if (element.childNodes[0] !== undefined)
+                arrayStructBuffer = element.childNodes[0].nodeValue.split(/\s* /);
+              else
+                arrayStructBuffer = [];
+
+              if (arrayStructBuffer[0] === '')
+                arrayStructBuffer.splice(0, 1);
+              if (arrayStructBuffer[arrayStructBuffer.length - 1] === '')
+                arrayStructBuffer.splice(arrayStructBuffer.length - 1, 1);
+
+              arrayStruct = [];
+              if (fields[j].arraysize !== undefined) {
+                arrayStruct = fields[j].arraysize.split('x');
+                arrayStructLength = arrayStruct.length;
+
+                // According to VOTable standard, only the last dimension is variable
+                if (/\*/.test(arrayStruct[arrayStructLength - 1])) {
+                  arrayStructLastDim = arrayStructBuffer.length;
+                  if (isComplex)
+                    arrayStructLastDim /= 2;
+                  for (k = 0; k < arrayStructLength - 1; k++)
+                    arrayStructLastDim /= arrayStruct[k];
+                  arrayStruct[arrayStructLength - 1] = arrayStructLastDim;
+                }
+                if (isComplex)
+                  arrayStruct.unshift(2);
+              }
+
+              arrayStructBuffer.forEach(function (element, index, array) {
+                array[index] = convertStringToValue(
+                  element, fields[j].datatype, fields[j].precision);
+              });
+
+              columns[j] = createMultidimensionalArray(arrayStruct, arrayStructBuffer);
+            } else {
+              if (element.childNodes[0] !== undefined) {
+                value = element.childNodes[0].nodeValue;
+                if ((fields[j].null !== undefined && value === fields[j].null) || value === '')
+                  value = null;
+                else
+                  value = convertStringToValue(value, fields[j].datatype, fields[j].precision);
+              } else if (fields[j].datatype !== 'char' && fields[j].datatype !== 'unicodeChar')
+                value = null;
+              else
+                value = '';
+
+              columns[j] = value;
+            }
+            j++;
+          });
+        rows[i] = columns;
+        columns = [];
+        j = 0;
+        i++;
+      });
+
+    thisParser.parsingTime = new Date().getTime() - start;
+    debug('Performance Parsing : ' + thisParser.parsingTime + ' ms.');
+    tablesData[selected.resource.i][selected.table.i] = rows;
+  };
+
+  /**
+   * Convert string value into typed value.
+   *
+   * @param {string} val
+   * @param {string} type
+   * @precision {string} precision
+   *
+   * @return {type}
+   */
+  function convertStringToValue(val, type, precision) {
+    switch (type) {
+    case 'boolean':
+      if (val.toLowerCase() === 'true')
+        return true;
+      if (val.toLowerCase() === 'false')
+        return false;
+      break;
+    case 'bit':
+    case 'unsignedByte':
+    case 'short':
+    case 'int':
+    case 'long':
+      return parseInt(val);
+    case 'char':
+    case 'unicodeChar':
+      return val;
+    case 'float':
+    case 'double':
+    case 'floatComplex':
+    case 'doubleComplex':
+      if (val.indexOf('Inf') > -1) {
+        if (val.substring(0, 1) === '-')
+          return Number.NEGATIVE_INFINITY;
+        return Number.POSITIVE_INFINITY;
+      }
+      if (precision !== undefined)
+        return (parseFloat(val).toFixed(precision) / 1);
+      return (parseFloat(val)) / 1;
+    default:
+      return val;
+    }
+  };
+
+  /**
+   * Return a multidimensional array.
+   * There exists no standard in the VOTable format for defining the null
+   * value of an ARRAY's element.
+   * I.e. there is no way to represent [intA, intB, intC] with intA/B/C as a 'magic number'.
+   * " The VOTable specification notes that the content of each TD
+   *   element must be consistent with the defined field type.
+   *   So if we have an array column, say the CDMatrix for an image
+   *   returned in a SIA, then the null must be a valid 2x2 matrix.
+   *   I.e., <TD/> or <TD></TD> are not acceptable only <TD>NaN NaN NaN NaN</TD>
+   *   (after specifying <VALUES null=’NaN NaN NaN NaN’ />)
+   *   Currently we don’t worry much about array values in our databases
+   *   (though some support arrays). "
+   *
+   * @param {Array} arrayStruct - result's number of elements for each of its dimensions
+   * @param {Array} tempArray - one dimensional array we want to
+   *                            transform into a multidimensional one
+   *
+   * @return {Array}
+   */
+  function createMultidimensionalArray(arrayStruct, tempArray) {
+    var arrayStructLength = arrayStruct.length,
+        res, i;
+
+    // If the result has more than one dimension
+    if (arrayStructLength > 1) {
+      var arrayLength = arrayStruct[arrayStructLength - 1],
+          arrayStructRec = arrayStruct.slice(0, arrayStructLength - 1),
+          tempArrayRec,
+          // length of each array who will be passed to the recursive call
+          tempArrayRecLength = tempArray.length / arrayLength;
+      res = new Array(arrayLength);
+      for (i = 0; i < arrayLength; i++) {
+        tempArrayRec = tempArray.slice(tempArrayRecLength * i, tempArrayRecLength * (i + 1));
+        res[i] = createMultidimensionalArray(arrayStructRec, tempArrayRec);
+      }
+    } else
+      res = tempArray;
+
+    return res;
+  };
+
+  //-----------------------------------------------------------------------------
+  // Functions related to B64 parsing
+
+  /**
+   * Parse data of currently selected table.
+   * The algorithm of this function can be separated in three steps:
+   * 1) We extract from the B64 stream the value of the current column,
+   *    by reading a number of B64 characters equivalent to the current
+   *    column's datasize value (rounded up), in the form of a binary array
+   * 2) That binary array is converted into the current column's datatype
+   * 3) The obtained value is stored into the result array
+   * Note: if the current field's arraysize is defined, theses steps are
+   * repeated for each element in the array
+   *
+   * @return {Array}
+   */
+  function parseB64CurrentTableData() {
+    var fields = [],
+        rows = [],
+        columns = [],
+        ptrCurrentField = 0,
+        bitArray = [],
+        dataB64Length,
+        nbFields,
+        // Note: here floatComplex and doubleComplex have only half the size
+        // they posseses in the VOTable documentation.
+        // This is because we mutiply their arraysize value by two,
+        // as they count as a couple of float/complex
+        dataTypeSize = {
+          short: 16,
+          int: 32,
+          long: 64,
+          float: 32,
+          double: 64,
+          floatComplex: 32,
+          doubleComplex: 64,
+          boolean: 8,
+          char: 8,
+          unicodeChar: 16,
+          bit: 8,
+          unsignedByte: 8
+        },
+        arrayStruct,
+        arraySize,
+        // In the case of a bit array, arraySize is equal to the number of bytes
+        // (rounded up) required to store the bits;
+        // so we need another variable to store their exact number
+        bitArraySize,
+        tempArray = [],
+        dataSize,
+        dataType,
+        value = '',
+        i = 0, k,
+        start;
+
+    endParsingB64 = false;
+
+    dataB64 = selected.table.xml.getElementsByTagName(prefix + 'STREAM')[0].childNodes[0].nodeValue;
+
+    // We must clean the B64 data from all the spaces and tabs it could contains
+    dataB64 = dataB64.replace(/[ \t\r]+/g, '');
+
+    dataB64Length = dataB64.length;
+    fields = thisParser.getCurrentTableFields();
+    nbFields = fields.length;
+
+    start = new Date().getTime();
+
+    do {
+      dataType = fields[ptrCurrentField].datatype;
+      dataSize = dataTypeSize[dataType];
+
+      arraySize = 1;
+      arrayStruct = [];
+
+      // tempArray is reintialized here and not after we determined if
+      // the current value was an array or not because of the non-array
+      // complex values, who will be later in the algorithm considered as ones
+      tempArray = [];
+
+      if (fields[ptrCurrentField].arraysize !== undefined) {
+        arrayStruct = fields[ptrCurrentField].arraysize.split('x');
+
+        if (/\*/.test(arrayStruct[arrayStruct.length - 1])) {
+          // If the last dimension of the array is variable (i.e. equals to '*'),
+          // its value is coded on the 4 first bytes, before the values themselves
+          bitArray = streamB64(32);
+          value = bin2uint32(bitArray);
+          for (k = 0; k < arrayStruct.length - 1; k++)
+            value /= arrayStruct[k];
+          arrayStruct[arrayStruct.length - 1] = value;
+          // we reinitialize bitArray in the case where '*' was equal to 0,
+          // i.e. the array contains no elements
+          bitArray = [];
+        }
+
+        arraySize = arrayStruct[0];
+        for (k = 1; k < arrayStruct.length; k++)
+          arraySize *= arrayStruct[k];
+      }
+
+      switch (dataType) {
+      case 'floatComplex':
+      case 'doubleComplex':
+        arraySize *= 2;
+        arrayStruct.unshift(2);
+        break;
+      case 'bit':
+        // arraySize represents the array's length in bytes;
+        // so when manipulating bit array we need to keep their exact number
+        // stored inside another value
+        bitArraySize = arraySize;
+        arraySize = Math.ceil(arraySize / 8);
+        break;
+      }
+
+      for (k = 0; k < arraySize; k++) {
+        bitArray = streamB64(dataSize);
+
+        // If the returned value is null, it means that all the B64 data has been deciphered;
+        // and that we are reading the padding characters '='
+        if (bitArray === null) {
+          dataType = 'noData';
+          break;
+        }
+
+        switch (dataType) {
+        case 'short':
+          value = bin2short16(bitArray);
+          break;
+        case 'int':
+          value = bin2int32(bitArray);
+          break;
+        case 'long':
+          value = bin2long64(bitArray);
+          break;
+        case 'float':
+        case 'floatComplex':
+          value = bin2float32(bitArray);
+          break;
+        case 'double':
+        case 'doubleComplex':
+          value = bin2double64(bitArray);
+          break;
+        case 'boolean':
+          value = bin2boolean(bitArray);
+          break;
+        case 'char':
+          value = String.fromCharCode(bin2ubyte8(bitArray));
+          break;
+        case 'unicodeChar':
+          value = String.fromCharCode(bin2ubyte16(bitArray));
+          break;
+          // Precisions about the bit array serialization en B64:
+          // (as it is not clearely described in the VOTable documentation)
+          // A bit array is stored on the minimal number of bytes possible
+          // AND the padding bits are added at the END of the array
+          // (so we use the arraysize to distinguish them from the others)
+          // For example:
+          // 11110 will be serialized on 1 byte, 11110000 (3 bits of padding)
+          // 100010011 will be serialized on 2 bytes, 10001001 100000000 (7 bits of padding)
+        case 'bit':
+          value = bitArray;
+          break;
+        case 'unsignedByte':
+          value = bin2ubyte8(bitArray);
+          break;
+        }
+
+        if (fields[ptrCurrentField].precision !== undefined && isFinite(value))
+          value = (value.toFixed(fields[ptrCurrentField].precision)) / 1;
+
+        // In the case of a bit array, each value is already an array, so we concatenate them
+        if (dataType === 'bit')
+          tempArray = tempArray.concat(value);
+        else if (arraySize !== 1)
+          tempArray.push(value);
+      }
+
+      if (dataType === 'noData')
+        break;
+
+      if (dataType === 'bit') {
+        // We suppress the padding bits
+        tempArray.length = bitArraySize;
+        tempArray.forEach(function (element, index, array) {
+          array[index] = parseInt(element);
+        });
+        if (tempArray.length !== 1)
+          value = tempArray;
+        else
+          value = tempArray[0];
+      } else if (arraySize !== 1) {
+        if (dataType !== 'char' && dataType !== 'unicodeChar')
+          value = createMultidimensionalArray(arrayStruct, tempArray);
+        else
+          value = tempArray.join('');
+      } else {
+        if (fields[ptrCurrentField].null !== undefined
+            && value === parseInt(fields[ptrCurrentField].null))
+          value = null;
+      }
+
+      columns[ptrCurrentField] = value;
+
+      if (ptrCurrentField === (nbFields - 1)) {
+        ptrCurrentField = 0;
+        rows[i] = columns;
+        columns = [];
+        i += 1;
+      } else
+        ptrCurrentField += 1;
+    } while (ptrStream < dataB64Length);
+
+    dataB64 = '';
+    bufferBitArray = '';
+    // After each B64 parsing, we must reset ptrStream to 0
+    // (or we will not be able to parse another B64 datas)
+    ptrStream = 0;
+    tablesData[selected.resource.i][selected.table.i] = rows;
+
+    thisParser.parsingTime = new Date().getTime() - start;
+    debug('Performance parsing B64: ' + thisParser.parsingTime + ' ms.');
+  };
+
+  /**
+   * Convert binary array to int 32 bits (unsigned).
+   * Used for determining the size of a variable-length array
+   *
+   * @param {Array} bitArray
+   *
+   * @return {integer}
+   */
+  function bin2uint32(bitArray) {
+    var arrayStructBuffer, dataview, binary;
+
+    arrayStructBuffer = new ArrayBuffer(4);
+    dataview = new DataView(arrayStructBuffer);
+    binary = bitArray.join('');
+    dataview.setUint32(0, parseInt(binary, 2));
+
+    return dataview.getUint32(0);
+  };
+
+  /**
+   * Read a value serialized in base 64 data, and return it as binary array.
+   * Explanation: AAAABgAq/////g== is the B64 chain that represents 6 (int), 42 (short), -2 (int)
+   * FIRST EXAMPLE: reading of the first column's value (int, i.e. dataSize==32):
+   * needBit= 6, i.e. the value we want to read is located on the characters AAAABg
+   * bufferBitArray is empty
+   * ptrStream= 0 (character A)
+   * nb= 0 (c.f. A's B64 value converted in decimal on 6 bits)
+   * we push [0,0,0,0,0,0] in bitArray, bitArray.length= 6 < 32 (dataSize)
+   * bitArray === [0,0,0,0,0,0]
+   * ptrStream= 1 (character A)
+   * nb= 0 (c.f. A's B64 value converted in decimal on 6 bits)
+   * we push [0,0,0,0,0,0] in bitArray, bitArray.length= 12 < 32 (dataSize)
+   * bitArray === [0,0,0,0,0,0,0,0,0,0,0,0]
+   * ptrStream= 2 (character A)
+   * nb= 0 (c.f. A's B64 value converted in decimal on 6 bits)
+   * on push [0,0,0,0,0,0] in bitArray, bitArray.length= 18 < 32 (dataSize)
+   * bitArray === [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
+   * ptrStream= 3 (character A)
+   * nb= 0 (c.f. A's B64 value converted in decimal on 6 bits)
+   * we push [0,0,0,0,0,0] in bitArray, bitArray.length= 24 < 32 (dataSize)
+   * bitArray === [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
+   * ptrStream= 4 (character B)
+   * nb= 1 (c.f. B's B64 value converted in decimal on 6 bits)
+   * we push [0,0,0,0,0,1] in bitArray, bitArray.length= 30 < 32 (dataSize)
+   * bitArray === [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
+   * ptrStream= 5 (character g)
+   * nb= 103 (c.f. g's B64 value converted in decimal on 6 bits), equal to 100000 in base 2
+   * we push [1,0] in bitArray, bitArray.length= 32 === 32 (dataSize),
+   * the rest is [0,0,0,0]
+   * bitArray === [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0]
+   * And 110 is equal to 6
+   * As we have attained the size corresponding to the value's datasize,
+   * we store the rest (that are the 4 first bits of the next column's value) in bufferBitArray
+   * SECOND EXAMPLE: reading of the second column's value (short, i.e. dataSize==16):
+   * needBit= 2, i.e. the value we want to read is located on the characters Aq
+   * bufferBitArray is equal to [0,0,0,0],
+   * we so initialize bitArray to [0,0,0,0] and reset bufferBitArray
+   * ptrStream= 6 (character A)
+   * nb= 0 (c.f. A's B64 value converted in decimal on 6 bits)
+   * we push [0,0,0,0,0,0] in bitArray, bitArray.length= 10 < 16 (dataSize)
+   * bitArray === [0,0,0,0,0,0,0,0,0,0]
+   * ptrStream= 7 (character q)
+   * nb= 113 (c.f. B's B64 value converted in decimal on 6 bits) equal to 101010 in base 2
+   * we push [1,0,1,0,1,0] in bitArray, bitArray.length= 16 === 16 (dataSize)
+   * bitArray === [0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0]
+   * And 101010 is equal to 42
+   * etc...
+   *
+   * @param {integer} dataSize
+   *
+   * @return {Array} - Binary array corresponding to the current value we are reading
+   */
+  function streamB64(dataSize) {
+    var bufferLenght, needBit, bitArray = [],
+        i, nb, z;
+
+    // If we have not yet attained the end of the B64 stream, but we
+    // know that all the next characters are padding characters '='
+    if (endParsingB64)
+      return null;
+    else {
+      bufferLenght = bufferBitArray.length;
+
+      // We define the number of B64 characters we need to read;
+      // and take in account the rest we obtained at previous value's streaming
+      needBit = Math.ceil((dataSize - bufferLenght) / 6);
+
+      for (i = 0; i < bufferLenght; i++)
+        bitArray.push(bufferBitArray[i]);
+
+      bufferBitArray = [];
+
+      for (i = 0; i < needBit; i++) {
+        // When we encounter the character '=', we must realize the
+        // chain's parsing in 2 steps: (i.e. first set endParsingB64
+        // to true, and then return null at the next call of the
+        // function).
+        // That's because the left bits of the character '=' are part
+        // of the last value we are parsing.
+        // Example: we have table with 1 unique field
+        // (datatype==='short') and 2 lines, the 1st containing 42,
+        // the 2nd 24 i.e. in B64 this data is equal to ACoAG===, or
+        // in base 2:
+        // 000000 000010 101000 000000 000110 00XXXX XXXXXX XXXXXX
+        // 24 has then its value spread on the 2 left bits of the first '='; so we must read them
+        // (and let know the parser that we have encountered an '='),
+        // and then at the reading of the next character, end the parsing
+        if (dataB64[ptrStream] === '=') {
+          // In a very specific case, the '=' we are reading, combined
+          // to the bits we possesses in bitArray is equal to a bit
+          // number equal to dataSize, and so we must immediately
+          // return null, without using the endParsingB64 flag.
+          // If we do not, the function will return 0, who will be
+          // considered as a value, and added to the parsed data.
+          // Example: we have a line with 2 columns, the first containing 1 unsignedByte,
+          // and the second an array of unsignedBytes with a VARIABLE length;
+          // The B64 chain we stream is DQAAAAA= (i.e. 13 et []), égal en base 2 à:
+          // 000011 010000 000000 000000 000000 000000 000000 ******, i.e.
+          // 00001101 (13),
+          // 00000000 00000000 00000000 00000000 (empty array with its length on 4 bytes, 0)
+          // 00****** (rest)
+          // Explanation about the rest's treatment:
+          // When we finish to read the last 'A' of the chain, we store [0,0] in bufferBitArray
+          // When we read '=', we set endParsingB64 on true
+          // (i.e. on know that after reading it, the parsing will be over)
+          // Two cases are then -generally- encountered:
+          // i) The value we are reading exists, and is composed of
+          // bufferBitArray+(nb of 0 needed to attain dataSize);
+          // (explained in the previous ACoAG=== example)
+          // ii) The value we are reading doesn't exists, and what we have stored in bufferBitArray
+          // are just padding bits
+          // EXCEPT there exists a third case - an exception of i) -,
+          // where tabBuffer+'=' is equal to a bit number that match dataSize
+          // i.e. bitArray= [0,0] followed by '=' with a dataSize of 8 (unsignedByte)
+          // give [0,0,0,0,0,0,0,0], interpreted by the parser as the value 0
+          // (and this is false, 0 will have been encoded with bitArray followed by 'A' and not '=')
+          // We must so immediately return null in that specific case
+          if (bitArray.length + 6 === dataSize)
+            return null;
+          else
+            endParsingB64 = true;
+        }
+        // 10 is the ASCII character of the carriage return
+        if (dataB64.charCodeAt(ptrStream) === 10)
+          i -= 1;
+        else {
+          nb = b642uint6(dataB64.charCodeAt(ptrStream));
+
+          // >>: Right Shift Binary operator
+          // Example: 110 >>= 1 === 11
+          for (z = 32; z > 0; z >>= 1) {
+            if (bitArray.length !== dataSize) {
+              // nb & z => binary &&, i.e. :
+              // return 1 when each bit of the two values are equals to 1.
+              bitArray.push(((nb & z) === z) ? '1' : '0');
+            } else {
+              bufferBitArray.push(((nb & z) === z) ? '1' : '0');
+            }
+          }
+        }
+        // We read the next character of the B64 string
+        ptrStream += 1;
+      }
+      return bitArray;
+    }
+  };
+
+  /**
+   * Convert Ascii code to base 64 value.
+   * Return the base 10 value of a B64 character, by using its ASCII value
+   * For example, g in B64 is equal to 100000, who is equal to 32 in base 10
+   *
+   * @param {integer} character
+   *
+   * @return {integer}
+   */
+  function b642uint6(character) {
+    var byte;
+
+    if (character > 64 && character < 91) { // char A-Z
+      byte = character - 65;
+    } else if (character > 96 && character < 123) { // char a-z
+      byte = character - 71;
+    } else if (character > 47 && character < 58) { // number 0-9
+      byte = character + 4;
+    } else if (character === 43) { // char +
+      byte = 62;
+    } else if (character === 47) { // char /
+      byte = 63;
+    }
+    return byte;
+  };
+
+  /**
+   * Convert binary array to short 16 bits (signed).
+   *
+   * @param {Array} bitArray
+   *
+   * @return {integer} - (16 bits)
+   */
+  function bin2short16(bitArray) {
+    var arrayStructBuffer, dataview, binary;
+
+    arrayStructBuffer = new ArrayBuffer(2);
+    dataview = new DataView(arrayStructBuffer);
+    binary = bitArray.join('');
+    dataview.setUint16(0, parseInt(binary, 2));
+
+    return dataview.getInt16(0);
+  };
+
+  /**
+   * Convert binary array to int 32 bits (signed).
+   *
+   * @param {Array} bitArray
+   *
+   * @return {integer} - (32 bits)
+   */
+  function bin2int32(bitArray) {
+    var arrayStructBuffer, dataview, binary;
+
+    arrayStructBuffer = new ArrayBuffer(4);
+    dataview = new DataView(arrayStructBuffer);
+    binary = bitArray.join('');
+    dataview.setUint32(0, parseInt(binary, 2));
+
+    return dataview.getInt32(0);
+  };
+
+  /**
+   * Convert binary array to long 64 bits (signed).
+   * As JavaScript does not possesses any method that can directly
+   * return an integer coded on 64 bits, we must process by using
+   * setUint32 and getUint32
+   * Two cases exists:
+   * i) The integer is positive, and we simply have to obtain
+   *    separately the two 4 bytes of right and left, and then add
+   *    them by multiplying the number obtained with the 4 left bytes
+   *    by 2^32
+   * ii) The integer is negative, and we must obtain its opposite bit reversing all its bits and
+   *    do the ones' complement
+   *     We then repeat the same steps described in i)
+   * As with this method we always work with positive integers coded
+   * on 4 bytes while using dataView, we get the values with the
+   * getUint method (and NOT with getInt), because as the integer is
+   * coded on 8 bytes, the 4 right bytes can have their most
+   * significant bit equal to 1 (i.e. getInt will return a negative
+   * result)
+   *
+   * @param {Array} bitArray
+   *
+   * @return {integer} - (64 bit)
+   */
+  function bin2long64(bitArray) {
+    var arrayStructBuffer, dataview, binary,
+        tempBitArray,
+        i, j,
+        bufferLeftBytes,
+        sign = 1;
+
+    if (bitArray[0] === '1') {
+      sign = -1;
+      tempBitArray = bitArray.slice(0);
+      for (i = 0; i < bitArray.length; i++) {
+        bitArray[i] = tempBitArray[i] === '0' ? 1 : 0;
+      }
+      j = bitArray.length - 1;
+      while (bitArray[j] === 1) {
+        bitArray[j] = 0;
+        j--;
+      }
+      bitArray[j] = 1;
+    }
+
+    arrayStructBuffer = new ArrayBuffer(4);
+    dataview = new DataView(arrayStructBuffer);
+
+    binary = bitArray.slice(0, 32).join('');
+    dataview.setUint32(0, parseInt(binary, 2));
+    bufferLeftBytes = dataview.getUint32(0);
+
+    binary = bitArray.slice(32, 64).join('');
+    dataview.setUint32(0, parseInt(binary, 2));
+    // (4294967296) base 10 === (2^32) base 10
+    return sign * (bufferLeftBytes * 4294967296 + dataview.getUint32(0));
+  };
+
+  /**
+   * Convert binary array to float 32 bits.
+   *
+   * @param {Array} - bitArray
+   *
+   * @return {double} (32 bits)
+   */
+  function bin2float32(bitArray) {
+    var arrayStructBuffer, dataview, binary;
+
+    arrayStructBuffer = new ArrayBuffer(4);
+    dataview = new DataView(arrayStructBuffer);
+    binary = bitArray.join('');
+    dataview.setUint32(0, parseInt(binary, 2));
+
+    return dataview.getFloat32(0);
+  };
+
+  /**
+   * Convert binary array to double 64 bits.
+   *
+   * @param {Array} - bitArray
+   *
+   * @return {double} (64 bits)
+   */
+  function bin2double64(bitArray) {
+    var arrayStructBuffer, dataview, binary;
+
+    arrayStructBuffer = new ArrayBuffer(8);
+    dataview = new DataView(arrayStructBuffer);
+
+    // We have to use the setUint32 method twice,
+    // because a method such as setFloat64 doesn't exists in JavaScript
+    binary = bitArray.slice(0, 32).join('');
+    dataview.setUint32(0, parseInt(binary, 2));
+
+    binary = bitArray.slice(32, 64).join('');
+    dataview.setUint32(4, parseInt(binary, 2));
+
+    return dataview.getFloat64(0);
+  };
+
+  /**
+   * Convert binary array to boolean
+   *
+   * @param {Array} - bitArray
+
+   * @return {boolean}
+   */
+  function bin2boolean(bitArray) {
+    var bool = String.fromCharCode(bin2ubyte8(bitArray));
+
+    switch (bool) {
+    case 'T':
+    case 't':
+    case '1':
+      return true;
+    case 'F':
+    case 'f':
+    case '0':
+      return false;
+    case ' ':
+    case '?':
+      return null;
+    }
+    return null;
+  };
+
+  /**
+   * Convert binary array to int 8 bits (unsigned : 0 - 255).
+   *
+   * @param {Array} - bitArray
+   *
+   * @return {integer} - (8 bit)
+   */
+  function bin2ubyte8(bitArray) {
+    var arrayStructBuffer, dataview, binary;
+
+    arrayStructBuffer = new ArrayBuffer(1);
+    dataview = new DataView(arrayStructBuffer);
+    binary = bitArray.join('');
+    dataview.setUint8(0, parseInt(binary, 2));
+
+    return dataview.getUint8(0);
+  };
+
+  /**
+   * Convert binary array to int 16 bits (signed)
+   *
+   * @param {Array} - bitArray
+   *
+   * @return {integer} - (16 bit)
+   */
+  function bin2ubyte16(bitArray) {
+    var arrayStructBuffer, dataview, binary;
+
+    arrayStructBuffer = new ArrayBuffer(2);
+    dataview = new DataView(arrayStructBuffer);
+    binary = bitArray.join('');
+    dataview.setUint8(0, parseInt(binary, 2));
+
+    return dataview.getUint16(0);
+  };
+
+  //--------------------------------------------------------------------------------
+  // Functions related to Metadata parsing
+
+  /**
+   * Parse Xml Metadata for three case : votable, resource and table.
+   *
+   * @param {string} metaType - ('votable' OR 'resource' OR 'table'
+   *
+   * @return {Object}
+   */
+  function parseXmlMetadata(metaType) {
+    var output = {},
+        selectedNode;
+
+    switch (metaType) {
+    case 'votable':
+      selectedNode = xmlData.getElementsByTagName(prefix + 'VOTABLE')[0];
+      break;
+    case 'resource':
+      selectedNode = selected.resource.xml;
+      break;
+    case 'table':
+      selectedNode = selected.table.xml;
+      break;
+    }
+    output = getNodeComponents(selectedNode);
+    if (output.groups === undefined)
+      output.groups = [];
+    if (output.params === undefined)
+      output.params = [];
+    if (output.infos === undefined)
+      output.infos = [];
+    switch (metaType) {
+    case 'votable':
+      // COOSYS: deprecated in VOTable documentation
+      if (output.coosyss === undefined)
+        output.coosyss = [];
+      break;
+    case 'resource':
+      // COOSYS: deprecated in VOTable documentation
+      if (output.coosyss === undefined)
+        output.coosyss = [];
+      if (output.links === undefined)
+        output.links = [];
+      break;
+    case 'table':
+      if (output.fields === undefined)
+        output.fields = [];
+      if (output.links === undefined)
+        output.links = [];
+      break;
+    }
+    return output;
+  };
+
+  /**
+   * Return the different components of an xml node
+   *
+   * @param {Object} node
+   *
+   * @return {Object}
+   */
+  function getNodeComponents(node) {
+    var data = {},
+        output = {},
+        childrens, i, attributeName;
+
+    output = getNodeAttributes(node);
+    //childrens = node.children; // --> does not work on IE
+    childrens = getChildren(node);
+
+
+    for (i = 0; i < childrens.length; i++) {
+      attributeName = childrens[i].tagName.substr(prefix.length);
+      // We do not take the resource, table, field, and data nodes as they are parsed by
+      // other specific methods
+      if (attributeName !== 'RESOURCE'
+          && attributeName !== 'TABLE'
+          && attributeName !== 'FIELD'
+          && attributeName !== 'DATA') {
+        switch (attributeName) {
+        case 'GROUP':
+          data = getNodeComponents(childrens[i]);
+          break;
+        case 'DESCRIPTION':
+          data = childrens[i].childNodes[0].nodeValue;
+          break;
+        default:
+          data = getNodeAttributes(childrens[i]);
+        }
+        attributeName = attributeName.toLowerCase() + 's';
+        if (!output[attributeName]) {
+          if (attributeName !== 'descriptions') {
+            output[attributeName] = new Array(data);
+          } else {
+            output['description'] = data;
+          }
+        } else {
+          output[attributeName].push(data);
+        }
+      }
+      data = {};
+    }
+    return output;
+  };
+
+  /**
+   * Return the different attributes of an xml node
+   *
+   * @param {Object} node
+   *
+   * @return {Array}
+   */
+  function getNodeAttributes(node) {
+    if (node===undefined) {
+      return [];
+    }
+
+    var data = {},
+        j;
+    for (j = 0; j < node.attributes.length; j += 1) {
+      data[node.attributes[j].name] = node.attributes[j].value;
+    }
+    return data;
+  };
+
+  /**
+   * Return Metadata of the VOTable file.
+   *
+   * @return {Object}
+   */
+  this.getVOTableMetadata = function () {
+    return votableMetadata;
+  };
+
+  /**
+   * Return Metadata of the current resource.
+   *
+   * @return {Object}
+   */
+  this.getCurrentResourceMetadata = function () {
+    return currentResourceMetadata;
+  };
+
+  /**
+   * Return Metadata of the current table.
+   *
+   * @return {Object}
+   */
+  this.getCurrentTableMetadata = function () {
+    return currentTableMetadata;
+  };
+
+  /**
+   * Update the groups Metadata by setting their FIELDrefs and PARAMrefs values to the related ones
+   * We must distinguish each case:
+   * i) The FIELDrefs are easy to link with their fields of reference, because according to the
+   *      VOTable documentation, a FIELDref can only be related to a field defined in the same table
+   *      (and only in a table)
+   * ii) The PARAMrefs are more complicated, because they can references PARAM defined not only in
+   *      the current node, but also in the resource/VOTable nodes located above the current node
+   * Note: As the PARAMrefs and the FIELDrefs can possess their own
+   * ucd and such attributes, we keep them when we do the link to the
+   * referenced element by adding 'Ref' to the attribute name
+   *
+   * @param {Object[]} groups
+   * @param {Object[]} params - List of the parameters related to the groups
+   *
+   * @return {Object[]}
+   */
+  function setGroupsRefs(groups, params) {
+    var i, j, k, nbElts, key;
+    for (k = 0; k < groups.length; k++) {
+      if (groups[k].groups === undefined)
+        groups[k].groups = [];
+      if (groups[k].params === undefined)
+        groups[k].params = [];
+      if (groups[k]['fieldrefs'] !== undefined) {
+        for (i = 0; i < groups[k]['fieldrefs'].length; i++) {
+          for (j = 0; j < currentTableFields.length; j++) {
+            if (currentTableFields[j].ID === groups[k]['fieldrefs'][i].ref) {
+              if (groups[k]['fields'] === undefined)
+                groups[k]['fields'] = [];
+              groups[k]['fields'].push(currentTableFields[j]);
+              nbElts = groups[k]['fields'].length - 1;
+              for (key in groups[k]['fieldrefs'][i]) {
+                if (key !== 'ref') {
+                  groups[k]['fields'][nbElts][key + 'Ref'] = groups[k]['fieldrefs'][i][key];
+                }
+              }
+            }
+          }
+        }
+        delete groups[k]['fieldrefs'];
+      }
+      if (groups[k]['paramrefs'] !== undefined) {
+        for (i = 0; i < groups[k]['paramrefs'].length; i++) {
+          for (j = 0; j < params.length; j++) {
+            if (params[j].ID === groups[k]['paramrefs'][i].ref) {
+              if (groups[k]['params'] === undefined)
+                groups[k]['params'] = [];
+              groups[k]['params'].push(params[j]);
+              nbElts = groups[k]['params'].length - 1;
+              for (key in groups[k]['paramrefs'][i]) {
+                if (key !== 'ref') {
+                  groups[k]['params'][nbElts][key + 'Ref'] = groups[k]['paramrefs'][i][key];
+                }
+              }
+            }
+          }
+        }
+        delete groups[k]['paramrefs'];
+      }
+      if (groups[k]['groups'] !== undefined)
+        groups[k]['groups'] = setGroupsRefs(groups[k]['groups'], params);
+    }
+    return groups;
+  };
+
+  /**
+   * Set the values of ref attributes in the VOTable's groups
+   */
+  function setVOTableGroups() {
+    if (votableMetadata['groups'] !== undefined)
+      setGroupsRefs(votableMetadata['groups'], getRelatedMetadataParams(1));
+  };
+
+  /**
+   * Set the values of ref attributes in the current resource's groups
+   */
+  function setCurrentResourceGroups() {
+    if (currentResourceMetadata['groups'] !== undefined)
+      setGroupsRefs(currentResourceMetadata['groups'], getRelatedMetadataParams(2));
+  };
+
+  /**
+   * Set the values of ref attributes in the current table's groups
+   */
+  function setCurrentTableGroups() {
+    if (currentTableMetadata['groups'] !== undefined)
+      setGroupsRefs(currentTableMetadata['groups'], getRelatedMetadataParams(3));
+  };
+
+  /**
+   * Return the array of parameters related to the relative position of the PARAMrefs
+   * we want to links them to in the VOTable xml tree
+   *
+   * @param {integer} deepness (1 if VOTable, 2 if resource, 3 if table)
+   *
+   * @return {Object[]}
+   */
+  function getRelatedMetadataParams(deepness) {
+    var param = [];
+    if (deepness >= 1 && votableMetadata['params'] !== undefined)
+      param = param.concat(votableMetadata['params']);
+    if (deepness >= 2 && currentResourceMetadata['params'] !== undefined)
+      param = param.concat(currentResourceMetadata['params']);
+    if (deepness >= 3 && currentTableMetadata['params'] !== undefined)
+      param = param.concat(currentTableMetadata['params']);
+    return param;
+  };
+
+  /**
+   * Return VOTable's groups
+   *
+   * @return {Object[]}
+   */
+  this.getVOTableGroups = function () {
+    if (votableMetadata === undefined) {
+      debug('Warning: no Meta from VOTable file; it seems no file has been loadFileed');
+      return undefined;
+    }
+    return votableMetadata['groups'] !== undefined ? votableMetadata['groups'] : [];
+  };
+
+  /**
+   * Return current resource's groups
+   *
+   * @return {Object[]}
+   */
+  this.getCurrentResourceGroups = function () {
+    if (currentResourceMetadata === undefined) {
+      debug('Warning: no Meta from Resource');
+      return undefined;
+    }
+    return currentResourceMetadata['groups'] !== undefined
+      ? currentResourceMetadata['groups'] : [];
+  };
+
+  /**
+   * Return current table's groups
+   *
+   * @return {Object[]}
+   */
+  this.getCurrentTableGroups = function () {
+    if (currentTableMetadata === undefined) {
+      debug('Warning: no Meta from Table');
+      return undefined;
+    }
+    return currentTableMetadata['groups'] !== undefined
+      ? currentTableMetadata['groups'] : [];
+  };
+
+  //----------------------------------------------------------------------------
+  // Functions related to the results's rendering (in HTML format)
+
+  /**
+   * Get fields of table (HTML)
+   *
+   * @return {string}
+   */
+  this.getHtmlCurrentTableFields = function () {
+    var i, fields, nbFields, output = '';
+
+    fields = this.getCurrentTableFields();
+    nbFields = fields.length;
+    output += '<tr>';
+    for (i = 0; i < nbFields; i++) {
+      output += '<th>' + fields[i].name + '</th>';
+    }
+    output += '</tr>';
+
+    return output;
+  };
+
+  /**
+   * Get tablesData of table (HTML)
+   *
+   * @param {integer} min - (optional)
+   * @param {integer} max - (optional)
+   *
+   * @return {string}
+   */
+  this.getHtmlCurrentTableData = function (min, max) {
+    var i, j, data, nbRows, nbColumns, output = '';
+
+    if (typeof (min) === 'string') {
+      min = parseInt(min);
+    }
+    if (typeof (max) === 'string') {
+      max = parseInt(max);
+    }
+    min = min || 0;
+
+    data = this.getCurrentTableData();
+
+    if (max && max < data.TR.length) {
+      nbRows = max;
+    } else {
+      if (!max) {
+        debug('Warning. You specified the maximum at "' + max
+              + '" but the maximum possible is ' + data.length);
+      }
+      nbRows = data.length;
+    }
+
+    for (i = min; i < nbRows; i++) {
+      nbColumns = data[i].length;
+      output += '<tr>';
+
+      for (j = 0; j < nbColumns; j++) {
+        output += '<td>' + data[i][j] + '</td>';
+      }
+
+      output += '</tr>';
+    }
+
+    return output;
+  };
+
+  //----------------------------------------------------------------------------
+  // Debug functions
+
+  /**
+   * Display error in browser console.
+   *
+   * @param {boolean} display
+   */
+  this.displayErrors = function (display) {
+    if (display) {
+      debugMode = true;
+    } else {
+      debugMode = false;
+    }
+  };
+
+  /**
+   * Print error in console if debug mode is active.
+   *
+   * @param {string} error
+   */
+  function debug(message) {
+    if (debugMode)
+      console.warn('DEBUG => ' + message);
+  };
+};
+
+// Export parser in CommonJS format if possible.
+if (typeof module === 'object') module.exports = new VOTableParser();
diff --git a/public/index.html b/public/index.html
index 5b9b521..7b5e3ba 100644
--- a/public/index.html
+++ b/public/index.html
@@ -28,6 +28,9 @@
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
     <link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
     <title>Astron ESAP</title>
+
+    <script type="text/javascript" src="%PUBLIC_URL%/assets/js/samp.js"></script>
+    <script type="text/javascript" src="%PUBLIC_URL%/assets/js/votable.js"></script>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
diff --git a/src/components/query/samp/ReactVOTable.js b/src/components/query/samp/ReactVOTable.js
new file mode 100644
index 0000000..768ee9c
--- /dev/null
+++ b/src/components/query/samp/ReactVOTable.js
@@ -0,0 +1,94 @@
+import React from 'react';
+
+// wrapper around votable.js
+// https://gitlab.com/cdsdevcorner/votable.js/-/tree/master/
+
+// construct a json structure from an incoming votable in xml format
+/*
+ votable_as_json:
+ resources:
+ tables:
+ data: Array(100)
+ 0: Array(3)
+ 0: "https://vo.astron.nl/getproduct/APERTIF_DR1/190807041_AP_B001/image_mf_02.fits"
+ 1: "m1403+5324"
+ 2: "190807041_AP_B001"
+ fieldnames: (3) ["accref", "target", "task_id"]
+ */
+
+export const getVOTableAsJSON = (votable) => {
+    console.log("ReactVOTAble.getVOTableAsJSON")
+    const myCallback = () => {
+        console.log("myCallBack")
+    }
+
+    const myErrCallback = () => {
+        alert("error loading table")
+    }
+
+    var p = new window.VOTableParser();
+    p.displayErrors(true)
+    p.setCallbackFunctions(myCallback, myErrCallback)
+    p.loadFile(votable.URL)
+
+/*
+    // alternative, I don't know which one is better... loadFile or loadBufferedFile
+    p.loadBufferedFile(votable, true)
+    nr_of_tables = p.getNbTablesInFile()
+    nr_of_resources = p.getNbResourcesInFile()
+*/
+
+    // get the metadata
+    let meta = p.getVOTableMetadata()
+
+    var nbResources = p.getNbResourcesInFile();
+
+    var nbTablesInResource = 0;
+    var currentTableGroups = [];
+    var currentTableFields = [];
+    var currentTableData = [[]];
+
+    let votable_as_json = {}
+    let array_of_resources = []
+
+    for(var i = 0; i < nbResources; i++) {
+
+        let resource_as_json = {}
+        p.selectResource(i);
+        nbTablesInResource = p.getCurrentResourceNbTables();
+
+        let array_of_tables = []
+
+        for(var j = 0; j < nbTablesInResource; j++) {
+
+            let table_as_json = {}
+
+            p.selectTable(j);
+            currentTableGroups = p.getCurrentTableGroups();
+            currentTableFields = p.getCurrentTableFields();
+            currentTableData = p.getCurrentTableData();
+
+            // add the data to the json structure
+
+            // extract fieldnames for this table as an array of strings
+            let numberOfFields = currentTableFields.length
+            let fieldNames = []
+            for (var k = 0; k < numberOfFields; k++) {
+                fieldNames.push(currentTableFields[k].name)
+            }
+
+            table_as_json['fieldnames'] = fieldNames
+            table_as_json['data'] = currentTableData
+            array_of_tables.push(table_as_json)
+        }
+
+        resource_as_json['tables'] = array_of_tables
+        array_of_resources.push(resource_as_json)
+
+    }
+    votable_as_json['resources'] = array_of_resources
+
+    p.cleanMemory();
+
+    return votable_as_json
+}
\ No newline at end of file
diff --git a/src/components/query/samp/SampGrid.js b/src/components/query/samp/SampGrid.js
new file mode 100644
index 0000000..0d7fdfe
--- /dev/null
+++ b/src/components/query/samp/SampGrid.js
@@ -0,0 +1,43 @@
+import React from "react";
+import { Table } from "react-bootstrap";
+
+export default function SampResults(props) {
+
+    //alert('SampGrid')
+    let fieldnames = props.fieldnames
+    let data = props.votable_in_json['data']
+
+    return (
+        <>
+        <Table className="mt-3" responsive>
+            <thead>
+
+            <tr className="bg-light">
+                {fieldnames.map((field) => {
+                    return (
+                        <th key={field}>{field}</th>
+                    );
+                })}
+
+            </tr>
+
+            </thead>
+            <tbody>
+                {data.map((record) => {
+                    return (
+                        <tr key={record}>
+                            {record.map((col) => {
+                                return (
+                                    <td key={col}>{col}</td>
+                                )
+                            })}
+                        </tr>
+                    );
+                })}
+            </tbody>
+
+        </Table>
+        </>
+    );
+
+}
diff --git a/src/components/query/samp/SampPage.js b/src/components/query/samp/SampPage.js
new file mode 100644
index 0000000..f3f9313
--- /dev/null
+++ b/src/components/query/samp/SampPage.js
@@ -0,0 +1,112 @@
+import React, {useState }  from 'react';
+
+import { getVOTableAsJSON } from './ReactVOTable'
+import SampGrid from './SampGrid'
+
+export default function SampPage(props) {
+    const [ myVOTable, setMyVOTable] = useState([]);
+
+    const pingFunc = function (my_connection) {
+        my_connection.notifyAll([new window.samp.Message("samp.app.ping", {})])
+    }
+
+    const register = () => {
+        connector.register()
+    }
+    const unregister = () => {
+        connector.unregister()
+    }
+
+    const handlePingClick = () => {
+        connector.runWithConnection(pingFunc)
+    }
+
+    const handlePing = (cc, senderId, message, isCall) => {
+        alert('handle samp.app.ping')
+        if (isCall) {
+            return {text: "ping to you, " + cc.getName(senderId)};
+        }
+    }
+
+    const handleLoadVOTable = (cc, senderId, message, isCall) => {
+        // alert('handle table.load.votable')
+        var params = message["samp.params"];
+        var origUrl = params["url"];
+        var proxyUrl = cc.connection.translateUrl(origUrl);
+        var xhr = window.samp.XmlRpcClient.createXHR();
+        var e;
+
+        xhr.open("GET", proxyUrl);
+        xhr.onload = function() {
+            var xml = xhr.responseXML;
+            if (xml) {
+                try {
+                    let tableId = params["table-id"];
+                    //alert(tableId)
+
+                    // parse the VO Table in xml format
+                    let results = getVOTableAsJSON(xml)
+
+                    // assume a single resource and a single table for now
+                    let table= results.resources[0].tables[0]
+
+                    // add fieldnames and data to the state hook
+                    // this will trigger a render of this component
+                    setMyVOTable(table)
+                }
+                catch (e) {
+                    alert("Error displaying table:\n" +
+                        e.toString());
+                }
+            }
+            else {
+                alert("No XML response");
+            }
+        };
+        xhr.onerror = function(err) {
+            alert("Error getting table " + origUrl + "\n" +
+                "(" + err + ")");
+        };
+        xhr.send(null);
+    }
+
+    var cc = new window.samp.ClientTracker();
+
+    // attach eventhandlers
+    var callHandler = cc.callHandler;
+
+    callHandler["samp.app.ping"] = function(senderId, message, isCall) {
+        handlePing(cc,senderId, message, isCall)
+    };
+
+    callHandler["table.load.votable"] = function(senderId, message, isCall) {
+        handleLoadVOTable(cc,senderId, message, isCall)
+    };
+
+
+    var subs = cc.calculateSubscriptions();
+
+    // initialize the connector
+    var connector = new window.samp.Connector("astroview", {"samp.name": "AstroView"}, cc, subs)
+
+    // only render when myVOTable has a value
+    var renderSampGrid
+    let fieldnames = myVOTable['fieldnames']
+    if (fieldnames!==undefined) {
+        renderSampGrid = <SampGrid fieldnames={fieldnames} votable_in_json={myVOTable}/>
+    }
+
+    return (
+        <div className="App">
+            <div>
+                <h2>SAMP demo</h2>
+                <p>Start a SAMP enabled application (like Topcat), register to the hub and transmit data from Topcat.</p>
+                <button variant="outline-warning" onClick={() => register()}>register</button>&nbsp;
+                <button variant="outline-success" onClick={() => handlePingClick()}>SAMP Ping</button>&nbsp;
+                <button variant="outline-warning" onClick={() => unregister()}>unregister</button>&nbsp;
+
+                {renderSampGrid}
+            </div>
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/src/contexts/GlobalContext.js b/src/contexts/GlobalContext.js
index 5a8830a..3cdb045 100644
--- a/src/contexts/GlobalContext.js
+++ b/src/contexts/GlobalContext.js
@@ -9,7 +9,7 @@ export function GlobalContextProvider({ children }) {
   
   const api_host =
     process.env.NODE_ENV === "development"
-      ? "https://sdc.astron.nl:5555/esap-api/"
+      ? "http://localhost:8000/esap-api/"
       : "https://sdc.astron.nl:5555/esap-api/";
   // "https://sdc.astron.nl:5555/esap-api/"
   // "http://localhost:5555/esap-api/"
diff --git a/src/routes/Routes.js b/src/routes/Routes.js
index 25d2872..d433e82 100644
--- a/src/routes/Routes.js
+++ b/src/routes/Routes.js
@@ -9,8 +9,10 @@ import { BrowserRouter as Router } from "react-router-dom";
 import NavBar from "../components/NavBar";
 import Rucio from "../components/Rucio";
 import Interactive from "../components/Interactive";
+
 import { IVOAContextProvider } from "../contexts/IVOAContext";
 import { IDAContext } from "../contexts/IDAContext";
+import SampPage from '../components/query/samp/SampPage';
 
 export default function Routes() {
   const { navbar, handleLogin, handleLogout, handleError } = useContext(GlobalContext);
@@ -47,6 +49,7 @@ export default function Routes() {
         <Route exact path={["/adex-query", "/archives/:uri/query"]}>
           <QueryCatalogs />
         </Route>
+        <Route exact path="/samp" component={SampPage} />
       </Switch>
     </Router>
   );
-- 
GitLab