diff --git a/examples/al-gw.html b/examples/al-gw.html
index 8a4f829cf..bee55f15e 100644
--- a/examples/al-gw.html
+++ b/examples/al-gw.html
@@ -8,7 +8,7 @@
import A from '../src/js/A.js';
let aladin;
A.init.then(() => {
- aladin = A.aladin('#aladin-lite-div', {projection: "TAN", target: '15 16 57.636 -60 55 7.49', showCooGrid: true, fov: 90, fullScreen: true});
+ aladin = A.aladin('#aladin-lite-div', {projection: "TAN", target: '15 16 57.636 -60 55 7.49', samp: true, showCooGrid: true, fov: 90, fullScreen: true});
var moc_0_99 = A.MOCFromURL("./gw/gw_0.9.fits",{ name: "GW 90%", color: "#ff0000", opacity: 0.7, lineWidth: 5, perimeter: true});
var moc_0_95 = A.MOCFromURL("./gw/gw_0.6.fits",{ name: "GW 60%", color: "#00ff00", opacity: 0.8, lineWidth: 5, perimeter: true});
diff --git a/src/js/A.js b/src/js/A.js
index 0cf0a9d6f..c17ccc5aa 100644
--- a/src/js/A.js
+++ b/src/js/A.js
@@ -165,8 +165,6 @@ A.MOCFromJSON = function (jsonMOC, options) {
};
-// TODO: try first without proxy, and then with, if param useProxy not set
-// API
A.catalogFromURL = function (url, options, successCallback, errorCallback, useProxy) {
var catalog = A.catalog(options);
diff --git a/src/js/Aladin.js b/src/js/Aladin.js
index b97af9720..35a7b8072 100644
--- a/src/js/Aladin.js
+++ b/src/js/Aladin.js
@@ -51,6 +51,7 @@ import { ALEvent } from "./events/ALEvent.js";
import { Color } from './Color.js';
import { ImageFITS } from "./ImageFITS.js";
import { DefaultActionsForContextMenu } from "./DefaultActionsForContextMenu.js";
+import { SAMPConnector } from "./vo/samp.js";
import A from "./A.js";
import $ from 'jquery';
@@ -471,6 +472,13 @@ export let Aladin = (function () {
this.contextMenu = new ContextMenu(this);
this.contextMenu.attachTo(this.view.catalogCanvas, DefaultActionsForContextMenu.getDefaultActions(this));
}
+
+ if (options.samp) {
+ this.samp = new SAMPConnector(this);
+ ALEvent.SAMP_AVAILABILITY.listenedBy(this.aladinDiv, function (e) {
+ console.log('is hub running samp', e.detail.isHubRunning)
+ });
+ }
};
/**** CONSTANTS ****/
@@ -504,6 +512,7 @@ export let Aladin = (function () {
reticleColor: "rgb(178, 50, 178)",
reticleSize: 22,
log: true,
+ samp: true,
allowFullZoomout: false,
realFullscreen: false,
showAllskyRing: false,
diff --git a/src/js/View.js b/src/js/View.js
index be7d0b4a5..6cafc9303 100644
--- a/src/js/View.js
+++ b/src/js/View.js
@@ -370,7 +370,6 @@ export let View = (function () {
}
this.computeNorder();
- this.redraw();
};
var pixelateCanvasContext = function (ctx, pixelateFlag) {
diff --git a/src/js/events/ALEvent.js b/src/js/events/ALEvent.js
index abd568a90..f52b19cc6 100644
--- a/src/js/events/ALEvent.js
+++ b/src/js/events/ALEvent.js
@@ -55,6 +55,8 @@ export class ALEvent {
static GRAPHIC_OVERLAY_LAYER_CHANGED = new ALEvent("AL:GraphicOverlayLayer.changed");
+ static SAMP_AVAILABILITY = new ALEvent("AL:samp.started");
+
constructor(name) {
this.name = name;
}
diff --git a/src/js/libs/samp.js b/src/js/libs/samp.js
new file mode 100644
index 000000000..7f181a0b8
--- /dev/null
+++ b/src/js/libs/samp.js
@@ -0,0 +1,1319 @@
+// 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,
+// .
+// 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.
+
+export let 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)
+ var TYPE_STRING = "string";
+ var TYPE_LIST = "list";
+ var 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, ">");
+ };
+
+ // 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
+ + ""
+ + XmlRpc.escapeXml(obj)
+ + "";
+ }
+ else if (type === TYPE_LIST) {
+ result = [];
+ result.push(prefix + "",
+ prefix + " ",
+ prefix + " ");
+ for (i = 0; i < obj.length; i++) {
+ result.push(v2x(obj[i], prefix + " "));
+ }
+ result.push(prefix + " ",
+ prefix + " ",
+ prefix + "");
+
+ return result.join("\n");
+ }
+ else if (type === TYPE_MAP) {
+ result = [];
+ result.push(prefix + "");
+ result.push(prefix + " ");
+ for (i in obj) {
+ result.push(prefix + " ");
+ result.push(prefix + " "
+ + XmlRpc.escapeXml(i)
+ + "");
+ result.push(v2x(obj[i], prefix + " "));
+ result.push(prefix + " ");
+ }
+ result.push(prefix + " ");
+ result.push(prefix + "");
+ 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 and/or "
+ + "in ?");
+ }
+ }
+ 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 content - multiple elements");
+ }
+ };
+
+ // Turns the content of an XML-RPC 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 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 ) 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 ");
+ }
+ 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(
+ "",
+ "",
+ " " + this.methodName + "",
+ " ");
+ for (var i = 0; i < this.params.length; i++) {
+ lines.push(" ",
+ XmlRpc.valueToXml(this.params[i], " "),
+ " ");
+ }
+ lines.push(
+ " ",
+ "");
+ 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 = " Registered: ";
+ 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:
+ //
+ //
+ 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.Connection = Connection;
+ jss.CallableClient = CallableClient;
+ jss.ClientTracker = ClientTracker;
+
+ return jss;
+})();
\ No newline at end of file
diff --git a/src/js/vo/samp.js b/src/js/vo/samp.js
new file mode 100644
index 000000000..b469e01e6
--- /dev/null
+++ b/src/js/vo/samp.js
@@ -0,0 +1,180 @@
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+// Aladin Lite 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.
+//
+// Aladin Lite 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 Aladin Lite.
+//
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File vo/samp.js
+ *
+ *
+ * Author: Matthieu Baumann [CDS, matthieu.baumann@astro.unistra.fr]
+ *
+ *****************************************************************************/
+
+import { ALEvent } from "../events/ALEvent";
+import { samp } from '../libs/samp';
+
+export class SAMPConnector {
+ constructor(aladin) {
+ // Define listeners
+ let cc = new samp.ClientTracker();
+ let callHandler = cc.callHandler;
+
+ callHandler["script.aladin.send"] = function(senderId, message, isCall) {
+ var params = message["samp.params"];
+ aladin.setBaseImageLayer(params["url"])
+ };
+
+ callHandler["coord.pointAt.sky"] = function(senderId, message, isCall) {
+ var params = message["samp.params"];
+
+ aladin.gotoRaDec(+params["ra"], +params["dec"])
+ };
+
+ callHandler["coverage.load.moc.fits"] = function(senderId, message, isCall) {
+ var params = message["samp.params"];
+
+ let name = params["name"];
+ let moc = A.MOCFromURL(params["url"], {name: name, lineWidth: 3});
+ aladin.addMOC(moc);
+ };
+
+ callHandler["image.load.fits"] = function(senderId, message, isCall) {
+ let params = message["samp.params"];
+
+ let url = params["url"];
+ let name = params["name"];
+ const image = aladin.createImageFITS(url, name, options, (e) => window.alert(e));
+
+ aladin.setOverlayImageLayer(image, name);
+ };
+
+ callHandler["table.load.votable"] = function(senderId, message, isCall) {
+ let params = message["samp.params"];
+
+ let url = params["url"];
+ let name = params["name"];
+
+ let cat = A.catalogFromURL(
+ url,
+ {name: name, onClick: 'showTable'},
+ null,
+ (e) => window.alert(e)
+ );
+ aladin.addCatalog(cat)
+ };
+
+ let subs = cc.calculateSubscriptions();
+ let meta = {
+ "samp.name": "Aladin Lite",
+ "samp.description": "Aladin Lite web visualizer SAMP connector"
+ };
+ // Arrange for document to be adjusted for presence of hub every 2 sec.
+ this.connector = new samp.Connector("Aladin Lite", meta, cc, subs);
+ window.addEventListener('load', (e) => {
+ this.connector.onHubAvailability((isHubRunning) => {
+ // Communicate to Aladin Lite
+ ALEvent.SAMP_AVAILABILITY.dispatchedTo(aladin.aladinDiv, { isHubRunning: isHubRunning } );
+ }, 2000);
+ });
+
+ window.addEventListener('unload', (e) => {
+ this.connector.unregister();
+ });
+
+ // TODO put that in a button
+ //this.connector.register();
+ }
+
+ // Broadcasts a message given a hub connection.
+ _send(mtype, params) {
+ // Provides execution of a SAMP operation with register-on-demand.
+ this.connector.runWithConnection(
+ (connection) => {
+ let msg = new samp.Message(mtype, params);
+ connection.notifyAll([msg]);
+ },
+ (e) => {
+ window.alert(e)
+ }
+ )
+ }
+
+ /**
+ * Load a VOTable by url
+ * @param {String} url - URL of the VOTable document to load
+ * @param {String} [tableId] - Identifier which may be used to refer to the loaded table in subsequent messages
+ * @param {String} [name] - Name which may be used to label the loaded table in the application GUI
+ */
+ loadVOTable(url, tableId, name) {
+ this._send("table.load.votable", {
+ url: url,
+ "table-id": tableId,
+ name: name
+ })
+ }
+
+ /**
+ * Load a fits image by url
+ * @param {String} url - URL of the FITS image to load
+ * @param {String} [imageId] - Identifier which may be used to refer to the loaded image in subsequent messages
+ * @param {String} [name] - Name which may be used to label the loaded image in the application GUI
+ */
+ loadImageFITS(url, imageId, name) {
+ this._send("image.load.fits", {
+ "url": url,
+ "image-id": imageId,
+ "name": name
+ })
+ }
+
+ /**
+ * Load a Multi-Order-Coverage FITS file
+ * @param {String} url - URL of a FITS file containing the MOC to load
+ * @param {String} [coverageId] - Identifier which may be used to refer to the loaded coverage specification in subsequent messages
+ * @param {String} [name] - Name which may be used to label the loaded image in the application GUI
+ */
+ loadMocFITS(url, coverageId, name) {
+ this._send("coverage.load.moc.fits", {
+ "url": url,
+ "coverage-id": coverageId,
+ "name": name
+ })
+ }
+
+ /**
+ * Load a HiPS by an url
+ * @param {String} url - base URL for the HiPS to load
+ */
+ loadHiPS(url) {
+ const cmd = 'load ' + url;
+ this._send("script.aladin.send", { "script": cmd })
+ }
+
+ /**
+ * Send a ra, dec position to the hub
+ * @param {Float} ra - right ascension in degrees
+ * @param {Float} dec - declination in degrees
+ */
+ centerAtRaDec(ra, dec) {
+ this._send("coord.pointAt.sky", { "ra": ra.toString(), "dec": dec.toString() })
+ }
+}
+