Source: synopticapi.js

/*
 *
 * SCADAvis.io Synoptic API © 2018-2022 Ricardo L. Olsen / DSC Systems ALL RIGHTS RESERVED.
 * 
 * WARNING: THE UNAUTHORIZED COPY OR HOST OF THIS FILE IS ILLEGAL!
 * 
 * LICENSE AGREEMENT:
 * THE USER OF THIS SOFTWARE LIBRARY HEREBY AGREES (UNLESS SPECIFICALLY LICENSED OU AUTHORIZED TO) 
 * TO JUST LINK (POINT TO OUR SERVERS) ITS OWN WEB APPS TO THIS SOFTWARE LIBRARY AND STRICTLY 
 * SHALL NOT IN ANY WAY HOST IT BY HIMSELF OR BY THIRD PARTY MEANS.
 *    
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 */
 
/**
 * SCADAvis.io synoptic API.
 * @class scadavis - Must be created with the "new" keyword. E.g. var svgraph = new scadavis("div1", "", "https://svgurl.com/svgurl.svg");
 * @param {string} [container] - ID of the container object. If empty or null the iframe will be appended to the body.
 * @param {string} [iframeparams] - Parameter string for configuring iframe (excluding id and src and sandbox) e.g. 'frameborder="0" height="250" width="250"'.
 * @param {string} [svgurl] - URL for the SVG file.
 * @param {{container: string|Object, iframeparams: string, svgurl: string}} [paramsobj] - Alternatively parameters can be passed in an object.
 * E.g.: svgraph = new scadavis( {container: "div1"} );
 */
