diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1eb90daef35c8c5a426395fadfd5108eef1f11e4..bb75e53da512545ac3a1a5889db3f96d7c545805 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -28,12 +28,12 @@ deploy-esap-gui:
       - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
       - mkdir -p ~/.ssh
       - chmod 700 ~/.ssh
-      - ssh-keyscan sdc.astron.nl >> ~/.ssh/known_hosts
+      - ssh-keyscan dop814.astron.nl >> ~/.ssh/known_hosts
       - chmod 644 ~/.ssh/known_hosts
     script:
         - echo "Deploying to server"
-        - scp -r build/static/* vermaas@sdc.astron.nl:~/shared/static
-        - scp -r build/* vermaas@sdc.astron.nl:~/shared/public_html/esap-gui
+        - scp -r build/static/* vermaas@dop814.astron.nl:~/shared/static
+        - scp -r build/* vermaas@dop814.astron.nl:~/shared/public_html/esap-gui
         - echo "Deployed"
     when: manual
     only:
diff --git a/public/assets/js/samp.js b/public/assets/js/samp.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d1f749ae23be02e0ed9d1ff45d7ccfb377d67ab
--- /dev/null
+++ b/public/assets/js/samp.js
@@ -0,0 +1,1318 @@
+// samp
+// ----
+// Provides capabilities for using the SAMP Web Profile from JavaScript.
+// Exported tokens are in the samp.* namespace.
+// Inline documentation is somewhat patchy (partly because I don't know
+// what javascript documentation is supposed to look like) - it is
+// suggested to use it conjunction with the provided examples,
+// currently visible at http://astrojs.github.com/sampjs/
+// (gh-pages branch of github sources).
+
+// LICENCE
+// =======
+// samp.js - A Javascript module for connection to VO SAMP hubs
+// Written in 2013 by Mark Taylor
+//
+// This file is distributed under the CC0 Public Domain Dedication,
+// <http://creativecommons.org/publicdomain/zero/1.0/>.
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any
+// warranty.
+
+var samp = (function() {
+
+    // Constants defining well-known location of SAMP Web Profile hub etc.
+    var WEBSAMP_PORT = 21012;
+    var WEBSAMP_PATH = "/";
+    var WEBSAMP_PREFIX = "samp.webhub.";
+    var WEBSAMP_CLIENT_PREFIX = "";
+
+    // Tokens representing permissible types in a SAMP object (e.g. a message)
+    TYPE_STRING = "string";
+    TYPE_LIST = "list";
+    TYPE_MAP = "map";
+
+    var heir = function(proto) {
+        function F() {};
+        F.prototype = proto;
+        return new F();
+    };
+
+    // Utility functions for navigating DOM etc.
+    // -----------------------------------------
+
+    var getSampType = function(obj) {
+        if (typeof obj === "string") {
+            return TYPE_STRING;
+        }
+        else if (obj instanceof Array) {
+            return TYPE_LIST;
+        }
+        else if (obj instanceof Object && obj !== null) {
+            return TYPE_MAP;
+        }
+        else {
+            throw new Error("Not legal SAMP object type: " + obj);
+        }
+    };
+    var getChildElements = function(el, childTagName) {
+        var children = el.childNodes;
+        var child;
+        var childEls = [];
+        var i;
+        for (i = 0; i < children.length; i++) {
+            child = children[i];
+            if (child.nodeType === 1) {  // Element
+                if (childTagName && (child.tagName !== childTagName)) {
+                    throw new Error("Child <" + children[i].tagName + ">"
+                                  + " of <" + el.tagName + ">"
+                                  + " is not a <" + childTagName + ">");
+                }
+                childEls.push(child);
+            }
+        }
+        return childEls;
+    };
+    var getSoleChild = function(el, childTagName) {
+        var children = getChildElements(el, childTagName);
+        if (children.length === 1 ) {
+            return children[0];
+        }
+        else {
+            throw new Error("No sole child of <" + el.tagName + ">");
+        }
+    };
+    var getTextContent = function(el) {
+        var txt = "";
+        var i;
+        var child;
+        for (i = 0; i < el.childNodes.length; i++ ) {
+            child = el.childNodes[i];
+            if (child.nodeType === 1) {           // Element 
+                throw new Error("Element found in text content");
+            }
+            else if (child.nodeType === 3 ||      // Text
+                     child.nodeType === 4 ) {     // CDATASection
+                txt += child.nodeValue;
+            }
+        }
+        return txt;
+    };
+    var stringify = function(obj) {
+        return typeof JSON === "undefined" ? "..." : JSON.stringify(obj);
+    };
+
+    // XmlRpc class:
+    // Utilities for packing and unpacking XML-RPC messages.
+    // See xml-rpc.com.
+
+    var XmlRpc = {};
+
+    // Takes text and turns it into something suitable for use as the content
+    // of an XML-RPC string - special characters are escaped.
+    XmlRpc.escapeXml = function(s) {
+        return s.replace(/&/g, "&amp;")
+                .replace(/</g, "&lt;")
+                .replace(/>/g, "&gt;");
+    };
+
+    // Asserts that the elements of paramList match the types given by typeList.
+    // TypeList must be an array containing only TYPE_STRING, TYPE_LIST
+    // and TYPE_MAP objects in some combination.  paramList must be the
+    // same length.
+    // In case of mismatch an error is thrown.
+    XmlRpc.checkParams = function(paramList, typeList) {
+        var i;
+        for (i = 0; i < typeList.length; i++) {
+            if (typeList[i] !== TYPE_STRING &&
+                typeList[i] !== TYPE_LIST &&
+                typeList[i] !== TYPE_MAP) {
+                throw new Error("Unknown type " + typeList[i]
+                              + " in check list");
+            }
+        }
+        var npar = paramList.length;
+        var actualTypeList = [];
+        var ok = true;
+        for (i = 0; i < npar; i++) {
+            actualTypeList.push(getSampType(paramList[i]));
+        }
+        ok = ok && (typeList.length === npar);
+        for (i = 0; ok && i < npar; i++ ) {
+            ok = ok && typeList[i] === actualTypeList[i];
+        }
+        if (!ok) {
+            throw new Error("Param type list mismatch: " 
+                          + "[" + typeList + "] != "
+                          + "[" + actualTypeList + "]");
+        }
+    };
+
+    // Turns a SAMP object (structure of strings, lists, maps) into an
+    // XML string suitable for use with XML-RPC.
+    XmlRpc.valueToXml = function v2x(obj, prefix) {
+        prefix = prefix || "";
+        var a;
+        var i;
+        var result;
+        var type = getSampType(obj);
+        if (type === TYPE_STRING) {
+            return prefix
+                 + "<value><string>"
+                 + XmlRpc.escapeXml(obj)
+                 + "</string></value>";
+        }
+        else if (type === TYPE_LIST) {
+            result = [];
+            result.push(prefix + "<value>",
+                        prefix + "  <array>",
+                        prefix + "    <data>");
+            for (i = 0; i < obj.length; i++) {
+                result.push(v2x(obj[i], prefix + "      "));
+            }
+            result.push(prefix + "    </data>",
+                        prefix + "  </array>",
+                        prefix + "</value>");
+          
+            return result.join("\n");
+        }
+        else if (type === TYPE_MAP) {
+            result = [];
+            result.push(prefix + "<value>");
+            result.push(prefix + "  <struct>");
+            for (i in obj) {
+                result.push(prefix + "    <member>");
+                result.push(prefix + "      <name>"
+                          + XmlRpc.escapeXml(i)
+                          + "</name>");
+                result.push(v2x(obj[i], prefix + "      "));
+                result.push(prefix + "    </member>");
+            }
+            result.push(prefix + "  </struct>");
+            result.push(prefix + "</value>");
+            return result.join("\n");
+        }
+        else {
+            throw new Error("bad type");  // shouldn't get here
+        }
+    };
+
+    // Turns an XML string from and XML-RPC message into a SAMP object
+    // (structure of strings, lists, maps).
+    XmlRpc.xmlToValue = function x2v(valueEl, allowInt) {
+        var childEls = getChildElements(valueEl);
+        var i;
+        var j;
+        var txt;
+        var node;
+        var childEl;
+        var elName;
+        if (childEls.length === 0) {
+            return getTextContent(valueEl);
+        }
+        else if (childEls.length === 1) {
+            childEl = childEls[0];
+            elName = childEl.tagName;
+            if (elName === "string") {
+                return getTextContent(childEl);
+            }
+            else if (elName === "array") {
+                var valueEls =
+                    getChildElements(getSoleChild(childEl, "data"), "value");
+                var list = [];
+                for (i = 0; i < valueEls.length; i++) {
+                    list.push(x2v(valueEls[i], allowInt));
+                }
+                return list;
+            }
+            else if (elName === "struct") {
+                var memberEls = getChildElements(childEl, "member");
+                var map = {};
+                var s_name;
+                var s_value;
+                var jc;
+                for (i = 0; i < memberEls.length; i++) {
+                    s_name = undefined;
+                    s_value = undefined;
+                    for (j = 0; j < memberEls[i].childNodes.length; j++) {
+                        jc = memberEls[i].childNodes[j];
+                        if (jc.nodeType == 1) {
+                            if (jc.tagName === "name") {
+                                s_name = getTextContent(jc);
+                            }
+                            else if (jc.tagName === "value") {
+                                s_value = x2v(jc, allowInt);
+                            }
+                        }
+                    }
+                    if (s_name !== undefined && s_value !== undefined) {
+                        map[s_name] = s_value;
+                    }
+                    else {
+                        throw new Error("No <name> and/or <value> "
+                                      + "in <member>?");
+                    }
+                }
+                return map;
+            }
+            else if (allowInt && (elName === "int" || elName === "i4")) {
+                return getTextContent(childEl);
+            }
+            else {
+                throw new Error("Non SAMP-friendly value content: "
+                              + "<" + elName + ">");
+            }
+        }
+        else {
+            throw new Error("Bad XML-RPC <value> content - multiple elements");
+        }
+    };
+
+    // Turns the content of an XML-RPC <params> element into an array of
+    // SAMP objects.
+    XmlRpc.decodeParams = function(paramsEl) {
+        var paramEls = getChildElements(paramsEl, "param");
+        var i;
+        var results = [];
+        for (i = 0; i < paramEls.length; i++) {
+            results.push(XmlRpc.xmlToValue(getSoleChild(paramEls[i], "value")));
+        }
+        return results;
+    };
+
+    // Turns the content of an XML-RPC <fault> element into an XmlRpc.Fault
+    // object.
+    XmlRpc.decodeFault = function(faultEl) {
+        var faultObj = XmlRpc.xmlToValue(getSoleChild(faultEl, "value"), true);
+        return new XmlRpc.Fault(faultObj.faultString, faultObj.faultCode);
+    };
+
+    // Turns an XML-RPC response element (should be <methodResponse>) into
+    // either a SAMP response object or an XmlRpc.Fault object.
+    // Note that a fault response does not throw an error, so check for
+    // the type of the result if you want to know whether a fault occurred.
+    // An error will however be thrown if the supplied XML does not
+    // correspond to a legal XML-RPC response.
+    XmlRpc.decodeResponse = function(xml) {
+        var mrEl = xml.documentElement;
+        if (mrEl.tagName !== "methodResponse") {
+            throw new Error("Response element is not <methodResponse>");
+        }
+        var contentEl = getSoleChild(mrEl);
+        if (contentEl.tagName === "fault") {
+            return XmlRpc.decodeFault(contentEl);
+        }
+        else if (contentEl.tagName === "params") {
+            return XmlRpc.decodeParams(contentEl)[0];
+        }
+        else {
+            throw new Error("Bad XML-RPC response - unknown element"
+                          + " <" + contentEl.tagName + ">");
+        }
+    };
+
+    // XmlRpc.Fault class:
+    // Represents an XML-RPC Fault response.
+    XmlRpc.Fault = function(faultString, faultCode) {
+        this.faultString = faultString;
+        this.faultCode = faultCode;
+    };
+    XmlRpc.Fault.prototype.toString = function() {
+        return "XML-RPC Fault (" + this.faultCode + "): " + this.faultString;
+    };
+
+    // XmlRpcRequest class:
+    // Represents an call which can be sent to an XML-RPC server.
+    var XmlRpcRequest = function(methodName, params) {
+        this.methodName = methodName;
+        this.params = params || [];
+    }
+    XmlRpcRequest.prototype.toString = function() {
+        return this.methodName + "(" + stringify(this.params) + ")";
+    };
+    XmlRpcRequest.prototype.addParam = function(param) {
+        this.params.push(param);
+        return this;
+    };
+    XmlRpcRequest.prototype.addParams = function(params) {
+        var i;
+        for (i = 0; i < params.length; i++) {
+            this.params.push(params[i]);
+        }
+        return this;
+    };
+    XmlRpcRequest.prototype.checkParams = function(typeList) {
+        XmlRpc.checkParams(this.params, typeList);
+    };
+    XmlRpcRequest.prototype.toXml = function() {
+        var lines = [];
+        lines.push(
+           "<?xml version='1.0'?>",
+           "<methodCall>",
+           "  <methodName>" + this.methodName + "</methodName>",
+           "  <params>");
+        for (var i = 0; i < this.params.length; i++) {
+            lines.push("    <param>",
+                       XmlRpc.valueToXml(this.params[i], "      "),
+                       "    </param>");
+        }
+        lines.push(
+           "  </params>",
+           "</methodCall>");
+        return lines.join("\n");
+    };
+
+    // XmlRpcClient class:
+    // Object capable of sending XML-RPC calls to an XML-RPC server.
+    // That server will typically reside on the host on which the
+    // javascript is running; it is not likely to reside on the host
+    // which served the javascript.  That means that sandboxing restrictions
+    // will be in effect.  Much of the work done here is therefore to
+    // do the client-side work required to potentially escape the sandbox.
+    // The endpoint parameter, if supplied, is the URL of the XML-RPC server.
+    // If absent, the default SAMP Web Profile server is used.
+    var XmlRpcClient = function(endpoint) {
+        this.endpoint = endpoint ||
+                        "http://localhost:" + WEBSAMP_PORT + WEBSAMP_PATH;
+    };
+
+    // Creates an XHR facade - an object that presents an interface
+    // resembling that of an XMLHttpRequest Level 2.
+    // This facade may be based on an actual XMLHttpRequest Level 2 object
+    // (on browsers that support it), or it may fake one using other
+    // available technology.
+    //
+    // The created facade in any case presents the following interface:
+    //
+    //    open(method, url)
+    //    send(body)
+    //    abort()
+    //    setContentType()
+    //    responseText
+    //    responseXML
+    //    onload
+    //    onerror(err)  - includes timeout; abort is ignored
+    //
+    // See the documentation at http://www.w3.org/TR/XMLHttpRequest/
+    // for semantics.
+    //
+    // XMLHttpRequest Level 2 supports Cross-Origin Resource Sharing (CORS)
+    // which makes sandbox evasion possible.  Faked XHRL2s returned by
+    // this method may use CORS or some other technology to evade the
+    // sandbox.  The SAMP hub itself may selectively allow some of these
+    // technologies and not others, according to configuration.
+    XmlRpcClient.createXHR = function() {
+
+        // Creates an XHR facade based on a genuine XMLHttpRequest Level 2.
+        var XhrL2 = function(xhr) {
+            this.xhr = xhr;
+            xhr.onreadystatechange = (function(l2) {
+                return function() {
+                    if (xhr.readyState !== 4) {
+                        return;
+                    }
+                    else if (!l2.completed) {
+                        if (+xhr.status === 200) {
+                            l2.completed = true;
+                            l2.responseText = xhr.responseText;
+                            l2.responseXML = xhr.responseXML;
+                            if (l2.onload) {
+                                l2.onload();
+                            }
+                        }
+                    }
+                };
+            })(this);
+            xhr.onerror = (function(l2) {
+                return function(event) {
+                    if (!l2.completed) {
+                        l2.completed = true;
+                        if (l2.onerror) {
+                            if (event) {
+                                event.toString = function() {return "No hub?";};
+                            }
+                            else {
+                                event = "No hub?";
+                            }
+                            l2.onerror(event);
+                        }
+                    }
+                };
+            })(this);
+            xhr.ontimeout = (function(l2) {
+                return function(event) {
+                    if (!l2.completed) {
+                        l2.completed = true;
+                        if (l2.onerror) {
+                            l2.onerror("timeout");
+                        }
+                    }
+                };
+            })(this);
+        };
+        XhrL2.prototype.open = function(method, url) {
+            this.xhr.open(method, url);
+        };
+        XhrL2.prototype.send = function(body) {
+            this.xhr.send(body);
+        };
+        XhrL2.prototype.abort = function() {
+            this.xhr.abort();
+        }
+        XhrL2.prototype.setContentType = function(mimeType) {
+            if ("setRequestHeader" in this.xhr) {
+                this.xhr.setRequestHeader("Content-Type", mimeType);
+            }
+        }
+
+        // Creates an XHR facade based on an XDomainRequest (IE8+ only).
+        var XdrL2 = function(xdr) {
+            this.xdr = xdr;
+            xdr.onload = (function(l2) {
+                return function() {
+                    var e;
+                    l2.responseText = xdr.responseText;
+                    if (xdr.contentType === "text/xml" ||
+                        xdr.contentType === "application/xml" ||
+                        /\/x-/.test(xdr.contentType)) {
+                        try {
+                            var xdoc = new ActiveXObject("Microsoft.XMLDOM");
+                            xdoc.loadXML(xdr.responseText);
+                            l2.responseXML = xdoc;
+                        }
+                        catch (e) {
+                            l2.responseXML = e;
+                        }
+                    }
+                    if (l2.onload) {
+                        l2.onload();
+                    }
+                };
+            })(this);
+            xdr.onerror = (function(l2) {
+                return function(event) {
+                    if (l2.onerror) {
+                        l2.onerror(event);
+                    }
+                };
+            })(this);
+            xdr.ontimeout = (function(l2) {
+                return function(event) {
+                    if (l2.onerror) {
+                        l2.onerror(event);
+                    }
+                };
+            })(this);
+        };
+        XdrL2.prototype.open = function(method, url) {
+            this.xdr.open(method, url);
+        };
+        XdrL2.prototype.send = function(body) {
+            this.xdr.send(body);
+        };
+        XdrL2.prototype.abort = function() {
+            this.xdr.abort();
+        };
+        XdrL2.prototype.setContentType = function(mimeType) {
+            // can't do it.
+        };
+
+        // Creates an XHR Facade based on available XMLHttpRequest-type
+        // capabilibities.
+        // If an actual XMLHttpRequest Level 2 is available, use that.
+        if (typeof XMLHttpRequest !== "undefined") {
+            var xhr = new XMLHttpRequest();
+            if ("withCredentials" in xhr) {
+                return new XhrL2(xhr);
+            }
+        }
+
+        // Else if an XDomainRequest is available, use that.
+        if (typeof XDomainRequest !== "undefined") {
+            return new XdrL2(new XDomainRequest());
+        }
+
+        // Else fake an XMLHttpRequest using Flash/flXHR, if available
+        // and use that.
+        if (typeof flensed.flXHR !== "undefined") {
+            return new XhrL2(new flensed.flXHR({instancePooling: true}));
+        }
+
+        // No luck.
+        throw new Error("no cross-origin mechanism available");
+    };
+
+    // Executes a request by passing it to the XML-RPC server.
+    // On success, the result is passed to the resultHandler.
+    // On failure, the errHandler is called with one of two possible
+    // arguments: an XmlRpc.Fault object, or an Error object.
+    XmlRpcClient.prototype.execute = function(req, resultHandler, errHandler) {
+        (function(xClient) {
+            var xhr;
+            var e;
+            try {
+                xhr = XmlRpcClient.createXHR();
+                xhr.open("POST", xClient.endpoint);
+                xhr.setContentType("text/xml");
+            }
+            catch (e) {
+                errHandler(e);
+                throw e;
+            }
+            xhr.onload = function() {
+                var xml = xhr.responseXML;
+                var result;
+                var e;
+                if (xml) {
+                    try {
+                        result = XmlRpc.decodeResponse(xml);
+                    }
+                    catch (e) {
+                        if (errHandler) {
+                            errHandler(e);
+                        }
+                        return;
+                    }
+                }
+                else {
+                    if (errHandler) {
+                        errHandler("no XML response");
+                    }
+                    return;
+                }
+                if (result instanceof XmlRpc.Fault) {
+                    if (errHandler) {
+                        errHandler(result);
+                    }
+                }
+                else {
+                    if (resultHandler) {
+                        resultHandler(result);
+                    }
+                }
+            };
+            xhr.onerror = function(event) {
+                if (event) {
+                    event.toString = function() {return "No hub?";}
+                }
+                else {
+                    event = "No hub";
+                }
+                if (errHandler) {
+                    errHandler(event);
+                }
+            };
+            xhr.send(req.toXml());
+            return xhr;
+        })(this);
+    };
+
+    // Message class:
+    // Aggregates an MType string and a params map.
+    var Message = function(mtype, params) {
+        this["samp.mtype"] = mtype;
+        this["samp.params"] = params;
+    };
+
+    // Connection class:
+    // this is what clients use to communicate with the hub.
+    //
+    // All the methods from the Hub Abstract API as described in the
+    // SAMP standard are available as methods of a Connection object.
+    // The initial private-key argument required by the Web Profile is
+    // handled internally by this object - you do not need to supply it
+    // when calling one of the methods.
+    //
+    // All these calls have the same form:
+    //
+    //    connection.method([method-args], resultHandler, errorHandler)
+    //
+    // the first argument is an array of the arguments (as per the SAMP
+    // abstract hub API), the second argument is a function which is
+    // called on successful completion with the result of the SAMP call
+    // as its argument, and the third argument is a function which is
+    // called on unsuccessful completion with an error object as its
+    // argument.  The resultHandler and errorHandler arguments are optional.
+    //
+    // So for instance if you have a Connection object conn,
+    // you can send a notify message to all other clients by doing, e.g.:
+    //
+    //    conn.notifyAll([new samp.Message(mtype, params)])
+    //
+    // Connection has other methods as well as the hub API ones
+    // as documented below.
+    var Connection = function(regInfo) {
+        this.regInfo = regInfo;
+        this.privateKey = regInfo["samp.private-key"];
+        if (! typeof(this.privateKey) === "string") {
+            throw new Error("Bad registration object");
+        }
+        this.xClient = new XmlRpcClient();
+    };
+    (function() {
+        var connMethods = {
+            call: [TYPE_STRING, TYPE_STRING, TYPE_MAP],
+            callAll: [TYPE_STRING, TYPE_MAP],
+            callAndWait: [TYPE_STRING, TYPE_MAP, TYPE_STRING],
+            declareMetadata: [TYPE_MAP],
+            declareSubscriptions: [TYPE_MAP],
+            getMetadata: [TYPE_STRING],
+            getRegisteredClients: [],
+            getSubscribedClients: [TYPE_STRING],
+            getSubscriptions: [TYPE_STRING],
+            notify: [TYPE_STRING, TYPE_MAP],
+            notifyAll: [TYPE_MAP],
+            ping: [],
+            reply: [TYPE_STRING, TYPE_MAP]
+        };
+        var fn;
+        var types;
+        for (fn in connMethods) {
+            (function(fname, types) {
+                // errHandler may be passed an XmlRpc.Fault or a thrown Error.
+                Connection.prototype[fname] =
+                        function(sampArgs, resultHandler, errHandler) {
+                    var closer =
+                        (function(c) {return function() {c.close()}})(this);
+                    errHandler = errHandler || closer
+                    XmlRpc.checkParams(sampArgs, types);
+                    var request = new XmlRpcRequest(WEBSAMP_PREFIX + fname);
+                    request.addParam(this.privateKey);
+                    request.addParams(sampArgs);
+                    return this.xClient.
+                           execute(request, resultHandler, errHandler);
+                };
+            })(fn, connMethods[fn]);
+        }
+    })();
+    Connection.prototype.unregister = function() {
+        var e;
+        if (this.callbackRequest) {
+            try {
+                this.callbackRequest.abort();
+            }
+            catch (e) {
+            }
+        }
+        var request = new XmlRpcRequest(WEBSAMP_PREFIX + "unregister");
+        request.addParam(this.privateKey);
+        try {
+            this.xClient.execute(request);
+        }
+        catch (e) {
+            // log unregister failed
+        }
+        delete this.regInfo;
+        delete this.privateKey;
+    };
+
+    // Closes this connection.  It unregisters from the hub if still
+    // registered, but may harmlessly be called multiple times.
+    Connection.prototype.close = function() {
+        var e;
+        if (this.closed) {
+            return;
+        }
+        this.closed = true;
+        try {
+            if (this.regInfo) {
+                this.unregister();
+            }
+        }
+        catch (e) {
+        }
+        if (this.onclose) {
+            oc = this.onclose;
+            delete this.onclose;
+            try {
+                oc();
+            }
+            catch (e) {
+            }
+        }
+    };
+
+    // Arranges for this connection to receive callbacks.
+    //
+    // The callableClient argument must be an object implementing the
+    // SAMP callable client API, i.e. it must have the following methods:
+    //
+    //     receiveNotification(string sender-id, map message)
+    //     receiveCall(string sender-id, string msg-id, map message)
+    //     receiveResponse(string responder-id, string msg-tag, map response)
+    // 
+    // The successHandler argument will be called with no arguments if the
+    // allowCallbacks hub method completes successfully - it is a suitable
+    // hook to use for declaring subscriptions.
+    //
+    // The CallableClient class provides a suitable implementation, see below.
+    Connection.prototype.setCallable = function(callableClient,
+                                                successHandler) {
+        var e;
+        if (this.callbackRequest) {
+            try {
+                this.callbackRequest.abort();
+            }
+            catch (e) {
+            }
+            finally {
+                delete this.callbackRequest;
+            }
+        }
+        if (!callableClient && !this.regInfo) {
+            return;
+        }
+        var request =
+            new XmlRpcRequest(WEBSAMP_PREFIX + "allowReverseCallbacks");
+        request.addParam(this.privateKey);
+        request.addParam(callableClient ? "1" : "0");
+        var closer = (function(c) {return function() {c.close()}})(this);
+        if (callableClient) {
+            (function(connection) {
+                var invokeCallback = function(callback) {
+                    var methodName = callback["samp.methodName"];
+                    var methodParams = callback["samp.params"];
+                    var handlerFunc = undefined;
+                    if (methodName === WEBSAMP_CLIENT_PREFIX
+                                     + "receiveNotification") {
+                        handlerFunc = callableClient.receiveNotification;
+                    }
+                    else if (methodName === WEBSAMP_CLIENT_PREFIX
+                                          + "receiveCall") {
+                        handlerFunc = callableClient.receiveCall;
+                    }
+                    else if (methodName === WEBSAMP_CLIENT_PREFIX
+                                          + "receiveResponse") {
+                        handlerFunc = callableClient.receiveResponse;
+                    }
+                    else {
+                        // unknown callback??
+                    }
+                    if (handlerFunc) {
+                        handlerFunc.apply(callableClient, methodParams);
+                    }
+                };
+                var startTime;
+                var resultHandler = function(result) {
+                    if (getSampType(result) != TYPE_LIST) {
+                        errHandler(new Error("pullCallbacks result not List"));
+                        return;
+                    }
+                    var i;
+                    var e;
+                    for (i = 0; i < result.length; i++) {
+                        try {
+                            invokeCallback(result[i]);
+                        }
+                        catch (e) {
+                            // log here?
+                        }
+                    }
+                    callWaiter();
+                };
+                var errHandler = function(error) {
+                    var elapsed = new Date().getTime() - startTime;
+                    if (elapsed < 1000) {
+                        connection.close()
+                    }
+                    else {
+                        // probably a timeout
+                        callWaiter();
+                    }
+                };
+                var callWaiter = function() {
+                    if (!connection.regInfo) {
+                        return;
+                    }
+                    var request =
+                        new XmlRpcRequest(WEBSAMP_PREFIX + "pullCallbacks");
+                    request.addParam(connection.privateKey);
+                    request.addParam("600");
+                    startTime = new Date().getTime();
+                    connection.callbackRequest =
+                        connection.xClient.
+                                   execute(request, resultHandler, errHandler);
+                };
+                var sHandler = function() {
+                    callWaiter();
+                    successHandler();
+                };
+                connection.xClient.execute(request, sHandler, closer);
+            })(this);
+        }
+        else {
+            this.xClient.execute(request, successHandler, closer);
+        }
+    };
+
+    // Takes a public URL and returns a URL that can be used from within
+    // this javascript context.  Some translation may be required, since
+    // a URL sent by an external application may be cross-domain, in which
+    // case browser sandboxing would typically disallow access to it.
+    Connection.prototype.translateUrl = function(url) {
+        var translator = this.regInfo["samp.url-translator"] || "";
+        return translator + url;
+    };
+    Connection.Action = function(actName, actArgs, resultKey) {
+        this.actName = actName;
+        this.actArgs = actArgs;
+        this.resultKey = resultKey;
+    };
+
+    // Suitable implementation for a callable client object which can
+    // be supplied to Connection.setCallable().
+    // Its callHandler and replyHandler members are string->function maps
+    // which can be used to provide handler functions for MTypes and
+    // message tags respectively.
+    //
+    // In more detail:
+    // The callHandler member maps a string representing an MType to
+    // a function with arguments (senderId, message, isCall).
+    // The replyHandler member maps a string representing a message tag to
+    // a function with arguments (responderId, msgTag, response).
+    var CallableClient = function(connection) {
+        this.callHandler = {};
+        this.replyHandler = {};
+    };
+    CallableClient.prototype.init = function(connection) {
+    };
+    CallableClient.prototype.receiveNotification = function(senderId, message) {
+        var mtype = message["samp.mtype"];
+        var handled = false;
+        var e;
+        if (mtype in this.callHandler) {
+            try {
+                this.callHandler[mtype](senderId, message, false);
+            }
+            catch (e) {
+            }
+            handled = true;
+        }
+        return handled;
+    };
+    CallableClient.prototype.receiveCall = function(senderId, msgId, message) {
+        var mtype = message["samp.mtype"];
+        var handled = false;
+        var response;
+        var result;
+        var e;
+        if (mtype in this.callHandler) {
+            try {
+                result = this.callHandler[mtype](senderId, message, true) || {};
+                response = {"samp.status": "samp.ok",
+                            "samp.result": result};
+                handled = true;
+            }
+            catch (e) {
+                response = {"samp.status": "samp.error",
+                            "samp.error": {"samp.errortxt": e.toString()}};
+            }
+        }
+        else {
+            response = {"samp.status": "samp.warning",
+                        "samp.result": {},
+                        "samp.error": {"samp.errortxt": "no action"}};
+        }
+        this.connection.reply([msgId, response]);
+        return handled;
+    };
+    CallableClient.prototype.receiveResponse = function(responderId, msgTag,
+                                                        response) {
+        var handled = false;
+        var e;
+        if (msgTag in this.replyHandler) {
+            try {
+                this.replyHandler[msgTag](responderId, msgTag, response);
+                handled = true;
+            }
+            catch (e) {
+            }
+        }
+        return handled;
+    };
+    CallableClient.prototype.calculateSubscriptions = function() {
+        var subs = {};
+        var mt;
+        for (mt in this.callHandler) {
+            subs[mt] = {};
+        }
+        return subs;
+    };
+
+    // ClientTracker is a CallableClient which also provides tracking of
+    // registered clients.
+    //
+    // Its onchange member, if defined, will be called with arguments
+    // (client-id, change-type, associated-data) whenever the list or
+    // characteristics of registered clients has changed.
+    var ClientTracker = function() {
+        var tracker = this;
+        this.ids = {};
+        this.metas = {};
+        this.subs = {};
+        this.replyHandler = {};
+        this.callHandler = {
+            "samp.hub.event.shutdown": function(senderId, message) {
+                tracker.connection.close();
+            },
+            "samp.hub.disconnect": function(senderId, message) {
+                tracker.connection.close();
+            },
+            "samp.hub.event.register": function(senderId, message) {
+                var id = message["samp.params"]["id"];
+                tracker.ids[id] = true;
+                tracker.changed(id, "register", null);
+            },
+            "samp.hub.event.unregister": function(senderId, message) {
+                var id = message["samp.params"]["id"];
+                delete tracker.ids[id];
+                delete tracker.metas[id];
+                delete tracker.subs[id];
+                tracker.changed(id, "unregister", null);
+            },
+            "samp.hub.event.metadata": function(senderId, message) {
+                var id = message["samp.params"]["id"];
+                var meta = message["samp.params"]["metadata"];
+                tracker.metas[id] = meta;
+                tracker.changed(id, "meta", meta);
+            },
+            "samp.hub.event.subscriptions": function(senderId, message) {
+                var id = message["samp.params"]["id"];
+                var subs = message["samp.params"]["subscriptions"];
+                tracker.subs[id] = subs;
+                tracker.changed(id, "subs", subs);
+            }
+        };
+    };
+    ClientTracker.prototype = heir(CallableClient.prototype);
+    ClientTracker.prototype.changed = function(id, type, data) {
+        if (this.onchange) {
+            this.onchange(id, type, data);
+        }
+    };
+    ClientTracker.prototype.init = function(connection) {
+        var tracker = this;
+        this.connection = connection;
+        var retrieveInfo = function(id, type, infoFuncName, infoArray) {
+            connection[infoFuncName]([id], function(info) {
+                infoArray[id] = info;
+                tracker.changed(id, type, info);
+            });
+        };
+        connection.getRegisteredClients([], function(idlist) {
+            var i;
+            var id;
+            tracker.ids = {};
+            for (i = 0; i < idlist.length; i++) {
+                id = idlist[i];
+                tracker.ids[id] = true;
+                retrieveInfo(id, "meta", "getMetadata", tracker.metas);
+                retrieveInfo(id, "subs", "getSubscriptions", tracker.subs);
+            }
+            tracker.changed(null, "ids", null);
+        });
+    };
+    ClientTracker.prototype.getName = function(id) {
+        var meta = this.metas[id];
+        return (meta && meta["samp.name"]) ? meta["samp.name"] : "[" + id + "]";
+    };
+
+    // Connector class:
+    // A higher level class which can manage transparent hub
+    // registration/unregistration and client tracking.
+    //
+    // On construction, the name argument is mandatory, and corresponds
+    // to the samp.name item submitted at registration time.
+    // The other arguments are optional.
+    // meta is a metadata map (if absent, no metadata is declared)
+    // callableClient is a callable client object for receiving callbacks
+    // (if absent, the client is not callable).
+    // subs is a subscriptions map (if absent, no subscriptions are declared)
+    var Connector = function(name, meta, callableClient, subs) {
+        this.name = name;
+        this.meta = meta;
+        this.callableClient = callableClient;
+        this.subs = subs;
+        this.regTextNodes = [];
+        this.whenRegs = [];
+        this.whenUnregs = [];
+        this.connection = undefined;
+        this.onreg = undefined;
+        this.onunreg = undefined;
+    };
+    var setRegText = function(connector, txt) {
+        var i;
+        var nodes = connector.regTextNodes;
+        var node;
+        for (i = 0; i < nodes.length; i++) {
+            node = nodes[i];
+            node.innerHTML = "";
+            node.appendChild(document.createTextNode(txt));
+        }
+    };
+    Connector.prototype.setConnection = function(conn) {
+        var connector = this;
+        var e;
+        if (this.connection) {
+            this.connection.close();
+            if (this.onunreg) {
+                try {
+                    this.onunreg();
+                }
+                catch (e) {
+                }
+            }
+        }
+        this.connection = conn;
+        if (conn) {
+            conn.onclose = function() {
+                connector.connection = null;
+                if (connector.onunreg) {
+                    try {
+                        connector.onunreg();
+                    }
+                    catch (e) {
+                    }
+                }
+                connector.update();
+            };
+            if (this.meta) {
+                conn.declareMetadata([this.meta]);
+            }
+            if (this.callableClient) {
+                if (this.callableClient.init) {
+                    this.callableClient.init(conn);
+                }
+                conn.setCallable(this.callableClient, function() {
+                    conn.declareSubscriptions([connector.subs]);
+                });
+            }
+            if (this.onreg) {
+                try {
+                    this.onreg(conn);
+                }
+                catch (e) {
+                }
+            }
+        }
+        this.update();
+    };
+    Connector.prototype.register = function() {
+        var connector = this;
+        var regErrHandler = function(err) {
+            setRegText(connector, "no (" + err.toString() + ")");
+        };
+        var regSuccessHandler = function(conn) {
+            connector.setConnection(conn);
+            setRegText(connector, conn ? "Yes" : "No");
+        };
+        register(this.name, regSuccessHandler, regErrHandler);
+    };
+    Connector.prototype.unregister = function() {
+        if (this.connection) {
+            this.connection.unregister([]);
+            this.setConnection(null);
+        }
+    };
+
+    // Returns a document fragment which contains Register/Unregister
+    // buttons for use by the user to attempt to connect/disconnect
+    // with the hub.  This is useful for models where explicit
+    // user registration is encouraged or required, but when using
+    // the register-on-demand model such buttons are not necessary.
+    Connector.prototype.createRegButtons = function() {
+        var connector = this;
+        var regButt = document.createElement("button");
+        regButt.setAttribute("type", "button");
+        regButt.appendChild(document.createTextNode("Register"));
+        regButt.onclick = function() {connector.register();};
+        this.whenUnregs.push(regButt);
+        var unregButt = document.createElement("button");
+        unregButt.setAttribute("type", "button");
+        unregButt.appendChild(document.createTextNode("Unregister"));
+        unregButt.onclick = function() {connector.unregister();};
+        this.whenRegs.push(unregButt);
+        var regText = document.createElement("span");
+        this.regTextNodes.push(regText);
+        var node = document.createDocumentFragment();
+        node.appendChild(regButt);
+        node.appendChild(document.createTextNode(" "));
+        node.appendChild(unregButt);
+        var label = document.createElement("span");
+        label.innerHTML = " <strong>Registered: </strong>";
+        node.appendChild(label);
+        node.appendChild(regText);
+        this.update();
+        return node;
+    };
+
+    Connector.prototype.update = function() {
+        var i;
+        var isConnected = !! this.connection;
+        var enableds = isConnected ? this.whenRegs : this.whenUnregs;
+        var disableds = isConnected ? this.whenUnregs : this.whenRegs;
+        for (i = 0; i < enableds.length; i++) {
+            enableds[i].removeAttribute("disabled");
+        }
+        for (i = 0; i < disableds.length; i++) {
+            disableds[i].setAttribute("disabled", "disabled");
+        }
+        setRegText(this, "No");
+    };
+
+    // Provides execution of a SAMP operation with register-on-demand.
+    // You can use this method to provide lightweight registration/use
+    // of web SAMP.  Simply provide a connHandler function which
+    // does something with a connection (e.g. sends a message) and
+    // Connector.runWithConnection on it.  This will connect if not
+    // already connected, and call the connHandler on with the connection.
+    // No explicit registration action is then required from the user.
+    //
+    // If the regErrorHandler argument is supplied, it is a function of
+    // one (error) argument called in the case that registration-on-demand
+    // fails.
+    //
+    // This is a more-or-less complete sampjs page:
+    //   <script>
+    //     var connector = new samp.Connector("pinger", {"samp.name": "Pinger"})
+    //     var pingFunc = function(connection) {
+    //       connection.notifyAll([new samp.Message("samp.app.ping", {})])
+    //     }
+    //   </script>
+    //   <button onclick="connector.runWithConnection(pingFunc)">Ping</button>
+    Connector.prototype.runWithConnection =
+            function(connHandler, regErrorHandler) {
+        var connector = this;
+        var regSuccessHandler = function(conn) {
+            connector.setConnection(conn);
+            connHandler(conn);
+        };
+        var regFailureHandler = function(e) {
+            connector.setConnection(undefined);
+            regErrorHandler(e);
+        };
+        var pingResultHandler = function(result) {
+            connHandler(connector.connection);
+        };
+        var pingErrorHandler = function(err) {
+            register(this.name, regSuccessHandler, regFailureHandler);
+        };
+        if (this.connection) {
+            // Use getRegisteredClients as the most lightweight check
+            // I can think of that this connection is still OK.
+            // Ping doesn't work because the server replies even if the
+            // private-key is incorrect/invalid.  Is that a bug or not?
+            this.connection.
+                 getRegisteredClients([], pingResultHandler, pingErrorHandler);
+        }
+        else {
+            register(this.name, regSuccessHandler, regFailureHandler);
+        }
+    };
+
+    // Sets up an interval timer to run at intervals and notify a callback
+    // about whether a hub is currently running.
+    // Every millis milliseconds, the supplied availHandler function is
+    // called with a boolean argument: true if a (web profile) hub is
+    // running, false if not.
+    // Returns the interval timer (can be passed to clearInterval()).
+    Connector.prototype.onHubAvailability = function(availHandler, millis) {
+        samp.ping(availHandler);
+
+        // Could use the W3C Page Visibility API to avoid making these
+        // checks when the page is not visible.
+        return setInterval(function() {samp.ping(availHandler);}, millis);
+    };
+
+    // Determines whether a given subscriptions map indicates subscription
+    // to a given mtype.
+    var isSubscribed = function(subs, mtype) {
+        var matching = function(pattern, mtype) {
+            if (pattern == mtype) {
+                return true;
+            }
+            else if (pattern === "*") {
+                return true;
+            }
+            else {
+                var prefix;
+                var split = /^(.*)\.\*$/.exec(pat);
+                if (split) {
+                    prefix = split[1];
+                    if (prefix === mtype.substring(0, prefix.length)) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        };
+        var pat;
+        for (pat in subs) {
+            if (matching(pat, mtype)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // Attempts registration with a SAMP hub.
+    // On success the supplied connectionHandler function is called
+    // with the connection as an argument, on failure the supplied
+    // errorHandler is called with an argument that may be an Error
+    // or an XmlRpc.Fault.
+    var register = function(appName, connectionHandler, errorHandler) {
+        var xClient = new XmlRpcClient();
+        var regRequest = new XmlRpcRequest(WEBSAMP_PREFIX + "register");
+        var securityInfo = {"samp.name": appName};
+        regRequest.addParam(securityInfo);
+        regRequest.checkParams([TYPE_MAP]);
+        var resultHandler = function(result) {
+            var conn;
+            var e;
+            try {
+                conn = new Connection(result);
+            }
+            catch (e) {
+                errorHandler(e);
+                return;
+            }
+            connectionHandler(conn);
+        };
+        xClient.execute(regRequest, resultHandler, errorHandler);
+    };
+
+    // Calls the hub ping method once.  It is not necessary to be
+    // registered to do this.
+    // The supplied pingHandler function is called with a boolean argument:
+    // true if a (web profile) hub is running, false if not.
+    var ping = function(pingHandler) {
+        var xClient = new XmlRpcClient();
+        var pingRequest = new XmlRpcRequest(WEBSAMP_PREFIX + "ping");
+        var resultHandler = function(result) {
+            pingHandler(true);
+        };
+        var errorHandler = function(error) {
+            pingHandler(false);
+        };
+        xClient.execute(pingRequest, resultHandler, errorHandler);
+    };
+
+
+    /* Exports. */
+    var jss = {};
+    jss.XmlRpcRequest = XmlRpcRequest;
+    jss.XmlRpcClient = XmlRpcClient;
+    jss.Message = Message;
+    jss.TYPE_STRING = TYPE_STRING;
+    jss.TYPE_LIST = TYPE_LIST;
+    jss.TYPE_MAP = TYPE_MAP;
+    jss.register = register;
+    jss.ping = ping;
+    jss.isSubscribed = isSubscribed;
+    jss.Connector = Connector;
+    jss.CallableClient = CallableClient;
+    jss.ClientTracker = ClientTracker;
+
+    return jss;
+})();
diff --git a/public/assets/js/votable.js b/public/assets/js/votable.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2e225acfda8bda53bbd94c7a3d0955b8056b7ec
--- /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 5b9b5211a1e345465ce2e6b7c0c47cda85f5ed79..071a9961a07f690b7432e7c3a03b569729d8781c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -28,6 +28,13 @@
     <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>
