widget.controller.js

592 lines | 19.393 kB Blame History Raw Download
/*
 * Copyright © 2016-2017 The Thingsboard Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import $ from 'jquery';
import 'javascript-detect-element-resize/detect-element-resize';
import Subscription from '../api/subscription';

/* eslint-disable angular/angularelement */

/*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
                                         datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
                                         dashboardTimewindowApi, widget, aliasesInfo, widgetType) {

    var vm = this;

    $scope.$timeout = $timeout;
    $scope.$q = $q;
    $scope.$injector = $injector;
    $scope.tbRaf = tbRaf;

    $scope.rpcRejection = null;
    $scope.rpcErrorText = null;
    $scope.rpcEnabled = false;
    $scope.executingRpcRequest = false;

    var gridsterItemInited = false;

    var cafs = {};

    /*
     *   data = array of datasourceData
     *   datasourceData = {
     *   			tbDatasource,
     *   			dataKey,     { name, config }
     *   			data = array of [time, value]
     *   }
     */

    var widgetContext = {
        inited: false,
        $scope: $scope,
        $container: $('#container', $element),
        $containerParent: $($element),
        width: 0,
        height: 0,
        isEdit: isEdit,
        isMobile: false,
        widgetConfig: widget.config,
        settings: widget.config.settings,
        units: widget.config.units || '',
        decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
        subscriptions: {},
        defaultSubscription: null,
        timewindowFunctions: {
            onUpdateTimewindow: function(startTimeMs, endTimeMs) {
                if (widgetContext.defaultSubscription) {
                    widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs);
                }
            },
            onResetTimewindow: function() {
                if (widgetContext.defaultSubscription) {
                    widgetContext.defaultSubscription.onResetTimewindow();
                }
            }
        },
        subscriptionApi: {
            createSubscription: function(options, subscribe) {
                return createSubscription(options, subscribe);
            },


    //      type: "timeseries" or "latest" or "rpc"
    /*      devicesSubscriptionInfo = [
            {
                deviceId:   ""
                deviceName: ""
                timeseries: [{ name: "", label: "" }, ..]
                attributes: [{ name: "", label: "" }, ..]
            }
    ..
    ]*/

    //      options = {
    //        timeWindowConfig,
    //        useDashboardTimewindow,
    //        legendConfig,
    //        decimals,
    //        units,
    //        callbacks [ onDataUpdated(subscription, apply) ]
    //      }
    //

            createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
                return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe);
            },
            removeSubscription: function(id) {
                var subscription = widgetContext.subscriptions[id];
                if (subscription) {
                    subscription.destroy();
                    delete widgetContext.subscriptions[id];
                }
            }
        },
        controlApi: {
            sendOneWayCommand: function(method, params, timeout) {
                if (widgetContext.defaultSubscription) {
                    return widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout);
                }
                return null;
            },
            sendTwoWayCommand: function(method, params, timeout) {
                if (widgetContext.defaultSubscription) {
                    return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout);
                }
                return null;
            }
        },
        utils: {
            formatValue: formatValue
        }
    };

    var subscriptionContext = {
        $scope: $scope,
        $q: $q,
        $filter: $filter,
        $timeout: $timeout,
        tbRaf: tbRaf,
        timeService: timeService,
        deviceService: deviceService,
        datasourceService: datasourceService,
        utils: utils,
        widgetUtils: widgetContext.utils,
        dashboardTimewindowApi: dashboardTimewindowApi,
        types: types,
        stDiff: stDiff,
        aliasesInfo: aliasesInfo
    };

    var widgetTypeInstance;

    vm.useCustomDatasources = false;

    try {
        widgetTypeInstance = new widgetType(widgetContext);
    } catch (e) {
        handleWidgetException(e);
        widgetTypeInstance = {};
    }
    if (!widgetTypeInstance.onInit) {
        widgetTypeInstance.onInit = function() {};
    }
    if (!widgetTypeInstance.onDataUpdated) {
        widgetTypeInstance.onDataUpdated = function() {};
    }
    if (!widgetTypeInstance.onResize) {
        widgetTypeInstance.onResize = function() {};
    }
    if (!widgetTypeInstance.onEditModeChanged) {
        widgetTypeInstance.onEditModeChanged = function() {};
    }
    if (!widgetTypeInstance.onMobileModeChanged) {
        widgetTypeInstance.onMobileModeChanged = function() {};
    }
    if (!widgetTypeInstance.onDestroy) {
        widgetTypeInstance.onDestroy = function() {};
    }
    if (widgetTypeInstance.useCustomDatasources) {
        vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
    }

    //TODO: widgets visibility

    //var bounds = {top: 0, left: 0, bottom: 0, right: 0};
    /*var visible = false;*/
    /*vm.visibleRectChanged = visibleRectChanged;

    function visibleRectChanged(newVisibleRect) {
        visibleRect = newVisibleRect;
        updateVisibility();
    }*/

    $scope.clearRpcError = function() {
        if (widgetContext.defaultSubscription) {
            widgetContext.defaultSubscription.clearRpcError();
        }
    }

    vm.gridsterItemInitialized = gridsterItemInitialized;

    initialize();


    /*
            options = {
                 type,
                 targetDeviceAliasIds,  // RPC
                 targetDeviceIds,       // RPC
                 datasources,
                 timeWindowConfig,
                 useDashboardTimewindow,
                 legendConfig,
                 decimals,
                 units,
                 callbacks
            }
     */

    function createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
        var deferred = $q.defer();
        options.type = type;

        if (useDefaultComponents) {
            defaultComponentsOptions(options);
        } else {
            if (!options.timeWindowConfig) {
                options.useDashboardTimewindow = true;
            }
        }

        utils.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then(
            function (datasources) {
                options.datasources = datasources;
                var subscription = createSubscription(options, subscribe);
                if (useDefaultComponents) {
                    defaultSubscriptionOptions(subscription, options);
                }
                deferred.resolve(subscription);
            }
        );
        return deferred.promise;
    }

    function createSubscription(options, subscribe) {
        options.dashboardTimewindow = dashboardTimewindow;
        var subscription =
            new Subscription(subscriptionContext, options);
        widgetContext.subscriptions[subscription.id] = subscription;
        if (subscribe) {
            subscription.subscribe();
        }
        return subscription;
    }

    function defaultComponentsOptions(options) {
        options.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
            ? widget.config.useDashboardTimewindow : true;

        options.timeWindowConfig = options.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;
        options.legendConfig = null;

        if ($scope.displayLegend) {
            options.legendConfig = $scope.legendConfig;
        }
        options.decimals = widgetContext.decimals;
        options.units = widgetContext.units;

        options.callbacks = {
            onDataUpdated: function() {
                widgetTypeInstance.onDataUpdated();
            },
            onDataUpdateError: function(subscription, e) {
                handleWidgetException(e);
            },
            dataLoading: function(subscription) {
                if ($scope.loadingData !== subscription.loadingData) {
                    $scope.loadingData = subscription.loadingData;
                }
            },
            legendDataUpdated: function(subscription, apply) {
                if (apply) {
                    $scope.$digest();
                }
            },
            timeWindowUpdated: function(subscription, timeWindowConfig) {
                widget.config.timewindow = timeWindowConfig;
                $scope.$apply();
            }
        }
    }

    function defaultSubscriptionOptions(subscription, options) {
        if (!options.useDashboardTimewindow) {
            $scope.$watch(function () {
                return widget.config.timewindow;
            }, function (newTimewindow, prevTimewindow) {
                if (!angular.equals(newTimewindow, prevTimewindow)) {
                    subscription.updateTimewindowConfig(widget.config.timewindow);
                }
            });
        }
        if ($scope.displayLegend) {
            $scope.legendData = subscription.legendData;
        }
    }

    function createDefaultSubscription() {
        var subscription;
        var options;
        if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
            options = {
                type: widget.type,
                datasources: angular.copy(widget.config.datasources)
            };
            defaultComponentsOptions(options);

            subscription = createSubscription(options);

            defaultSubscriptionOptions(subscription, options);

            // backward compatibility

            widgetContext.datasources = subscription.datasources;
            widgetContext.data = subscription.data;
            widgetContext.hiddenData = subscription.hiddenData;
            widgetContext.timeWindow = subscription.timeWindow;

        } else if (widget.type === types.widgetType.rpc.value) {
            $scope.loadingData = false;
            options = {
                type: widget.type,
                targetDeviceAliasIds: widget.config.targetDeviceAliasIds
            }
            options.callbacks = {
                rpcStateChanged: function(subscription) {
                    $scope.rpcEnabled = subscription.rpcEnabled;
                    $scope.executingRpcRequest = subscription.executingRpcRequest;
                },
                onRpcSuccess: function(subscription) {
                    $scope.executingRpcRequest = subscription.executingRpcRequest;
                },
                onRpcFailed: function(subscription) {
                    $scope.executingRpcRequest = subscription.executingRpcRequest;
                    $scope.rpcErrorText = subscription.rpcErrorText;
                    $scope.rpcRejection = subscription.rpcRejection;
                },
                onRpcErrorCleared: function() {
                    $scope.rpcErrorText = null;
                    $scope.rpcRejection = null;
                }
            }
            subscription = createSubscription(options);
        } else if (widget.type === types.widgetType.static.value) {
            $scope.loadingData = false;
        }
        if (subscription) {
            widgetContext.defaultSubscription = subscription;
        }
    }


    function initialize() {

        if (!vm.useCustomDatasources) {
            createDefaultSubscription();
        } else {
            $scope.loadingData = false;
        }

        $scope.$on('toggleDashboardEditMode', function (event, isEdit) {
            onEditModeChanged(isEdit);
        });

        addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef

        $scope.$watch(function () {
            return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;
        }, function () {
            //updateBounds();
            $scope.$emit("widgetPositionChanged", widget);
        });

        $scope.$on('gridster-item-resized', function (event, item) {
            if (!widgetContext.isMobile) {
                widget.sizeX = item.sizeX;
                widget.sizeY = item.sizeY;
            }
        });

        $scope.$on('mobileModeChanged', function (event, newIsMobile) {
            onMobileModeChanged(newIsMobile);
        });

        $scope.$on("$destroy", function () {
            removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
            onDestroy();
        });
    }

    function handleWidgetException(e) {
        $log.error(e);
        $scope.widgetErrorData = utils.processWidgetException(e);
    }

    function onInit() {
        if (!widgetContext.inited) {
            widgetContext.inited = true;
            try {
                widgetTypeInstance.onInit();
            } catch (e) {
                handleWidgetException(e);
            }
            if (!vm.useCustomDatasources && widgetContext.defaultSubscription) {
                widgetContext.defaultSubscription.subscribe();
            }
        }
    }

    function checkSize() {
        var width = widgetContext.$containerParent.width();
        var height = widgetContext.$containerParent.height();
        var sizeChanged = false;

        if (!widgetContext.width || widgetContext.width != width || !widgetContext.height || widgetContext.height != height) {
            if (width > 0 && height > 0) {
                widgetContext.$container.css('height', height + 'px');
                widgetContext.$container.css('width', width + 'px');
                widgetContext.width = width;
                widgetContext.height = height;
                sizeChanged = true;
            }
        }
        return sizeChanged;
    }

    function onResize() {
        if (checkSize()) {
            if (widgetContext.inited) {
                if (cafs['resize']) {
                    cafs['resize']();
                    cafs['resize'] = null;
                }
                cafs['resize'] = tbRaf(function() {
                    try {
                        widgetTypeInstance.onResize();
                    } catch (e) {
                        handleWidgetException(e);
                    }
                });
            } else if (gridsterItemInited) {
                onInit();
            }
        }
    }

    function gridsterItemInitialized(item) {
        if (item && item.gridster) {
            widgetContext.isMobile = item.gridster.isMobile;
            gridsterItemInited = true;
            if (checkSize()) {
                onInit();
            }
            // gridsterItemElement = $(item.$element);
            //updateVisibility();
        }
    }

    function onEditModeChanged(isEdit) {
        if (widgetContext.isEdit != isEdit) {
            widgetContext.isEdit = isEdit;
            if (widgetContext.inited) {
                if (cafs['editMode']) {
                    cafs['editMode']();
                    cafs['editMode'] = null;
                }
                cafs['editMode'] = tbRaf(function() {
                    try {
                        widgetTypeInstance.onEditModeChanged();
                    } catch (e) {
                        handleWidgetException(e);
                    }
                });
            }
        }
    }

    function onMobileModeChanged(isMobile) {
        if (widgetContext.isMobile != isMobile) {
            widgetContext.isMobile = isMobile;
            if (widgetContext.inited) {
                if (cafs['mobileMode']) {
                    cafs['mobileMode']();
                    cafs['mobileMode'] = null;
                }
                cafs['mobileMode'] = tbRaf(function() {
                    try {
                        widgetTypeInstance.onMobileModeChanged();
                    } catch (e) {
                        handleWidgetException(e);
                    }
                });
            }
        }
    }

    function isNumeric(val) {
        return (val - parseFloat( val ) + 1) >= 0;
    }

    function formatValue(value, dec, units) {
        if (angular.isDefined(value) &&
            value !== null && isNumeric(value)) {
            var formatted = value;
            if (angular.isDefined(dec)) {
                formatted = formatted.toFixed(dec);
            }
            formatted = (formatted * 1).toString();
            if (angular.isDefined(units) && units.length > 0) {
                formatted += ' ' + units;
            }
            return formatted;
        } else {
            return '';
        }
    }

    function onDestroy() {
        for (var id in widgetContext.subscriptions) {
            var subscription = widgetContext.subscriptions[id];
            subscription.destroy();
        }
        widgetContext.subscriptions = [];
        if (widgetContext.inited) {
            widgetContext.inited = false;
            for (var cafId in cafs) {
                if (cafs[cafId]) {
                    cafs[cafId]();
                    cafs[cafId] = null;
                }
            }
            try {
                widgetTypeInstance.onDestroy();
            } catch (e) {
                handleWidgetException(e);
            }
        }
    }

    //TODO: widgets visibility
    /*function updateVisibility(forceRedraw) {
        if (visibleRect) {
            forceRedraw = forceRedraw || visibleRect.containerResized;
            var newVisible = false;
            if (visibleRect.isMobile && gridsterItemElement) {
                var topPx = gridsterItemElement.position().top;
                var bottomPx = topPx + widget.sizeY * visibleRect.curRowHeight;
                newVisible = !(topPx > visibleRect.bottomPx ||
                bottomPx < visibleRect.topPx);
            } else {
                newVisible = !(bounds.left > visibleRect.right ||
                bounds.right < visibleRect.left ||
                bounds.top > visibleRect.bottom ||
                bounds.bottom < visibleRect.top);
            }
            if (visible != newVisible) {
                visible = newVisible;
                if (visible) {
                    onRedraw(50);
                }
            } else if (forceRedraw && visible) {
                onRedraw(50);
            }
        }
    }*/

/*    function updateBounds() {
        bounds = {
            top: widget.row,
            left: widget.col,
            bottom: widget.row + widget.sizeY,
            right: widget.col + widget.sizeX
        };
        updateVisibility(true);
        onRedraw();
    }*/

}

/* eslint-enable angular/angularelement */