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, "&") + .replace(/</g, "<") + .replace(/>/g, ">"); + }; + + // 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 dont 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> + <button variant="outline-warning" onClick={() => unregister()}>unregister</button> + + {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);