+
+    <script type="text/javascript" src="https://uilennest.net/repository/samp.js"></script>
+    <script type="text/javascript" src="https://uilennest.net/repository/votable.js"></script>
+
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
diff --git a/src/App.js b/src/App.js
index c462317b967f19164906133fc16492611255fa21..f78dae5507f7caba0e6516e8c63ca581f1d197e7 100644
--- a/src/App.js
+++ b/src/App.js
@@ -3,6 +3,7 @@ import "./App.css";
 import Routes from "./routes/Routes";
 import { GlobalContextProvider } from "./contexts/GlobalContext";
 import { QueryContextProvider } from "./contexts/QueryContext";
+import { IDAContextProvider } from "./contexts/IDAContext";
 
 // This is the App, only application global stuff goes here, like the global state provider.
 
@@ -11,7 +12,9 @@ export default function App() {
     <div>
       <GlobalContextProvider>
         <QueryContextProvider>
-          <Routes />
+          <IDAContextProvider>
+            <Routes />
+          </IDAContextProvider>
         </QueryContextProvider>
       </GlobalContextProvider>
     </div>
diff --git a/src/components/Interactive.js b/src/components/Interactive.js
index a82c57bf3a78de829c099fcefbc2b9de5d93303e..9d3a38b385bd28dddc1db049152c1fa5855260e2 100644
--- a/src/components/Interactive.js
+++ b/src/components/Interactive.js
@@ -1,24 +1,68 @@
-import React from "react";
-import { Alert } from "react-bootstrap";
+import React, { useContext } from "react";
+import { Button, Form, Container, Alert } from "react-bootstrap";
+import { IDAContext } from "../contexts/IDAContext";
 
 export default function Interactive() {
+  const { jhubURL, setJhubURL, jnotebookURL, setJnotebookURL, batchsystemsURL, setBatchsystemsURL } = useContext(IDAContext);
+
+  let list_of_jnotebooks = [
+    {"name" : "CSIC-IAA HCG-16 workflow", "url" : "https://mybinder.org/v2/gh/AMIGA-IAA/hcg-16/master"},
+    {"name" : "CDS MOCPy", "url" : "https://mybinder.org/v2/gh/cds-astro/mocpy/master"},
+    {"name" : "JIVE Jupyter CASA", "url": "https://mybinder.org/v2/gh/aardk/jupyter-casa/master"},
+    {"name" : "ASTRON VO Apertif", "url" : "https://mybinder.org/v2/gh/zhengmeyer/first-binder.git/master"},]
+
+  let list_of_jhubs = [
+    {"name" : "SKAO JupyterHub", "url" : "https://srcdev.skatelescope.org/escape"},
+    {"name" : "ASTRON JupyterHub", "url" : "https://sdc.astron.nl/hub/"},
+    {"name" : "IFAE-PIC JupyterHub", "url" : "https://jupyter.pic.es" },
+    {"name" : "CERN SWAN Service", "url" : "https://swan.cern.ch/"}]
+
+  let list_of_batchsystems = [
+    {"name" : "DIRAC EGI (LOFAR, KM3Net)", "url" : "https://dirac.egi.eu"},
+    {"name" : "CTA DIRAC", "url" : "https://ccdcta-web.in2p3.fr/DIRAC/"},
+  ]
+  
   return (
-    // <div class="embed-responsive embed-responsive-16by9">
-    //   <iframe
-    //     class="embed-responsive-item"
-    //     src="http://130.246.212.44/escape/"
-    //     allowfullscreen
-    //   ></iframe>
-    // </div>
-    <Alert variant="warning">
-      <p>You will leave ESAP GUI and be redirected to</p>
-      <a
-        target="_blank"
-        rel="noopener noreferrer"
-        href="https://srcdev.skatelescope.org/escape"
-      >
-        Interactive analysis platform hosted by SKAO
-      </a>
-    </Alert>
+    <Container fluid>
+    <Form className="mt-5">
+      <Form.Group controlId="jnotebook" onChange={
+        (event) => setJnotebookURL(list_of_jnotebooks.find((item) => item.name === event.target.value).url)
+        }>
+        <Form.Label>
+            <h3>Run ESCAPE ESFRI Jupyter Notebooks</h3>
+        </Form.Label>
+        <Form.Control className="mt-1" as="select">
+          {list_of_jnotebooks.map((option) => <option>{option.name}</option>)}
+        </Form.Control>
+      </Form.Group>
+      <Button href={jnotebookURL} target="_blank">Run selected notebook</Button>
+    </Form>
+    <Form className="mt-5">
+      <Form.Group controlId="jhub" onChange={
+        (event) => setJhubURL(list_of_jhubs.find((item) => item.name === event.target.value).url)
+        }>
+        <Form.Label>
+          <h3>Select ESCAPE JupyterHub Services</h3>
+        </Form.Label>
+        <Form.Control className="mt-1" as="select">
+          {list_of_jhubs.map((option) => <option>{option.name}</option>)}
+        </Form.Control>
+      </Form.Group>
+      <Button href={jhubURL} target="_blank">Launch JupyterHub</Button>
+    </Form>
+    <Form className="mt-5">
+      <Form.Group controlId="batchsystems" onChange={
+        (event) => setBatchsystemsURL(list_of_batchsystems.find((item) => item.name === event.target.value).url)
+        }>
+        <Form.Label>
+          <h3>Select HPC/HTC Services</h3>
+        </Form.Label>
+        <Form.Control className="mt-1" as="select">
+          {list_of_batchsystems.map((option) => <option>{option.name}</option>)}
+        </Form.Control>
+      </Form.Group>
+      <Button href={batchsystemsURL} target="_blank">Start HPC/HTC service</Button>
+    </Form>
+  </Container>
   );
 }
diff --git a/src/components/NavBar.js b/src/components/NavBar.js
index 10b442c39a2dc383051ac606a01c60382e16cb99..35e23b07845771a4001d38f6f46bbc9eaf318506 100644
--- a/src/components/NavBar.js
+++ b/src/components/NavBar.js
@@ -2,14 +2,16 @@ import React, { useContext } from "react";
 import { Navbar, Nav } from "react-bootstrap";
 import { NavLink } from "react-router-dom";
 import AuthControl from "./auth/authControl";
-
 import { QueryContext } from "../contexts/QueryContext";
+import { GlobalContext } from "../contexts/GlobalContext";
 
 export default function NavBar() {
+  const { navbar } = useContext(GlobalContext);
   const { config } = useContext(QueryContext);
+  if (!navbar) return null;
   if (!config) return null;
   // construct the navigation bar based on the configuration
-  const navlist = config.navbar;
+  const navlist = navbar.navbar;
 
   return (
     <Navbar bg="dark" variant="dark">
diff --git a/src/components/auth/authControl.js b/src/components/auth/authControl.js
index b6756bbf7e69fae54e29ff0cae2c43379776c96b..ebf8aaecea2cb684d4ec910a62db98ccc9de1bc3 100644
--- a/src/components/auth/authControl.js
+++ b/src/components/auth/authControl.js
@@ -4,14 +4,14 @@ import { Nav } from "react-bootstrap";
 import { GlobalContext } from "../../contexts/GlobalContext";
 
 export default function AuthControl() {
-  const { api_host, isAuthenticated } = useContext(GlobalContext);
+  const { api_host, isAuthenticated, loggedInUserName } = useContext(GlobalContext);
 
   console.log("loggedIn: ", isAuthenticated);
 
   if (isAuthenticated) {
     return (
       <Nav.Link as={NavLink} to="/logout">
-        Logout
+        {`Logout ${loggedInUserName}`}
       </Nav.Link>
     );
   }
diff --git a/src/components/query/ASTRONVOResults.js b/src/components/query/ASTRONVOResults.js
index 76937c2d05845f7bc1f3bd89f15e83578a2d4005..c52549672f77dc93ed8033029a7258c1b63889a3 100644
--- a/src/components/query/ASTRONVOResults.js
+++ b/src/components/query/ASTRONVOResults.js
@@ -1,13 +1,47 @@
-import React, { useContext } from "react";
+import React, { useContext, useState, useEffect } from "react";
 import { Table, Alert } from "react-bootstrap";
+import axios from "axios";
 import { QueryContext } from "../../contexts/QueryContext";
+import { GlobalContext } from "../../contexts/GlobalContext";
 import LoadingSpinner from "../LoadingSpinner";
 import Paginate from "../Paginate";
 import HandlePreview from "./HandlePreview";
 import Preview from "./Preview";
 
 export default function ASTRONVOResults({ catalog }) {
-  const { queryMap, page, setPage, preview } = useContext(QueryContext);
+  const { queryMap, preview } = useContext(QueryContext);
+  const { api_host } = useContext(GlobalContext);
+  const [page, setPage] = useState(queryMap.get(catalog).page);
+
+  useEffect(() => {
+    queryMap.set(catalog, {
+      catalog: catalog,
+      page: page,
+      esapquery: queryMap.get(catalog).esapquery + `&page=${page}`,
+    });
+    const url = api_host + "query/query/?" + queryMap.get(catalog).esapquery;
+    axios
+      .get(url)
+      .then((queryResponse) => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "fetched",
+          results: queryResponse.data,
+        });
+      })
+      .catch(() => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "error",
+          results: null,
+        });
+      });
+  }, [page])
+
   if (!queryMap) return null;
   if (queryMap.get(catalog).status === "fetched") {
     if (!("results" in queryMap.get(catalog).results))
diff --git a/src/components/query/ApertifResults.js b/src/components/query/ApertifResults.js
index 15b35243b35cd29cc5d0dd3d4439504bcd2481ac..e5b08f4f090fff002a60f5eb603c140180e056ba 100644
--- a/src/components/query/ApertifResults.js
+++ b/src/components/query/ApertifResults.js
@@ -1,13 +1,47 @@
-import React, { useContext } from "react";
+import React, { useContext, useState, useEffect } from "react";
 import { Table, Alert } from "react-bootstrap";
+import axios from "axios";
 import { QueryContext } from "../../contexts/QueryContext";
+import { GlobalContext } from "../../contexts/GlobalContext";
 import LoadingSpinner from "../LoadingSpinner";
 import Paginate from "../Paginate";
 import HandlePreview from "./HandlePreview";
 import Preview from "./Preview";
 
 export default function ApertifResults({ catalog }) {
-  const { queryMap, page, setPage, preview } = useContext(QueryContext);
+  const { queryMap, preview } = useContext(QueryContext);
+  const { api_host } = useContext(GlobalContext);
+  const [page, setPage] = useState(queryMap.get(catalog).page);
+
+  useEffect(() => {
+    queryMap.set(catalog, {
+      catalog: catalog,
+      page: page,
+      esapquery: queryMap.get(catalog).esapquery + `&page=${page}`,
+    });
+    const url = api_host + "query/query/?" + queryMap.get(catalog).esapquery;
+    axios
+      .get(url)
+      .then((queryResponse) => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "fetched",
+          results: queryResponse.data,
+        });
+      })
+      .catch(() => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "error",
+          results: null,
+        });
+      });
+  }, [page])
+
   if (!queryMap) return null;
   if (queryMap.get(catalog).status === "fetched") {
     if (!("results" in queryMap.get(catalog).results))
@@ -21,9 +55,9 @@ export default function ApertifResults({ catalog }) {
       <>
         <Paginate
           getNewPage={(args) => {
-            return args.target ? setPage(parseFloat(args.target.text)) : null;
+            return args.target ? setPage(parseInt(args.target.text)) : null;
           }}
-          currentPage={page}
+          currentPage={queryMap.get(catalog).page}
           numAdjacent={3}
           numPages={numPages}
         />
diff --git a/src/components/query/HandlePreview.js b/src/components/query/HandlePreview.js
index 04946bb3d282af85c5a94978c78df55ea596038b..be37a2b0f5d9aff1195fb26db726f19626b5af2d 100644
--- a/src/components/query/HandlePreview.js
+++ b/src/components/query/HandlePreview.js
@@ -20,7 +20,7 @@ export default function HandlePreview({ result }) {
                 (result.thumbnail && (
                     <Button
                         onClick={()=>{
-                            preview ? setPreview("") : setPreview(result.url);
+                            setPreview(result.url);
                             setURL(result.thumbnail);
                         }}
                     >
diff --git a/src/components/query/LOFARResults.js b/src/components/query/LOFARResults.js
index 0aaf6bd37cf93b60bf9d9fd277993681a2dfd05f..54a4ce05d3b9939e01ab52c03bdd28b85dc0f761 100644
--- a/src/components/query/LOFARResults.js
+++ b/src/components/query/LOFARResults.js
@@ -1,10 +1,44 @@
-import React, { useContext } from "react";
+import React, { useContext, useState, useEffect } from "react";
 import { Table, Alert } from "react-bootstrap";
+import axios from "axios";
 import { QueryContext } from "../../contexts/QueryContext";
+import { GlobalContext } from "../../contexts/GlobalContext";
 import LoadingSpinner from "../LoadingSpinner";
 
 export default function LOFARResults({ catalog }) {
   const { queryMap } = useContext(QueryContext);
+  const { api_host } = useContext(GlobalContext);
+  const [page, setPage] = useState(queryMap.get(catalog).page);
+
+  useEffect(() => {
+    queryMap.set(catalog, {
+      catalog: catalog,
+      page: page,
+      esapquery: queryMap.get(catalog).esapquery + `&page=${page}`,
+    });
+    const url = api_host + "query/query/?" + queryMap.get(catalog).esapquery;
+    axios
+      .get(url)
+      .then((queryResponse) => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "fetched",
+          results: queryResponse.data,
+        });
+      })
+      .catch(() => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "error",
+          results: null,
+        });
+      });
+  }, [page])
+  
   if (!queryMap) return null;
   if (queryMap.get(catalog).status === "fetched") {
     if (!("results" in queryMap.get(catalog).results))
diff --git a/src/components/query/QueryCatalogs.js b/src/components/query/QueryCatalogs.js
index 4fa5fc8732722480e6d356978f272e6fc55a5109..574cd74ba209ba39199eadc6c490d824e2b80536 100644
--- a/src/components/query/QueryCatalogs.js
+++ b/src/components/query/QueryCatalogs.js
@@ -15,10 +15,8 @@ export default function QueryCatalogs() {
   //  "catalogquery": "querystring",
   //  "status": "fetching|fechted",
   //  "results": null}
-  const { config, setConfigName, defaultConf, queryMap, formData, setFormData, page } = useContext(QueryContext);
-  const { api_host } = useContext(
-    GlobalContext
-  );
+  const { config, setConfigName, defaultConf, queryMap, formData, setFormData } = useContext(QueryContext);
+  const { api_host } = useContext(GlobalContext);
   const { uri } = useParams();
   console.log("uri:", uri);
   console.log("default conf:", defaultConf);
@@ -58,16 +56,22 @@ export default function QueryCatalogs() {
   useEffect(() => {
     console.log(config.query_schema);
     if (!formData) return;
-    const gui = config.query_schema.name;
-    const queries = parseQueryForm(gui, formData, page);
-
-    // Ideally query for each catalog is sent to ESAP API Gateway, and query results is returned
-    // This is under development in the backend at the moment
     queryMap.clear();
+    const gui = config.query_schema.name;
+    const queries = parseQueryForm(gui, formData);
     queries.forEach((query) => {
       queryMap.set(query.catalog, {
         catalog: query.catalog,
+        page: 1,
         esapquery: query.esapquery,
+      });
+    });
+
+    queryMap.forEach((query) => {
+      queryMap.set(query.catalog, {
+        catalog: query.catalog,
+        esapquery: query.esapquery,
+        page: query.page,
         status: "fetching",
         results: null,
       });
@@ -78,6 +82,7 @@ export default function QueryCatalogs() {
           queryMap.set(query.catalog, {
             catalog: query.catalog,
             esapquery: query.esapquery,
+            page: query.page,
             status: "fetched",
             results: queryResponse.data,
           });
@@ -86,12 +91,13 @@ export default function QueryCatalogs() {
           queryMap.set(query.catalog, {
             catalog: query.catalog,
             esapquery: query.esapquery,
+            page: query.page,
             status: "error",
             results: null,
           });
         });
     });
-  }, [formData, page]);
+  }, [formData]);
 
   function formTemplate({ TitleField, properties, title, description }) {
     return (
@@ -114,7 +120,16 @@ export default function QueryCatalogs() {
 
   console.log("queryMap", Array.from(queryMap.values()));
 
-  const uiSchemaProp = config.ui_schema ? { uiSchema: config.ui_schema } : {};
+  function renderUIschema(config) {
+    switch(config.query_schema.name) {
+      case 'apertif':
+        return config.ui_schema ? { uiSchema: config.ui_schema } : {} ;
+        // renderApertifUIschema(config.ui_schema);
+      default:
+        return config.ui_schema ? { uiSchema: config.ui_schema } : {}
+    }
+  }
+  const uiSchemaProp = renderUIschema(config);
   return (
     <Container fluid>
       <Form
diff --git a/src/components/query/QueryIVOARegistry.js b/src/components/query/QueryIVOARegistry.js
index 677b69c452fd27f2ecfb61d55aa0643647513cc4..ad404fdf1299b4e2b1f2c332eee2d98da83a0a77 100644
--- a/src/components/query/QueryIVOARegistry.js
+++ b/src/components/query/QueryIVOARegistry.js
@@ -17,11 +17,11 @@ export default function QueryIVOARegistry() {
   //  "catalogquery": "querystring",
   //  "status": "fetching|fechted",
   //  "results": null}
-  const { config, setConfigName, defaultConf, queryMap, formData, setFormData, page } = useContext(QueryContext);
+  const { config, setConfigName, defaultConf, queryMap, formData, setFormData, page, setPage } = useContext(QueryContext);
   const { api_host } = useContext(
     GlobalContext
   );
-  const { selectedRegistry, queryStep, setQueryStep, regPage } = useContext(
+  const { selectedRegistry, setSelectedRegistry, queryStep, setQueryStep, regPage } = useContext(
     IVOAContext
   );
   const { uri } = useParams();
@@ -50,7 +50,9 @@ export default function QueryIVOARegistry() {
         setConfigName("esap_ivoa");
     }
     return () => {
-      console.log("Set configuration back to default!");
+      console.log("cleaned up");
+      queryMap.clear();
+      setFormData();
       setConfigName(defaultConf);
     };
   }, [uri]);
diff --git a/src/components/query/ZooniverseResults.js b/src/components/query/ZooniverseResults.js
index aef7c46823e3d2cb49b457924272817ba37033a0..4b80c522cb975d50d61e34ad9cdf29ea822f81b3 100644
--- a/src/components/query/ZooniverseResults.js
+++ b/src/components/query/ZooniverseResults.js
@@ -1,6 +1,7 @@
-import React, { useContext, useState } from "react";
-import { Table, Alert, Form, Button } from "react-bootstrap";
-import * as deepEqual from "deep-equal";
+import React, { useContext, useEffect } from "react";
+import { Table, Alert, Form } from "react-bootstrap";
+import axios from "axios";
+import { GlobalContext } from "../../contexts/GlobalContext";
 import { QueryContext } from "../../contexts/QueryContext";
 import { BasketContext } from "../../contexts/BasketContext";
 import LoadingSpinner from "../LoadingSpinner";
@@ -335,6 +336,38 @@ function ZooniverseWorkflowResults(context) {
 export default function ZooniverseResults({ catalog }) {
   const context = useContext(QueryContext);
   const basketContext = useContext(BasketContext);
+
+  const { queryMap, page } = useContext(QueryContext);
+  const { api_host } = useContext(GlobalContext);
+  useEffect(() => {
+    queryMap.set(catalog, {
+      catalog: catalog,
+      page: page,
+      esapquery: queryMap.get(catalog).esapquery + `&page=${page}`,
+    });
+    const url = api_host + "query/query/?" + queryMap.get(catalog).esapquery;
+    axios
+      .get(url)
+      .then((queryResponse) => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "fetched",
+          results: queryResponse.data,
+        });
+      })
+      .catch(() => {
+        queryMap.set(catalog, {
+          catalog: catalog,
+          esapquery: queryMap.get(catalog).esapquery,
+          page: page,
+          status: "error",
+          results: null,
+        });
+      });
+  }, [page])
+
   if (!context.queryMap) return null;
   if (context.queryMap.get(catalog).status === "fetched") {
     if (context.queryMap.get(catalog).results.results.length === 0)
diff --git a/src/components/query/samp/ReactVOTable.js b/src/components/query/samp/ReactVOTable.js
new file mode 100644
index 0000000000000000000000000000000000000000..768ee9c25fbaa3a292e2f17f3b6a98b86399355e
--- /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 0000000000000000000000000000000000000000..81ef70264a49de2e7da46bc84b5bfe828042c9bf
--- /dev/null
+++ b/src/components/query/samp/SampGrid.js
@@ -0,0 +1,47 @@
+import React from "react";
+import { Table } from "react-bootstrap";
+
+export default function SampResults(props) {
+
+    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) => {
+                                let value = col.toString()
+
+                                if (value.includes('http')) {
+                                    value = <a href={value} target="_blank" rel="noopener noreferrer">{value}</a>
+                                }
+                                return (
+                                    <td key={value}>{value}</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 0000000000000000000000000000000000000000..01803cff27643788d758929151b318239d3ab5a9
--- /dev/null
+++ b/src/components/query/samp/SampPage.js
@@ -0,0 +1,96 @@
+import React, {useState }  from 'react';
+
+import { getVOTableAsJSON } from './ReactVOTable'
+import SampGrid from './SampGrid'
+
+export default function SampPage(props) {
+    const [ myVOTable, setMyVOTable] = useState([]);
+
+    // register to existing SAMP hub
+    const register = () => {
+        connector.register()
+    }
+
+    // unregister from existing SAMP hub
+    const unregister = () => {
+        connector.unregister()
+    }
+
+
+    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["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("ESAP", {"samp.name": "ESAP"}, 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-warning" onClick={() => unregister()}>unregister</button>&nbsp;
+
+                {renderSampGrid}
+            </div>
+        </div>
+    );
+}
\ No newline at end of file
diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/contexts/GlobalContext.js b/src/contexts/GlobalContext.js
index f0c962c7164273ad6c41e5d16424ccd4439bea85..4c6e27721b82edb0ad6a8dcdddacb1448f94be20 100644
--- a/src/contexts/GlobalContext.js
+++ b/src/contexts/GlobalContext.js
@@ -5,23 +5,43 @@ import getCookie from "../utils/getCookie";
 
 export const GlobalContext = createContext();
 
+function getUserName(api_host, setLoggedInUserName){
+  const profileUrl = api_host + "accounts/user-profiles/";
+  axios
+    .get(profileUrl, {withCredentials: true})
+    .then((response) => {
+      setLoggedInUserName(response.data.results[0].full_name);
+    })
+}
+
 export function GlobalContextProvider({ children }) {
   
-  console.log("ASTRON ESAP version 19 nov 2020 - 16:00");
   const api_host =
     process.env.NODE_ENV === "development"
-      ? "https://sdc.astron.nl:5555/esap-api/"
-      : "https://sdc.astron.nl:5555/esap-api/";
+      ? "http://localhost:8000/esap-api/"
+      : "https://sdc-dev.astron.nl:5555/esap-api/";
   // "https://sdc.astron.nl:5555/esap-api/"
   // "http://localhost:5555/esap-api/"
 
   const [archives, setArchives] = useState();
+  const [navbar, setNavbar] = useState();
+  const [loggedInUserName, setLoggedInUserName] = useState();
+
   useEffect(() => {
     axios
       .get(api_host + "query/archives-uri")
       .then((response) => setArchives(response.data.results));
   }, [api_host]);
 
+  useEffect(() => {
+    axios
+    .get(api_host + "query/configuration?name=navbar")
+      .then((response) => {
+        console.log("navbar response", response.data.configuration);
+        setNavbar(response.data.configuration);
+      });
+  }, [api_host]);
+
   // !!!!! Still need to look at sessionid and stuff
   const [sessionid, setSessionid] = useState(getCookie("sessionid"));
   console.log("waah", sessionid, getCookie("sessionid"), document.cookie);
@@ -33,6 +53,7 @@ export function GlobalContextProvider({ children }) {
     setIsAuthenticated(true);
     setSessionid(getCookie("sessionid"));
     history.replace("/");
+    getUserName(api_host, setLoggedInUserName);
     return null;
   };
 
@@ -40,6 +61,7 @@ export function GlobalContextProvider({ children }) {
     setIsAuthenticated(false);
     setSessionid(null);
     history.replace("/");
+    setLoggedInUserName("");
     return null;
   };
 
@@ -63,9 +85,11 @@ export function GlobalContextProvider({ children }) {
         isAuthenticated,
         sessionid,
         archives,
+        navbar,
         handleLogin,
         handleLogout,
         handleError,
+        loggedInUserName,
       }}
     >
       {children}
diff --git a/src/contexts/IDAContext.js b/src/contexts/IDAContext.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd250bf852ba41c37edd2db084e76ba0204e5fa7
--- /dev/null
+++ b/src/contexts/IDAContext.js
@@ -0,0 +1,22 @@
+import React, { useState, createContext } from 'react';
+
+export const IDAContext = createContext();
+export function IDAContextProvider({ children }) {
+    const [jhubURL, setJhubURL] = useState("https://srcdev.skatelescope.org/escape");
+    const [jnotebookURL, setJnotebookURL] = useState("https://mybinder.org/v2/gh/AMIGA-IAA/hcg-16/master");
+    const [batchsystemsURL, setBatchsystemsURL] = useState("https://dirac.egi.eu");
+    return (
+        <IDAContext.Provider
+            value={{
+                jhubURL,
+                setJhubURL,
+                jnotebookURL,
+                setJnotebookURL,
+                batchsystemsURL, 
+                setBatchsystemsURL,
+            }}
+        >
+        {children}
+        </IDAContext.Provider>
+    )
+}
\ No newline at end of file
diff --git a/src/contexts/IVOAContext.js b/src/contexts/IVOAContext.js
index e3a755312e9a4905448677479f54ea5cbb3a7993..0e3bdb72d9df67861b8fbe4dee2deb67912a5f8e 100644
--- a/src/contexts/IVOAContext.js
+++ b/src/contexts/IVOAContext.js
@@ -39,6 +39,7 @@ export function IVOAContextProvider({ children }) {
     <IVOAContext.Provider
       value={{
         selectedRegistry,
+        setSelectedRegistry,
         addRegistry: handleAddRegistry,
         removeRegistry: handleRemoveRegistry,
         regPage,
diff --git a/src/routes/Routes.js b/src/routes/Routes.js
index b70b60d98bdb5dad03f40c9bcaa7260bd923fcce..6f02266857301572581fa35bdd5957ad68574afb 100644
--- a/src/routes/Routes.js
+++ b/src/routes/Routes.js
@@ -1,5 +1,5 @@
 import React, { useContext } from "react";
-import { Switch, Route } from "react-router-dom";
+import { Switch, Route, Redirect } from "react-router-dom";
 import { Archives } from "../components/archives/Archives";
 import ArchiveDetails from "../components/archives/ArchiveDetails";
 import { GlobalContext } from "../contexts/GlobalContext";
@@ -7,18 +7,21 @@ import QueryCatalogs from "../components/query/QueryCatalogs";
 import QueryIVOARegistry from "../components/query/QueryIVOARegistry";
 import { BrowserRouter as Router } from "react-router-dom";
 import NavBar from "../components/NavBar";
-import { QueryContext } from "../contexts/QueryContext";
 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 { handleLogin, handleLogout, handleError } = useContext(GlobalContext);
-  const { config } = useContext(QueryContext);
-  if (!config) return null;
+  const { navbar, handleLogin, handleLogout, handleError } = useContext(GlobalContext);
+  const { jhubURL } = useContext(IDAContext);
+  if (!navbar) return null;
+  if (!jhubURL) return null;
 
   return (
-    <Router basename={config.frontend_basename}>
+    <Router basename={navbar.frontend_basename}>
       <NavBar />
       <Switch>
         <Route exact path={["/", "/archives"]}>
@@ -30,11 +33,15 @@ export default function Routes() {
         <Route exact path="/interactive">
           <Interactive />
         </Route>
+        <Route exact path="/vo-query">
+          <Redirect to="/archives/ivoa/query" />
+        </Route>
+        <Route exact path="/jhub" render={() => (window.location = {jhubURL})} />
         <Route exact path="/login" component={handleLogin} />
         <Route exact path="/logout" component={handleLogout} />
         <Route exact path="/error" component={handleError} />
         <Route exact path="/archives/:uri" component={ArchiveDetails} />
-        <Route exact path={["/vo-query", "/archives/ivoa/query"]}>
+        <Route exact path="/archives/ivoa/query">
           <IVOAContextProvider>
             <QueryIVOARegistry />
           </IVOAContextProvider>
@@ -42,7 +49,9 @@ export default function Routes() {
         <Route exact path={["/adex-query", "/archives/:uri/query"]}>
           <QueryCatalogs />
         </Route>
+        <Route exact path="/samp" component={SampPage} />
       </Switch>
+      <footer><small>esap-gui version 23 feb 2021 15:00</small></footer>
     </Router>
   );
 }
diff --git a/src/utils/form/parseADEXForm.js b/src/utils/form/parseADEXForm.js
index 75f134d0e1ae7407460128178484854c52e4ae1d..27bfa31c28a333d38b6e6c550026476fb643eb56 100644
--- a/src/utils/form/parseADEXForm.js
+++ b/src/utils/form/parseADEXForm.js
@@ -1,4 +1,4 @@
-export default function ParseADEXForm(formData, page) {
+export default function ParseADEXForm(formData) {
   let catalogs = ["apertif", "astron_vo", "lofar"];
   let queries = [];
   // queries is an array of dictionaries, where each dictionary consists of
@@ -39,8 +39,7 @@ export default function ParseADEXForm(formData, page) {
     let esapquery =
       query +
       `${`${query}` ? "&" : ""}archive_uri=` +
-      catalog +
-      `&page_size=30&page=${page}`;
+      catalog + `&page_size=30`;
 
     queries.push({
       catalog: catalog,
diff --git a/src/utils/form/parseASTRONVOForm.js b/src/utils/form/parseASTRONVOForm.js
index 6406cf850f4d9ce0453210e69e1bef7cb9ff5269..9162c828a9635cabe750e5dbbcccfd933dc8ea4c 100644
--- a/src/utils/form/parseASTRONVOForm.js
+++ b/src/utils/form/parseASTRONVOForm.js
@@ -1,4 +1,4 @@
-export default function ParseASTRONVOForm(formData, page) {
+export default function ParseASTRONVOForm(formData) {
   let queries = [];
   // queries is an array of dictionaries, where each dictionary consists of
   // {"catalog": "catalogname",
@@ -19,9 +19,7 @@ export default function ParseASTRONVOForm(formData, page) {
   //  "status": "null|fetching|fetched",
   //  "results": null}
   let catalog = formInput.find(([key]) => key === "catalog")[1];
-  let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog; //+ `&page=${page}`
-  // testing api with page=1, failing at the backend at the moment
-  page === 1 ? console.log("Page number is 1") : (esapquery += `&page=${page}`);
+  let esapquery = query + `${`${query}` ? "&" : ""}archive_uri=` + catalog;
   queries.push({
     catalog: catalog,
     esapquery: esapquery,
diff --git a/src/utils/form/parseApertifForm.js b/src/utils/form/parseApertifForm.js
index 6aab904362942e15d814bdf12d9ca3f12d7b244e..4b43e2816a9cd5e54cff0727a8a5ab061a80f421 100644
--- a/src/utils/form/parseApertifForm.js
+++ b/src/utils/form/parseApertifForm.js
@@ -1,4 +1,4 @@
-export default function ParseApertifForm(formData, page) {
+export default function ParseApertifForm(formData) {
   let queries = [];
   // queries is an array of dictionaries, where each dictionary consists of
   // {"catalog": "catalogname",
@@ -20,7 +20,7 @@ export default function ParseApertifForm(formData, page) {
   //  "results": null}
   let catalog = formInput.find(([key]) => key === "catalog")[1];
   let esapquery =
-    query + `${`${query}` ? "&" : ""}archive_uri=` + catalog + `&page=${page}`;
+    query + `${`${query}` ? "&" : ""}archive_uri=` + catalog;
   queries.push({
     catalog: catalog,
     esapquery: esapquery,
diff --git a/src/utils/form/parseIVOAForm.js b/src/utils/form/parseIVOAForm.js
index 2b3fadcf898d051b1ed7b733e549fd25e049fe1b..b9aa15cb545dd507a4806c0d70f56504e8d8718f 100644
--- a/src/utils/form/parseIVOAForm.js
+++ b/src/utils/form/parseIVOAForm.js
@@ -1,4 +1,4 @@
-export default function ParseIVOAForm(formData, page) {
+export default function ParseIVOAForm(formData) {
   let queries = [];
   // queries is an array of dictionaries, where each dictionary consists of
   // {"catalog": "catalogname",
@@ -28,8 +28,7 @@ export default function ParseIVOAForm(formData, page) {
     "get-services/?" +
     query +
     `${`${query}` ? "&" : ""}dataset_uri=` +
-    catalog +
-    `&page=${page}`;
+    catalog;
 
   queries.push({
     catalog: catalog,
diff --git a/src/utils/form/parseLOFARForm.js b/src/utils/form/parseLOFARForm.js
index 8f8ff91cf9c6cd1e297449c00b2e41702ed20a51..fb06e3d302fe9edfc571b2797167ef8f220cbbb8 100644
--- a/src/utils/form/parseLOFARForm.js
+++ b/src/utils/form/parseLOFARForm.js
@@ -1,4 +1,4 @@
-export default function ParseLOFARForm(formData, page) {
+export default function ParseLOFARForm(formData) {
   let queries = [];
   // queries is an array of dictionaries, where each dictionary consists of
   // {"catalog": "catalogname",
@@ -20,7 +20,7 @@ export default function ParseLOFARForm(formData, page) {
   //  "results": null}
   let catalog = formInput.find(([key]) => key === "catalog")[1];
   let esapquery =
-    query + `${`${query}` ? "&" : ""}archive_uri=` + catalog + `&page=${page}`;
+    query + `${`${query}` ? "&" : ""}archive_uri=` + catalog ;
   queries.push({
     catalog: catalog,
     esapquery: esapquery,
diff --git a/src/utils/form/parseQueryForm.js b/src/utils/form/parseQueryForm.js
index 62d7ec47344ccab0d0631caf302fb3369b441a42..92126fbc7c620b5b30e861dd77af06710f398610 100644
--- a/src/utils/form/parseQueryForm.js
+++ b/src/utils/form/parseQueryForm.js
@@ -6,22 +6,22 @@ import parseApertifForm from "./parseApertifForm";
 import parseASTRONVOForm from "./parseASTRONVOForm";
 import parseRucioForm from "./parseRucioForm";
 
-export default function parseQueryForm(gui, formData, page) {
+export default function parseQueryForm(gui, formData) {
   switch (gui) {
     case "adex":
-      return parseADEXForm(formData, page);
+      return parseADEXForm(formData);
     case "zooniverse":
-      return parseZooniverseForm(formData, page);
+      return parseZooniverseForm(formData);
     case "lofar":
-      return parseLOFARForm(formData, page);
+      return parseLOFARForm(formData);
     case "apertif":
-      return parseApertifForm(formData, page);
+      return parseApertifForm(formData);
     case "astron_vo":
-      return parseASTRONVOForm(formData, page);
+      return parseASTRONVOForm(formData);
     case "ivoa":
-      return parseIVOAForm(formData, page);
+      return parseIVOAForm(formData);
     case "rucio":
-        return parseRucioForm(formData, page);
+        return parseRucioForm(formData);
     default:
       return null;
   }
diff --git a/src/utils/form/parseRucioForm.js b/src/utils/form/parseRucioForm.js
index a0571bb15f2f15432fc230ea49a9d0309eaa281c..c460b203eca02fe1eb89e0076be6f78ce94e57d7 100644
--- a/src/utils/form/parseRucioForm.js
+++ b/src/utils/form/parseRucioForm.js
@@ -1,4 +1,4 @@
-export default function parseRucioForm(formData, page) {
+export default function parseRucioForm(formData) {
   let formInput = Object.entries(formData);
 
   let query = "";
@@ -16,6 +16,6 @@ export default function parseRucioForm(formData, page) {
   console.log("Rucio Query", query);
   return [{
     catalog: "rucio",
-    esapquery: esapquery + `&page=${page}`
+    esapquery: esapquery
   }];
 }
diff --git a/src/utils/form/parseVOServiceForm.js b/src/utils/form/parseVOServiceForm.js
index 69d411fc81eab9f8780108629e78264c70f23caa..6a4898bee252bd2535881e34aa1146946b83734c 100644
--- a/src/utils/form/parseVOServiceForm.js
+++ b/src/utils/form/parseVOServiceForm.js
@@ -1,4 +1,4 @@
-export default function ParseVOServiceForm(formData, access_url, page) {
+export default function ParseVOServiceForm(formData, access_url) {
   let queries = [];
   // queries is an array of dictionaries, where each dictionary consists of
   // {"catalog": "catalogname",
@@ -31,9 +31,7 @@ export default function ParseVOServiceForm(formData, access_url, page) {
   let esapquery =
     "query/?" +
     query +
-    `${`${query}` ? "&" : ""}dataset_uri=` +
-    catalog +
-    `&page=${page}`;
+    `${`${query}` ? "&" : ""}dataset_uri=` + catalog;
 
   queries.push({
     catalog: access_url,
diff --git a/src/utils/form/parseZooniverseForm.js b/src/utils/form/parseZooniverseForm.js
index 50284bb4c8ab1e2db35e4cd2b3446662b477e284..65618c354e53c316ef0b96626588d172596ba83a 100644
--- a/src/utils/form/parseZooniverseForm.js
+++ b/src/utils/form/parseZooniverseForm.js
@@ -1,4 +1,4 @@
-export default function parseZooniverseForm(formData, page) {
+export default function parseZooniverseForm(formData) {
   let catalogs = ["zooniverse_projects", "zooniverse_workflows"];
   let queries = [];
   // queries is an array of dictionaries, where each dictionary consists of
@@ -45,7 +45,7 @@ export default function parseZooniverseForm(formData, page) {
 
     queries.push({
       catalog: catalog,
-      esapquery: esapquery + `&page=${page}`,
+      esapquery: esapquery,
     });
   }
   console.log("Queries:", queries);