function scadavis(container, iframeparams, svgurl) {
    var _this = this;
    var version = "1.0.9";
    var id;
    var iframehtm;
    var scrolling = ' scrolling="no" '; 

    if (typeof container === "object") {
        _this.container = container.container || "";
        _this.apikey = container.apikey || "";
        _this.iframeparams = container.iframeparams || 'frameborder="0" height="250" width="250"';
        _this.svgurl = container.svgurl || "";
    }
    else {
        _this.container = container || "";
        _this.apikey = apikey || "";
        _this.iframeparams = iframeparams || 'frameborder="0" height="250" width="250"';
        _this.svgurl = svgurl || "";
    }

    _this.iframe = null;
    _this.componentloaded = false;
    _this.readyfordata = false;
    _this.domain = "https://scadavis.io";
    _this.rtdata = { data: { type: "tags", tags: [] } };
    _this.npts = {};
    _this.vals = {};
    _this.qualifs = {};
    _this.descriptions = {};
    _this.npt = 0;
    _this.svgobj = null;
    _this.zoomobj = null;
    _this.moveobj = null;
    _this.enabletoolsobj = null;
    _this.enablekeyboardobj = null;
    _this.enableflashobj = null;
    _this.hidewatermarkobj = null;
    _this.setcolorobj = [];
    _this.resetobj = null;
    _this.tagsList = "";
    _this.onready = null;
    _this.onclick = null;
    _this.loadingSVG = 0;

    /**
     * Generate an unique DOM element ID.
     * @private
     * @method guidGenerator
     * @returns {string} DOM ID.
     */
    _this.guidGenerator = function () {
        var S4 = function () {
            return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
        };
        return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
    }

    /**
     * Create a DOM element from HTML.
     * @method createElementFromHTML
     * @private
     * @returns {string} DOM ID.
     */
    _this.createElementFromHTML = function (htmlString) {
        var div = document.createElement('div');
        div.innerHTML = htmlString.trim();
        return div.firstChild;
    }

    id = _this.guidGenerator();

    if (typeof _this.container === "string") {
        if (_this.container.trim().length === 0) {
            _this.container = document.body;
        } else {
            _this.container = document.getElementById(_this.container);
            if (_this.container === null) {
                _this.container = document.body;
            }
        }
    }

    // default is scrolling='no'
    if ( _this.iframeparams.indexOf("scrolling") >=0  )
       scrolling = ''; 

    iframehtm = '<iframe id="' + id + '" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" ' + _this.iframeparams + scrolling + ' src="https://scadavis.io/synoptic/synoptic.html"></iframe>'
    if (_this.container.innerHTML !== undefined)
        _this.container.appendChild(_this.createElementFromHTML(iframehtm));
    else
        _this.container.insertAdjacentHTML('afterend', iframehtm);

    _this.iframe = document.getElementById(id);

    /**
     * Load the SVG synoptic display file from a SVG URL.
     * @method loadURL
     * @param {string} svgurl - The SVG URL.
     */
    _this.loadURL = function (svgurl) {
        _this.svgobj = null;
        _this.readyfordata = false;
        _this.svgurl = svgurl;
        if (_this.svgurl !== "" && _this.loadingSVG === 0) {
            _this.loadingSVG = 1;
            var xhr = new XMLHttpRequest();
            xhr.open('GET', _this.svgurl); // here you point to the SVG synoptic display file
            xhr.onload = function () {
                if (xhr.status === 200) {
                    _this.loadingSVG = 0;
                    if (_this.componentloaded) // SCADAvis component already loaded?
                        _this.iframe.contentWindow.postMessage(xhr.responseText, _this.domain); // send the SVG file contents to the component
                    else
                        _this.svgobj = xhr.responseText; // buffers the result for later use (this can save some time)
                }
            };
            xhr.onreadystatechange = function (oEvent) {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                    } else {
                        _this.loadingSVG = 0;
                        console.log("SCADAvis.io API: error loading SVG URL. " + xhr.statusText);
                    }
                }
            };
            xhr.send();
        }
    }

    /**
     * Reset all data values and tags.
     * @method resetData
     */
    _this.resetData = function () {
        _this.npt = 0;
        _this.npts = [];
        _this.vals = [];
        _this.qualifs = [];

        var obj = { data: { type: "resetData" } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.resetobj = obj;
    }

    /**
     * Update values for tags to the component. Send all tags available.
     * @method updateValues
     * @param {Object.<string, number>} [values] - values in a object like { "tag1" : 1.0, "tag2": 1.2, "tag3": true }.
     */
    _this.updateValues = function (values) {
        if (!_this.readyfordata) 
          return;
           
        if (typeof values === "object" && values !== null)
            Object.keys(values).map(function (tag, index) {
                var n;
                if (tag in _this.npts) {
                    n = _this.npts[tag];
                }
                else {
                    n = ++_this.npt;
                    _this.npts[tag] = n;
                }
                _this.vals[tag] = values[tag];
                _this.qualifs[tag] = 0x00;
            });

        var rtdata = { data: { type: "tags", tags: [] } };
        Object.keys(_this.npts).map(function (tag, index) {
            rtdata.data.tags[index] = {};
            rtdata.data.tags[index].path = tag;
            rtdata.data.tags[index].value = _this.vals[tag];
            rtdata.data.tags[index].quality = !(_this.qualifs[tag] & 0x80);
            if (typeof _this.vals[tag] == "number")
                rtdata.data.tags[index].type = "float";
            else
                if (typeof _this.vals[tag] == "boolean")
                    rtdata.data.tags[index].type = "bool";
                else
                    rtdata.data.tags[index].type = "string";
            rtdata.data.tags[index].parameters = { Value: { TagClientItem: _this.npts[tag], Alarmed: ((_this.qualifs[tag] & 0x100) === 0x100), Desc: _this.descriptions[tag] } };
        });
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(rtdata, _this.domain);
    }

    /**
     * Set a value for a tag. The component will be updated immediately if the component is ready for data.
     * Notice that updating the component at too many times per second can cause performance problems.
     * Preferably update many values using storeValue() then call updateValues() once (repeat after a second or more).
     * @method setValue
     * @param {string} tag - Tag name.
     * @param {number} value - Value for the tag.
     * @param {bool} [failed=false] - True if value is bad or old, false or absent if value is good.
     * @param {bool} [alarmed=false] - True if value is alarmed, false or absent if value is normal.
     * @param {string} [description=tag] - Description.
     * @returns {bool} Returns true if the component was updated (true) or the value was buffered (false).
     */
    _this.setValue = function (tag, value, failed, alarmed, description) {
        if (tag==="" || tag===undefined || tag===null) return  _this.readyfordata;
        var n;
        failed = failed || false;
        alarmed = alarmed || false;
        description = description || tag;
        if (tag in _this.npts) {
            n = _this.npts[tag];
        }
        else {
            n = ++_this.npt;
            _this.npts[tag] = n;
        }
        _this.vals[tag] = value;
        _this.qualifs[tag] = (failed ? 0x80 : 0x00) | (alarmed ? 0x100 : 0x00);
        _this.descriptions[tag] = description;
        if (_this.readyfordata) {
            var rtdata = { data: { type: "tags", tags: [] } };
            rtdata.data.tags[0] = {};
            rtdata.data.tags[0].path = tag;
            rtdata.data.tags[0].value = value;
            rtdata.data.tags[0].quality = !(_this.qualifs[tag] & 0x80);
            if (typeof value == "number")
                rtdata.data.tags[0].type = "float";
            else
                if (typeof value == "boolean")
                    rtdata.data.tags[0].type = "bool";
                else
                    rtdata.data.tags[0].type = "string";
            rtdata.data.tags[0].parameters = { Value: { TagClientItem: n, Alarmed: ((_this.qualifs[tag] & 0x100) === 0x100), Desc: _this.descriptions[tag] } };
            _this.iframe.contentWindow.postMessage(rtdata, _this.domain);
        }
        return _this.readyfordata;
    }

    /**
     * Store a value for a tag. The component will not be updated until called updateValues().
     * @method storeValue
     * @param {string} tag - Tag name.
     * @param {number} value - Value for the tag.
     * @param {bool} [failed=false] - True if value is bad or old, false or absent if value is good.
     * @param {bool} [alarmed=false] - True if value is alarmed, false or absent if value is normal.
     * @param {string} [description=tag] - Description.
     * @returns {bool} - Returns true if the component is ready for data, false if not.
     */
    _this.storeValue = function (tag, value, failed, alarmed, description) {
        if (tag==="" || tag===undefined || tag===null) return  _this.readyfordata;
        var n;
        failed = failed || false;
        alarmed = alarmed || false;
        description = description || tag;
        if (tag in _this.npts) {
            n = _this.npts[tag];
        }
        else {
            n = ++_this.npt;
            _this.npts[tag] = n;
        }
        _this.vals[tag] = value;
        _this.qualifs[tag] = (failed ? 0x80 : 0x00) | (alarmed ? 0x100 : 0x00);;
        _this.descriptions[tag] = description;
        return _this.readyfordata;
    }

    /**
     * Reset all data values and tags.
     * @method resetData
     */
    _this.resetData = function () {
        _this.npt = 0;
        _this.npts = {};
        _this.vals = {};
        _this.qualifs = {};
        _this.descriptions = {};

        var obj = { data: { type: "resetData" } };
        if (_this.readyfordata)
            window.postMessage(obj, _this.domain);
        else
            _this.resetobj = obj;
    }

    /**
     * Get a value for a tag.
     * @method getValue
     * @param {Object} tag - Tag name.
     * @returns {nuber} Returns the value for the tag or null if not found.
     */
    _this.getValue = function (tag) {
        if (tag in _this.vals) {
            return _this.vals[tag];
        }
        return null;
    }

    /**
     * Recover the API Key.
     * @method getApiKey
     * @returns {string} API Key.
     */
    _this.getApiKey = function () {
        return _this.apikey;
    }

    /**
     * Get SCADAvis.io API Version.
     * @method getVersion
     * @returns {string} SCADAvis.io API Version.
     */
    _this.getVersion = function () {
        return version;
    }

    /**
     * Get the DOM element of the iframe.
     * @method getIframe
     * @returns {Object} DOM element reference.
     */
    _this.getIframe = function () {
        return _this.iframe;
    }

    /**
     * Get the current state of the component.
     * @method getComponentState
     * @returns {number} 0=not loaded, 1=loaded and ready for graphics, 2=SVG graphics processed and ready for data.
     */
    _this.getComponentState = function () {
        if (_this.componentloaded == false)
            return 0;
        else
            if (_this.readyfordata == false)
                return 1;
        return 2;
    }

    /**
     * Get SCADAvis.io Component Version.
     * @method getComponentVersion
     * @returns {string} SCADAvis.io Component Version.
     */
    _this.getComponentVersion = function () {
        return version;
    }

    /**
     * Get tags list from the loaded SVG graphics.
     * @method getTagsList
     * @returns {string} Tags list.
     */
    _this.getTagsList = function () {
        return _this.tagsList;
    }

    /**
     * Move the graphic. Multiple calls have cumulative effect.
     * @method moveBy
     * @param {number} [dx=0] Horizontal distance.
     * @param {number} [dy=0] Vertical distance.
     * @param {boolean} [animate=false] Animate or not.
     */
    _this.moveBy = function (dx, dy, animate) {
        dx = dx || 0;
        dy = dy || 0;
        animate = animate || false;
        var obj = { data: { type: "moveBy", dx: dx, dy: dy, animate: animate } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.moveobj = obj;
    }

    /**
     * Apply zoom level. Multiple calls have cumulative effect.
     * @method zoomTo
     * @param {number} [zoomLevel=1.1] Zoom level. >1 zoom in, <1 zoom out.
     * @param {string|{x: number, y: number}} [target={x:0,y:0}] Id of object to zoom in/out or x/y coordinates.
     * @param {boolean} [animate=false] Animate or not.
     */
    _this.zoomTo = function (zoomLevel, target, animate) {
        zoomLevel = zoomLevel || 1.1;
        animate = animate || false;
        var obj = { data: { type: "zoomTo", zoomLevel: zoomLevel, target: target, animate: animate } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.zoomobj = obj;
    }

    /**
     * Apply default zoom level/position.
     * @method zoomToOriginal
     * @param {boolean} [animate=false] Animate or not.
     */
    _this.zoomToOriginal = function (animate) {
        animate = animate || false;
        var obj = { data: { type: "zoomToOriginal", animate: animate } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
    }

    /**
     * Enable or disable pan and zoom tools.
     * @method enableTools
     * @param {boolean} [panEnabled=true] Enable/disable Pan tool.
     * @param {boolean} [zoomEnabled=false] Enable/disable Zoom tool.
     */
    _this.enableTools = function (panEnabled, zoomEnabled) {
        if ( typeof panEnabled === "undefined" || panEnabled )
          panEnabled = true;
        if ( typeof zoomEnabled === "undefined" )
          zoomEnabled = false;
        var obj = { data: { type: "enableTools", panEnabled: panEnabled, zoomEnabled: zoomEnabled } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.enabletoolsobj = obj;
    }

    /**
     * Enable or disable pan and zoom via mouse.
     * @method enableMouse
     * @param {boolean} [panEnabled=true] Enable/disable pan via mouse.
     * @param {boolean} [zoomEnabled=true] Enable/disable zoom via mouse.
     */
    _this.enableMouse = function (panEnabled, zoomEnabled) {
        if ( typeof panEnabled === "undefined" || panEnabled )
          panEnabled = true;
        if ( typeof zoomEnabled === "undefined" || zoomEnabled )
          zoomEnabled = true;
        var obj = { data: { type: "enableMouse", panEnabled: panEnabled, zoomEnabled: zoomEnabled } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.enablemouseobj = obj;
    }

    /**
     * Enable or disable keyboard functions (zoom & pan).
     * @method enableKeyboard
     * @param {boolean} [keyEnabled=true] Enable/disable Pan tool.
     */
    _this.enableKeyboard = function (keyEnabled) {
        if ( typeof keyEnabled === "undefined" || keyEnabled )
          keyEnabled = true;
        var obj = { data: { type: "enableKeyboard", keyEnabled: keyEnabled } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.enablekeyboardobj = obj;
    }

    /**
     * Enable or disable alarm flash (objects blinking when alarmed).
     * @method enableAlarmFlash
     * @param {boolean} [alarmFlashEnabled=true] Enable/disable global alarm flash.
     */
    _this.enableAlarmFlash = function (alarmFlashEnabled) {
        if ( typeof alarmFlashEnabled === "undefined" || alarmFlashEnabled )
        alarmFlashEnabled = true;
        var obj = { data: { type: "enableAlarmFlash", alarmFlashEnabled: alarmFlashEnabled } };
        if (_this.componentloaded)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.enableflashobj = obj;
    }

    /**
     * Hides the watermark.
     * @method hideWatermark
     */
    _this.hideWatermark = function () {
        var obj = { data: { type: "hideWatermark" } };
        if (_this.readyfordata)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.hidewatermarkobj = obj;
    }

    /**
     * Set color code for color shortcuts.
     * @method setColor
     * @param {number} [colorNumber] Color shortcut number.
     * @param {string} [colorCode] Color code.
     */
    _this.setColor = function (colorNumber, colorCode) {
        var obj = { data: { type: "setColor", colorNumber: colorNumber, colorCode: colorCode } };
        if (_this.componentloaded)
            _this.iframe.contentWindow.postMessage(obj, _this.domain);
        else
            _this.setcolorobj.push(obj);
    }

    /**
     * Set event listeners.
     * @method on
     * @param {string} event Event name, one of: "ready", "click" (the first parameter of callback is the element id).
     * @param {function} callback Callback function.
     * @returns True for valid event, false for invalid event name.
     */
    _this.on = function (event, callback) {
        var ret = false;
        switch (event) {
            case "ready":
                _this.onready = callback;
                ret = true;
                break;
            case "click":
                _this.onclick = callback;
                ret = true;
                break;
            default:
                break;
        }

        return ret;
    }

    _this.componentloaded = false;
    _this.readyfordata = false;

    if (_this.svgurl !== "")
        _this.loadURL(_this.svgurl);

    window.addEventListener('message', function (event) { // receive messages, watch for messages from the SCADAvis.io component.

        // for better security: check the origin of the message ( must be from the SCADAvis.io domain and component iframe )
        if (event.source === _this.iframe.contentWindow &&
            event.origin === _this.domain) {

            // when message of type "loaded", get and send an SVG file to it
            if (typeof event.data === "object" && event.data.data.type !== undefined && event.data.data.type === "loaded") {
                _this.componentloaded = true;
                if (_this.setcolorobj.length>0) {
                    for (var i=0; i<_this.setcolorobj.length; i++)
                      event.source.postMessage(_this.setcolorobj[i], event.origin);
                    _this.setcolorobj = [];
                }
                if (_this.enableflashobj !== null)
                    event.source.postMessage(_this.enableflashobj, event.origin); // control alarm flash
                if (_this.svgobj !== null)
                    event.source.postMessage(_this.svgobj, event.origin); // send the SVG file contents to the component
                else
                    if (_this.svgurl !== "")
                        _this.loadURL(_this.svgurl);
            }

            // when message type "ready", the SVG screen is processed, then we can send real time data to the SCADAvis.io component
            if (typeof event.data === "object" && event.data.data.type !== undefined && event.data.data.type === "ready") {
                _this.readyfordata = true;
                _this.tagsList = event.data.data.attributes.tagsList;
                _this.updateValues();
                    if (_this.zoomobj) {
                        event.source.postMessage(_this.zoomobj, event.origin);
                        _this.zoomobj = null;
                    }
                    if (_this.moveobj) {
                        event.source.postMessage(_this.moveobj, event.origin);
                        _this.moveobj = null;
                    }
                    if (_this.enabletoolsobj) {
                        event.source.postMessage(_this.enabletoolsobj, event.origin);
                        _this.enabletoolsobj = null;
                    }
                    if (_this.enablemouseobj) {
                        event.source.postMessage(_this.enablemouseobj, event.origin);
                        _this.enablemouseobj = null;
                    }
                    if (_this.enablekeyboardobj) {
                        event.source.postMessage(_this.enablekeyboardobj, event.origin);
                        _this.enablekeyboardobj = null;
                    }
                    if (_this.hidewatermarkobj) {
                        event.source.postMessage(_this.hidewatermarkobj, event.origin);
                        _this.hidewatermarkobj = null;
                    }
                    if (_this.resetobj) {
                        event.source.postMessage(_this.resetobj, event.origin);
                        _this.resetobj = null;
                    }
                    if (_this.onready)
                        _this.onready();
            }

            // when message of type "click", emit the event callback
            if (typeof event.data === "object" && event.data.data.type !== undefined && event.data.data.type === "click") {
                if (_this.onclick)
                    _this.onclick(event.data.data.attributes.event, event.data.data.attributes.tag);
            }
        }
    });
}