diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index ef0bf2698f..81f9e6a14d 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -50,8 +50,11 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "configurationVersion": 2, + "configurationVersion": 3, "configuration": { + "processingSettings": { + "type": "ON_EVERY_MESSAGE" + }, "scope": "CLIENT_SCOPE", "notifyDevice": false, "sendAttributesUpdatedNotification": false, diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg index 4e41fd77c6..37b3a199c6 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Bottom right elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg index 33942432cb..2feb95e9ff 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Bottom tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg index df708d47d3..ab6798b48b 100644 --- a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Cross connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,480 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +500,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +513,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg index 33b6c0a222..74d048e884 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -85,6 +86,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -93,16 +171,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -113,12 +183,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -127,48 +196,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg index bfc97fe928..fd14834d12 100644 --- a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Left bottom elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg index eac3869a1a..af83a4abb3 100644 --- a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Left tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst topLine = \"M100 100L100 0\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg index 6dfbc9e265..5b6d30ab65 100644 --- a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Left top elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg index 86bb07a520..3e65bd6b04 100644 --- a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg @@ -4,6 +4,7 @@ "description": "Long horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 2, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -86,6 +87,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -94,16 +172,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -114,12 +184,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -128,49 +197,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - - - + + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg index d7a0aa8aa7..b5c9730842 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Long vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 2, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -85,6 +86,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -93,16 +171,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -113,12 +183,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -127,48 +196,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg index 17d8bf4e0c..62aecb065d 100644 --- a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Right tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst rightLine = \"M103 100H200\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "bottomFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.bottom-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg index f2d579c551..8ec4e3cc65 100644 --- a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Top right elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "line", @@ -10,23 +11,132 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "animationDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -37,12 +147,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -51,32 +160,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg index 004096aaa7..e4561a8347 100644 --- a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Top tee connector", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", "tags": [ { "tag": "line", @@ -15,23 +16,364 @@ "actions": null } ], - "behavior": [], + "behavior": [ + { + "id": "leftFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "leftFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.left-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.fluid-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowDirection", + "name": "{i18n:scada.symbol.animation-direction}", + "hint": "{i18n:scada.symbol.animation-direction-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "topFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.top-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlow", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowDirection", + "name": "{i18n:scada.symbol.flow-direction}", + "hint": "{i18n:scada.symbol.flow-direction-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": true, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "rightFlowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": "{i18n:scada.symbol.right-connector}", + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + } + ], "properties": [ { "id": "mainLine", "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -42,12 +384,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -56,32 +397,95 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "color", + "default": "#C8DFF7", + "disabled": false, + "visible": true + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg index 6ce9336fee..cfaf6793ea 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg @@ -3,6 +3,7 @@ "description": "Vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", "tags": [ { "tag": "arrow", @@ -85,6 +86,83 @@ }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", + "group": null, + "type": "value", + "valueType": "BOOLEAN", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": false, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "key": "state", + "scope": null + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null + }, + { + "id": "flowAnimationSpeed", + "name": "{i18n:scada.symbol.flow-animation-speed}", + "hint": "{i18n:scada.symbol.flow-animation-speed-hint}", + "group": null, + "type": "value", + "valueType": "DOUBLE", + "trueLabel": null, + "falseLabel": null, + "stateLabel": null, + "defaultGetValueSettings": { + "action": "DO_NOTHING", + "defaultValue": 1, + "executeRpc": { + "method": "getState", + "requestTimeout": 5000, + "requestPersistent": false, + "persistentPollingInterval": 1000 + }, + "getAttribute": { + "scope": null, + "key": "state" + }, + "getTimeSeries": { + "key": "state" + }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, + "dataToValue": { + "type": "NONE", + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + } + }, + "defaultSetValueSettings": null, + "defaultWidgetActionSettings": null } ], "properties": [ @@ -93,16 +171,8 @@ "name": "{i18n:scada.symbol.main-line}", "type": "switch", "default": true, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "mainLineSize", @@ -113,12 +183,11 @@ "subLabel": "Main", "divider": true, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "secondaryLineSize", @@ -127,48 +196,93 @@ "default": 2, "required": true, "subLabel": "Secondary", - "divider": null, "fieldSuffix": "px", - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", "min": 0, "max": 99, - "step": 1 + "step": 1, + "disabled": false, + "visible": true }, { "id": "lineColor", "name": "{i18n:scada.symbol.line-color}", "type": "color", "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { - "id": "arrowColor", - "name": "{i18n:scada.symbol.arrow-color}", + "id": "flowAnimationWidth", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 4, + "subLabel": "Width", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowAnimationColor", + "name": "{i18n:scada.symbol.flow}", + "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#1A1A1A", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#C8DFF7" + }, + { + "id": "flowStyleDash", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "required": true, + "subLabel": "{i18n:scada.symbol.dash}", + "divider": true, + "fieldSuffix": "px", + "min": 0, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowStyleGap", + "name": "{i18n:scada.symbol.flow-style}", + "group": "{i18n:scada.symbol.animation}", + "type": "number", + "default": 10, + "subLabel": "{i18n:scada.symbol.gap}", + "fieldSuffix": "px", + "min": 1, + "step": 1, + "disabled": false, + "visible": true + }, + { + "id": "flowDashCap", + "name": "{i18n:scada.symbol.flow-dash-cap}", + "group": "{i18n:scada.symbol.animation}", + "type": "select", + "default": "butt", + "items": [ + { + "value": "butt", + "label": "{i18n:scada.symbol.dash-cap-butt}" + }, + { + "value": "round", + "label": "{i18n:scada.symbol.dash-cap-round}" + }, + { + "value": "square", + "label": "{i18n:scada.symbol.dash-cap-square}" + } + ], + "disabled": false, + "visible": true } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index 86754f0bd4..d0d0741e21 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -2,13 +2,17 @@ "widgetsBundle": { "alias": "maps_v2", "title": "Maps", - "image": "data:image/png;base64,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", + "image": "tb-image:bWFwc19zeXN0ZW1fYnVuZGxlX2ltYWdlLnBuZw==:Ik1hcHMiIHN5c3RlbSBidW5kbGUgaW1hZ2U=:SU1BR0U=;data:image/png;base64,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", + "scada": false, "description": "Visualize the latest location or trip of the devices or other entities on the indoor or outdoor maps.", "order": 6000, - "externalId": null, "name": "Maps" }, "widgetTypeFqns": [ + "map", + "image_map", + "trip_map", + "route_map", "maps_v2.openstreetmap", "maps_v2.google_maps", "maps_v2.image_map", diff --git a/application/src/main/data/json/system/widget_types/google_map.json b/application/src/main/data/json/system/widget_types/google_map.json index 9c7696d8db..cd69b304e9 100644 --- a/application/src/main/data/json/system/widget_types/google_map.json +++ b/application/src/main/data/json/system/widget_types/google_map.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.google_maps", "name": "Google Map", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/google_map_system_widget_image.png", "description": "Displays the location of the entities on Google Maps. Requires the Google map key to work properly. Highly customizable via custom markers, marker tooltips, and widget actions.", "descriptor": { @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", + "settingsDirective": "tb-map-widget-settings-legacy", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"google-map\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7568\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Google Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/here_map.json b/application/src/main/data/json/system/widget_types/here_map.json index 9f5d7e9812..b49e1dcf41 100644 --- a/application/src/main/data/json/system/widget_types/here_map.json +++ b/application/src/main/data/json/system/widget_types/here_map.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.here_map", "name": "HERE Map", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/here_map_system_widget_image.png", "description": "Displays the location of the entities on HERE Maps. Requires the HERE map key to work properly. Highly customizable via custom markers, marker tooltips, and widget actions.", "descriptor": { @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('here', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", + "settingsDirective": "tb-map-widget-settings-legacy", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"here\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"gmDefaultMapType\":\"roadmap\",\"mapProvider\":\"HERE.normalDay\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"useV3\":true,\"apiKey\":\"kVXykxAfZ6LS4EbCTO02soFVfjA7HoBzNVVH9u7nzoE\"},\"mapImageUrl\":\"tb-image;/api/images/system/here_map_system_widget_map_image.svg\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 02554e82f6..2b7d56957b 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -1,5 +1,5 @@ { - "fqn": "maps_v2.image_map", + "fqn": "image_map", "name": "Image Map", "deprecated": false, "image": "tb-image;/api/images/system/image_map_system_widget_image.png", @@ -7,30 +7,18 @@ "descriptor": { "type": "latest", "sizeX": 8.5, - "sizeY": 6.5, + "sizeY": 6, "resources": [], - "templateHtml": "", - "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n", + "settingsForm": [], + "dataKeySettingsForm": [], "settingsDirective": "tb-map-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-map-basic-config", + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"image\",\"imageSource\":{\"sourceType\":\"image\",\"url\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"entityAliasId\":null,\"entityKey\":null},\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"patternFunction\":null,\"trigger\":\"click\",\"autoclose\":true,\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\" \",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Image Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" }, - "tags": [ - "building", - "interior", - "venue", - "inside", - "room", - "office", - "manufacturing", - "floor", - "plant", - "storage", - "warehouse", - "depot" - ], "resources": [ { "link": "/api/images/system/image_map_system_widget_image.png", @@ -38,7 +26,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "image_map_system_widget_image.png", - "publicResourceKey": "hDdSISQr6elribOYD6T3uePXZI5WvNtM", + "publicResourceKey": "otJxNhSbraXccAZhnPzmfOIdEXra5Hf5", "mediaType": "image/png", "data": "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", "public": true @@ -49,54 +37,25 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "image_map_system_widget_map_image.svg", - "publicResourceKey": "QKRIYhDeBGwjaeIS601VvNLSsvZ25DRj", + "publicResourceKey": "P4hZLRjmo2P1RjGPZ4CrCMxh6xOQFQr5", "mediaType": "image/svg+xml", "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTEzNC41IiBoZWlnaHQ9Ijc2Mi43OCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI3LjA3MSAtMzA3LjkpIj4KICA8ZyBmaWxsPSJub25lIj4KICAgPHBhdGggZD0ibTkwNi4wMyA3MDYuMTMgMy40MjkyIDE3Ljc5Nm0tODgwLjg5IDQxLjEyMWMxNTAuNDQgNi44MzM0IDE0Ni4zOS0yNi4zMzQgMTY2LjQzLTI5LjMyIDM2LjE0NC01LjM4NDggMTE0LjI5LTYuNTI1NCAxNDguMzMtOC42MjM1IDQzLjM3OC0yLjY3MzggMTQxLjc2LTExLjIzMSAxODguODYtMTkuODM0IDM5LjgxMS03LjI3MjggMjIxLjM3LTAuODYyMzUgMzE5LjA3LTAuODYyMzUgNzAuODI3IDAgMTQ2LjkyLTEuNzI0NyAyMTguMTgtMS43MjQ3LTMxLjYyIDAgMTE3Ljg2LTIuNTg3MSA4Ni4yMzYtMi41ODcxbS0yNS4wOTEtNjguMTI2Yy01Mi44IDM0Ljc4NS02NS44OTUgNTEuNzQ5LTk1LjYzOSA4MS40OTMtMjQuOTMxIDI0LjkzMS0xNDAuNC0xOS4xMzktMTc4Ljk0IDM2LjY1LTEyLjI4MSAxNy43NzctNDcuMDAzIDQ2LjU0Ny02NS4xMDggNTkuMDcxLTIwLjEwNSAxMy45MDgtNTYuMDM3IDQ0Ljk1Ny02Ny43NjkgNzMuMDc4LTQuODAxNSAxMS41MDktMTMuMzggMzUuOTkzLTIzLjQ0OSA0Ni4wNjItMTAuNDk3IDEwLjQ5Ny0zOC4zNzcgNi4zODU3LTQ0LjAyMyAxNy42NDgtMTkuMDA1IDM3LjkwOC0yNS40NjUgMTAwLjkyLTY3LjYxOCAxMDIuMDVtMTkuMjgyLTYyNC4wMWMzNC42NTktMS44NzM4IDg0LjAyNyA3LjM5MTMgMTA5LjktNC4yODU0IDEzLjI4Mi01Ljk5NDEgNDEuNDA3LTIuNDYxNCA2Ni44MjktMi4zMjA1IDM1LjMyMiAwLjE5NTc4IDY0LjM4MiAwLjYzNDc3IDEwMS45MiA1LjAyMzIgMjUuMDMgMi45MjY1IDQ0LjY2MyAzNC4yODcgNTguNTI3IDUwLjY0NCAxNy4wOTkgMjAuMTczIDYyLjc2NC0xLjcxNDcgNjYuMzA2IDMyLjEzNCA1LjEwMjcgNDguNzY2LTYuMzI4NCA3OC42MzcgNi4xNDExIDk3LjM0MiAxOS45NjkgMjkuOTU0IDUwLjQ4NiAxNy44NTYgNDQuNjE5IDgzLjk3MW0tNDcyLjQ1LTM3OC43OWM0LjY0MzUgMjMuNzI5IDE1LjA2OSA3Mi43NzYgMTkuMDYxIDEzMC42NCAwLjg3MjA2IDEyLjY0IDUuNDQ3MiAyNC45OTMgNC4yMjIzIDQ1LjI3OC0yLjUxNzIgNDEuNjg4LTE1LjcxNyA0My42NzctMTUuMDkxIDYwLjM2NSAxLjQzMiAzOC4xODIgMzAuNjE0IDkzLjgzNyAzMC42MTQgMTM5LjcgMCAyNC4xODEtMi42Njk2IDExNS4zOSA3LjMzIDEzNS4zOSAwLjE1OTExIDAuMzE4MjEgMTAuMDY1IDM1Ljg4MyAxMC43NzkgNDkuMTU0IDAuOTQzNzggMTcuNTI1LTI0LjQ3OCAzOS40Ny0yOC4wMjcgNDYuNTY3LTUuNDc3NyAxMC45NTUtMzYuOTczIDEwLjg4Mi00MC4xIDI0LjE0Ni0zLjg2ODggMTYuNDE1LTMuODY2MyA0My43OTcgNC4wNDY1IDU5LjQ0MW05Ny4zMzctNjkxLjAxYy01LjAxMzMgMzUuNTE2LTQzLjY1OSAxMS4zMTctNTguNTM5IDIzLjc4MS0yMS4zMyAxNy44NjktNjIuNSAzMS40MzItNzAuMTI0IDM1LjM2Ny0zNS4wODggMTguMTA4LTExMC40Ny0xNS4xNDItMTI1LjYxIDQuMjY4NC0xNS45NTEgMjAuNDQ3LTAuMDczNSA2MS40NjYtOS4xNDY3IDg0LjE0OS02LjAzNTcgMTUuMDg5LTE4Ljg3NyAyMy4wMTctMjcuNDQgMzIuOTI4LTE5Ljc0OCAyMi44NTYtNjkuOTc0IDY5LjgyNC04NC43NTkgMTAwLTcuNDk3NCAxNS4zMDQtMy4yODQzIDQ0LjQyLTMuNDcwNSA2My4zNDMtMC4xMjc5MyAxMi45OTQtMC44MTAxNSAyMy4xMDQgMi40MDM0IDI4LjI3NiA0Ljk2MTYgNy45ODU4IDIzLjcyIDI4LjExMiAyNC4yMzkgNTAuNjExIDAuMjk0MTEgMTIuNzcxIDAuMDEzMyA3OC41OTEgMy4wNDg5IDg3LjY1NSAyLjMxMjYgNi45MDU1IDQuMjIgMjYuNTY1IDEwLjIxNCAzNi41ODcgMTEuMzU0IDE4Ljk4NCA0LjM4NzQgNDAuMTU3IDI3Ljg5NyA1My41MDggMTkuMDUgMTAuODE5IDQ2Ljg3OCAxMi4yMTkgODEuOTI2IDE0LjQ2MSAzMy43MDMgMi4xNTU5IDYxLjUxMi0xLjQzMDQgNzYuOTIxIDYuMTQxMSAxMS41ODUgNS42OTI3IDguNTgxNSAxNy45MzMgMTQuMjk1IDI5LjM2MSA1LjY0MDQgMTEuMjgxIDMxLjUwMyAxMS4xNTYgNDEuODA0IDQzLjQ1NSA3LjYwNTkgMjMuODQ3IDMuMDg1OSA0NC4xNTcgNi43MDc2IDY1Ljg4NyIgc3Ryb2tlPSIjMzY0ZTU5IiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTQzLjI3OCA1MTcuOTVzMjMwLjg1LTMuNjM4IDI1MC4wMS0zLjY1ODdjNy40ODIyLThlLTMgOC42MTk1IDUuMTUxOSAxNC4wMjEgMTEuNDU5IDI0LjU5NiAyOC43MTkgOTMuOTEgMTEyLjk0IDkzLjkxIDExMi45NCIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogICA8cGF0aCBkPSJtMzUuOTYxIDU3Ny43czE2NS41Mi0xLjY4NDUgMjQ4Ljc4LTEuNjg0NWM0Ljk0NzUgMCA3LjcyOTktMi44ODMzIDEwLjUzOC01LjcyOTggOS42NjExLTkuNzk0MiAyNS42MzItMjguNTkgMjUuNjMyLTI4LjU5IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0zOC40IDY0MS43MyAzOTMuMzEtNC4yNjg0IiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0iIzMzNiIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxwYXRoIGQ9Im0zOS4wMDkgNzA0LjU0IDQ4NC4xNi02LjcwNzYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSIjMzM2IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2Ij4KICAgPGcgc3Ryb2tlLXdpZHRoPSIxcHgiPgogICAgPHBhdGggZD0ibTMwMy45NiA2ODIuNTkgMTQ2LjggMS44MjkzYzEwLjUzNCAwLjEzMTI3IDE0LjM0NC0yLjYzNzQgMjUuNDg3LTYuMzcyOCAxMC40MTItMy40OTAzIDMxLjQyNC0yLjY5OSA0MS4zODUtMi43NzM4bDQwNS41Ni0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDI2LjIyIDMxNC44OWMyLjA2NzUgOS4wNTI3IDEuODQxOCA1MS43MjggNi41MDc5IDc0LjgzNSAxLjY3NDggOC4yOTM0IDguNjc1MSAxNC4wNjYgMTAuMDU1IDE0Ljg1OSA0LjkwMTUgMi44MTQ2IDEwLjgxNSA4LjE0OTggMTMuMDQ2IDE2LjA4OCA2Ljc1NzggMjQuMDQ2IDAuODc5NzIgNjguNDUyIDAuODc5NzIgMTEwLjY5IDAgNi4wOTc4IDEuNjYwMSAzMC4xNDctMi4xNTU5IDMzLjk2My0yLjU0MDggMi41NDA4LTAuMjgxNjMgMTIuOTkxLTMuNDM2OCAxNi4xNDRsLTkuODQ5NCA5Ljg0MzFjLTEwLjM2NyAxMC4zNi0xMS41OSA2LjUyNjEtMTcuNzM4IDE4LjgyMy0zLjU2NzcgNy4xMzU0IDUuNDAyNCAyMC42NzIgNy4zNTQzIDI0LjU3NiAxLjkzMjEgMy44NjQzLTEuODQyMiA0Ljc3NzctMS43OTI0IDcuNDQ2MyAwLjI1Mjg2IDEzLjU0NSAyLjI5NzUgMzczLjkzIDIuMjk3NSAzNzMuOTMiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzY1LjI0IDUxOS43OCA0LjExNiA1MDIuMTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMTE2LjUzIDUwNC4xOSAzLjg4MDYgMzEwLjk2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTMxNy42OCA1NzYuNDkgMTMwLjE5IDEuNTI0NGM0LjUxMDggMy4yNDE3IDIwLjM0NSA3Ljk2ODUgMjcuNzQ1IDQuMjY4NCAzLjE1NTUtMS41Nzc3IDkuNDE5LTUuMzg4MiAxNC4wMjUtMy45NjM2IDQuMjY3IDEuMzE5OCA2LjAxNjkgMy4xMTYzIDEwLjM2NiAzLjA0ODkgMTAuMzA0LTAuMTU5NzUgMjAuMjEyIDAuMzg3NDEgMzAuNDg5IDAuMzA0ODkgMTc3Ljg5LTEuNDI4MyAzNTYuNTktMi4xMzI1IDUzNC43Ny0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDc1LjMxIDU4Mi44OWMtMy40NDQyIDExLjM1MS0yLjEwMzQgMTIuNDM0IDMuNjU4NiAyMS4wMzcgMy43OTQ0IDUuNjY1NiA1MC44NjMgMTMuMDM4IDQxLjQ2NSAyNy4xMzUtMTAuNTM3IDE1LjgwNS0yMi44OTctNS40Nzc3LTMzLjg0My0xLjgyOTMtNS40NTI0IDEuODE3NC03LjM0OSA1LjQ1NjMtMy42NTg3IDkuMTQ2NiAyLjgwNjggMi44MDY4IDQuMDQ4IDEuODA0IDYuNTIwMyA1LjEwMDQiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjAxIDYzNi44NWM4LjMxOSAxMy4xMSAxOC44NDYgMTQuNjM1IDM1LjY3MiAxNC42MzUgMi45Mzg2IDAgNy44Ny0wLjkzMzcxIDEwLjY3MSAwIDExLjM1OSAzLjc4NjQgMjcuMTk0IDEwLjI3NiAzNi4yMDIgMjEuMTI5IDguMjggOS45NzY2IDEwLjI1MyAyMy44ODMgNy43MDIgMzcuMTA0LTYuMTY5OSAzMS45OC0xNi43MTQgNTYuOTg5LTE5LjA0NCA4Ni41NjktMS4zNDggMTcuMTE5IDQuNTA5NiAyMi41MzUgMTEuMDcxIDMzLjkyOSAxMC42NyAxOC41MjcgOC43MjQ1IDE0LjIgOC41NzE0IDM0LjI4Ni0wLjEzOTYzIDE4LjMxOSAwIDYwLjI2NCAwIDgwLjcxNCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNTEgNjU4Ljk2Yy0xMC42ODEgMC45MDQ1NC03LjEwOC01LjYwMjYtMTAuODI0LTguMDc5Ni00Ljc4NDUtMy4xODk3LTEyLjIyNy0xLjI1MS0xNi43NjktNS43OTI5LTAuNjY2MTItMC42NjYxMi04LjgwOTctNC4xMDg4LTEwLjE3NC0yLjc0NC04LjM2NDYgOC4zNjQ2LTMuMDQ4OSAyMC41NTItMy4wNDg5IDMzLjUzOGwzLjAyMiAzMzkuNyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MTcuOTkgNjUxLjAzYy0wLjIyMTcxLTIuNzAxOCAxLjkwMzUtNS41NjIxIDMuMzUzOC03LjAxMjQgMS43OTk0LTEuNzk5NCA2LjkyMjkgMS4wMDQyIDguODQxOC0wLjkxNDY2IDAuMjg3NjUtMC4yODc2NiAwLjg0MzI5LTExLjE2NCAwLjIyODY2LTEzLjU2OC0yLjA2NDgtOC4wNzQyLTIuMDU4LTI4LjY1Ny0yLjA1OC0zOC43MjF2LTczLjE3MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNjYgNjc1LjQyLTAuNDU3MzMtMzEuNTU2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc2Ni4zMiA1NzkuNjQgMC40MzExOCAxMy43OThjMy4xMzY0IDQuNjY5MiAzLjAxODIgOS42MDA3IDMuMDE4MiAxNi4zODV2MTU3LjM4IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTExMjIuOSA3NjUuOTFjLTIwMi4zMSA0LjY5MDUtNDAzLjc0LTEuMTEzOC02MDUuOTUgMy4zNTM5LTEwLjg2NCAwLjI0MDAyLTMuMzYxNS04LjU4NjMtMjguNTM3LTguNTg2MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im04NjAuMDEgNzM3LjA3cy05Ny40NDggMC44NTgwNi0xNDcuNTcgMC44NTgwNmMtNS4yNjg2IDAtNC41MTU1LTguMzI5OS03LjMwMDktOC4zMjk5LTMuOTc0NCAwLTguNjI5MiAwLjAyMDEtMTAuNTA5IDAuMDM1OS0yLjMzNDggMC4wMTk3LTEuODEwOSA4LjM2Ni00LjE0NTggOC4zNjY5LTQ2LjE2OSAwLjAxODgtMTY3LjQxLTEuMzA4LTE3NS4wNS0xLjMwOC00LjQyOTYgMC04LjU3NjMtNi40Mzk3LTEzLjEzMi02LjQzOTdoLTE0LjM5NSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im02NzUuMDEgODMxLjE3LTAuNjA5NzgtNTIxLjc3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc5OS40IDMxMy4wNiAxLjIxOTYgNDk1Ljg3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTczNi41OSAzMTIuNDUtMS4yMTk2IDcxNi40OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MzAuMDMgNjQzLjQ2IDM5Mi4zNy0zLjAxODIiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtODU5LjQ1IDMxNC45IDEuMjkzNSA1MDcuOTgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICAgPHBhdGggZD0ibTkyMS41NCAzMTAuNTkgMS43MjQ3IDUzMS43NSIgY29sb3I9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgIDxnIHN0cm9rZS13aWR0aD0iMXB4Ij4KICAgIDxwYXRoIGQ9Im03MzYuMjkgNDUzLjMxIDE4NS42OC0wLjMwNDg5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTEwNjAuOCA1MTQuOTdzLTM2My4yOC01LjYyNjItNTQ0LjY1IDIuNTIxOGMtNC4xNzc4IDAuMTg3NjktMTIuNSAxLjA2NzEtMTIuNSAxLjA2NzEtMS41NzEgMC4xMzQxLTIuMDAwOS0yLjMyNS0yLjU5MTYtMy41MDYyLTAuMDk2Ny0wLjE5MzQzLTcuMDYwOC0xLjkzMzQtNy42MjIyLTEuMzcyLTIuODkzMSAyLjg5MzEtNy42MzE3IDQuMjQ4Ny0xMi4xOTYgNC4xMTZsLTExMi4wNS0zLjI1NzgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzk5LjgyIDQ3OS42MSAxMS42NDIgNS42MDUzYzIuOTg0MSAxLjQzNjggNi41Mjg4LTAuNDc3MTIgOS45MTcxLTAuNDMxMThsMTI3LjIgMS43MjQ3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUxOS4yNSA1MTcuMTItMC40MzExOS0yMDguNjkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjkzIDM4OS43MWMxMS4wNDUgMCAzNS41MzMgMC42MTkyNyA0Mi41OC0xLjAwNCA4LjQwNTItMS45MzYyIDcuMDY2LTYuOTUzOCAxNC4xOTctNi45NTM4IDcuODA5NSAwIDYuNTQyOSA4LjA2MjQgMjAuMTQyIDguMDYyNCAxMy45OTEgMCA0NC45NzcgMC4zNzg4NiA2My45NCAwLjM3ODg2IDEyLjA4NCAwIDgyLjAwMyAwLjMwNDg5IDkzLjYwMSAwLjMwNDg5IDguNzYwNSAwIDEzLjE2LTIuMjg4MyAyMS4zNDItNy4wMTI0IDcuMTk1Mi00LjE1NDEgMi4wNTQ2LTkuNDkxNCAyMC40MjgtOC44NDE4IDIzLjE0NSAwLjgxODMzIDEyLjY0MyAxNC4wMjUgMzIuMzE4IDE0LjAyNWgxNTAuOTJjMTQuMzMyIDAtNC4xMTkxLTEzLjExIDI5LjI2OS0xMy40MTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4Ij4KICAgPHRleHQgeD0iNTg4LjY3OTU3IiB5PSI3MzUuODA0NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU4OC42Nzk1NyIgeT0iNzM1LjgwNDYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TGluY29sbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3MDkuODcxODMiIHk9Ii04MDIuMzc3MzgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjcwOS44NzE4MyIgeT0iLTgwMi4zNzczOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldvb2RsYXduPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTYyLjExOTI2IiB5PSItNzcxLjk2ODE0IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjIuMTE5MjYiIHk9Ii03NzEuOTY4MTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5FZGdlbW9vcjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5OC4zMDQ4NyIgeT0iLTczOC4zNjY0NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk4LjMwNDg3IiB5PSItNzM4LjM2NjQ2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTkyLjEyMjg2IiB5PSItNjc3LjIwMzk4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTIuMTIyODYiIHk9Ii02NzcuMjAzOTgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5IaWxsc2lkZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5Ny4zMjcwOSIgeT0iLTg2Mi42MTQwNyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk3LjMyNzA5IiB5PSItODYyLjYxNDA3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Um9jazwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU4Ny4zNzAxOCIgeT0iLTkyNi4xMzY2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1ODcuMzcwMTgiIHk9Ii05MjYuMTM2NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkNlbnRyYWw8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODczLjgzMjI4IiB5PSI1NzcuMDMyNDciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3My44MzIyOCIgeT0iNTc3LjAzMjQ3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI4NzUuOTY2NDkiIHk9IjUxMC4yNjE4MSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODc1Ljk2NjQ5IiB5PSI1MTAuMjYxODEiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yMXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg4MS4zMTY1OSIgeT0iNDUwLjE5ODc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI4ODEuMzE2NTkiIHk9IjQ1MC4xOTg3NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjI5dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNjE1Ljc5MjQ4IiB5PSIzODcuNzQ3MTYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjYxNS43OTI0OCIgeT0iMzg3Ljc0NzE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Mzd0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI0ODQuNjkwMzciIHk9IjQ4MS42NTI4NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDg0LjY5MDM3IiB5PSI0ODEuNjUyODYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yNXRoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU2My4wNDY3NSIgeT0iNTEzLjM2MTMzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjMuMDQ2NzUiIHk9IjUxMy4zNjEzMyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI0MzMuNTgwNzUiIHk9Ii00NjAuNzMzMTIiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQzMy41ODA3NSIgeT0iLTQ2MC43MzMxMiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkFtaWRvbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjQwNS41MzA5OCIgeT0iLTUyMy41NDAxNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDA1LjUzMDk4IiB5PSItNTIzLjU0MDE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QXJrYW5zYXM8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3NDUuNDg0NjIiIHk9Ii0zNzIuNTg1OTQiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc0NS40ODQ2MiIgeT0iLTM3Mi41ODU5NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTYuNzI4MzMiIHk9Ii01MzEuMjU5MjgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5Ni43MjgzMyIgeT0iLTUzMS4yNTkyOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldhY288L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTUuNDM0ODEiIHk9Ii0xMjIuNTAyOTUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5NS40MzQ4MSIgeT0iLTEyMi41MDI5NSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hemllPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgeD0iNjk1Ljc3Mjk1IiB5PSIxNjIuMDY4NzciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY5NS43NzI5NSIgeT0iMTYyLjA2ODc3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Wm9vPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjI0MC41ODk5NyIgeT0iNTc0LjQ0NTQzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyNDAuNTg5OTciIHk9IjU3NC40NDU0MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjA2LjAzMTc1IiB5PSI1MTEuNjM2NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIwNi4wMzE3NSIgeT0iNTExLjYzNjYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjYyMC40NDMxMiIgeT0iLTUwNi42ODIxOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNjIwLjQ0MzEyIiB5PSItNTA2LjY4MjE5IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TmltczwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzNzAuMjE2ODYiIHk9IjY5OC44NDAwOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzcwLjIxNjg2IiB5PSI2OTguODQwMDkiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYXBsZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCBkPSJtMzY3LjkxIDEwMTBoMjYzLjAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxnIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCI+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzM2LjI2NzQ2IiB5PSItNDMzLjEzNzc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3MzYuMjY3NDYiIHk9Ii00MzMuMTM3NzYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NZXJpZGlhbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NzIuODMyMTUiIHk9IjY0MC4yMDUyNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTcyLjgzMjE1IiB5PSI2NDAuMjA1MjYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5Eb3VnbGFzPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjQ5OS40ODk2MiIgeT0iMTAwOC42MDY5IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI0OTkuNDg5NjIiIHk9IjEwMDguNjA2OSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjQ3dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjE2LjY0NTQzIiB5PSI3MjUuOTgyOTciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIxNi42NDU0MyIgeT0iNzI1Ljk4Mjk3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9Ijc3NC44NzU2MSIgeT0iLTUwOC4xODk3MyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNzc0Ljg3NTYxIiB5PSItNTA4LjE4OTczIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWNDbGVhbjwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDI4Ny4zNikiIGQ9Im0zNjQuMTYgNjU4LjQzIDI5OS41MS0xLjAxMDJjNi40OTg3LTAuMDIxOSA2Ljk3NzIgOS4yNTQxIDE2LjU5NiA5LjM5MjUgMTIuMDU0IDAuMTczMzkgMjkuMTExLTAuNTM1NzIgNTQuMTE0LTAuMzAxMSIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8dGV4dCB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hY0FydGh1cjwvdHNwYW4+PC90ZXh0PgogIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzgwLjg0NjA3IiB5PSItNDkwLjI0NTk3IiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc4MC44NDYwNyIgeT0iLTQ5MC4yNDU5NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlNlbmVjYTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMjg3LjM2KSIgZD0ibTM2Ny43IDUzNy4yMSAxNDEuMjgtMS4wMTAyYzYuNDktMC4wNDY0IDEyLjc4MSA3LjIzNTQgMTkuMTkzIDcuMzIzNiA1NS45MjQgMC43Njg5IDE1OC42OS0wLjE3MzMzIDIzNi41MS0xLjAxMDIgNy44Mzk2LTAuMDg0MyAyMi42MzEtMTkuODU0IDMwLjMwNS0yMC40NTYgMjIuMjY2LTEuMzUxOCA0NS4xNzktMC41MDUwNyA2Ny42OC0wLjUwNTA3IDE2LjE0Ny0wLjYzMjQxIDMuNjEwMiAyMC43MDggMjYuNzY5IDIwLjcwOGwyNDMuNDUtMS4wMTAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDx0ZXh0IHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+UGF3bmVlPC90c3Bhbj48L3RleHQ+CiAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAyODcuMzYpIiBkPSJtNTU0LjI5IDcyMS40My00LjI4NTctMTc4LjIxLTIuODU3MS00NDAuNzEtMC4zNTcxNC03OS4yODYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1MjkuNjI1MzEiIHk9Ii01NTAuODQ3NzgiIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTI5LjYyNTMxIiB5PSItNTUwLjg0Nzc4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QnJvYWR3YXk8L3RzcGFuPjwvdGV4dD4KIDwvZz4KPC9zdmc+Cg==", "public": true - }, - { - "link": "/api/images/system/map_marker_image_0.png", - "title": "Map marker image 0", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_0.png", - "publicResourceKey": "CdCrVxsjA4EAiFaXK4a7K2MZFMeEuGeD", - "mediaType": "image/png", - "data": "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", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_1.png", - "title": "Map marker image 1", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_1.png", - "publicResourceKey": "DF3fuPXua9Vi3o3d9Nz2I1LXDTwEs2Tv", - "mediaType": "image/png", - "data": "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", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_2.png", - "title": "Map marker image 2", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_2.png", - "publicResourceKey": "rz5SFAw2Sg5T2EyXNdwLycoDwf4QbMiZ", - "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAyUSURBVGiB7Zp7kFRVesB/5/S9PdMz/ZoHMwo4MICDuoGVIYICIuzGcn0vC+oWGuNjs8mua9ySP4wpgyaiVVupbHYTsLJmNT7WNXExwqqzrq8g4oNxdXUgyEMQARmZd3fPTE/3vfd8+ePenhlgBsFlrFSqb9Wpvn3vd77f+b7zne87ffsqjv+wE4nYDQqWl5aWfDUcLqkAyOUHunID+Q8EnkilMo8C7gnoPPaRTCYnVyQT71+1bKl80PK+HGw9KPv27ZPde3bLjp075NVXX5FLL7lYKpLx9yoqKuqOR6f6PIFYLFZtW7r54YcfqV+4aBEdHe3ywm+e39eb6etzPZfS0kj5woUX1EUipWrj6xtZedddu11P5mYymc5j6Q19HrgsUrL67r/7+8VLly7j8cce3d3X29vZ0DB9yplnfWXcrFmzxjU2NiaBXevWrUsv/trXKmzbqnz/9+9VDuTyz35hi2OxWHV1ZbJ1245d1ltvvpFtb293Kyoq7LKystKysnLKy8soKyujtDTCxx/vSW3fsT3c0NAQWbpkiZvp7a9Np9Ndo+nWxwJrLYvmzV9gAaxbt/75urrxd592Wp0Oh0tWHSkbiUQSv3unuQlgxoyZltZm0TF1H+umUnrC1KlTAaipqUpESmMzFIRjsVj3SPJTpkyJA0ycOBGMnviFwSISLolEAAiHbftYsgAlJREbwA6HESUlXxg8lkcRXAQXwUVwEVwEF8FFcBH8/xhsnZC0ksw49eQPI5mmNtP54ccAIvqgqbz4aYn8zYoTUXXcFnueyZ8eXtleZt75iQnpU0VUvYiqB5mvu5p+XH9w8RtgnJMOLut/7rd4+fpRBcS52hz65csnHdxQ8clZnyuT3NV40sHRUnfq58mUWFJ70sEn+yiCi+AiuAgugovgIrgILoKL4CK4CC6Ci+D/Q+Djf/higk8Jzs0IMjIGYDGAp0AUeBbiHf3Xs/HGAHyYlYaRX0EYC4txNeIFugvWHyXzua8cnDjYGMBoQIFhRFfLmLjaCxqAw8iuHing/nCwGlLuMrKrveNfnccPFnyLtQ8c0a1jElye8sGFAYwUSCN54Q8GB4ljKKpHkBmLOZbB4FLgjhLVYxNcDFnkMXJUj03m0kOKR0sgYzLHRvlwpcDYI7oaGYvl5HB4ZRrJ1cf9fP5E/5NwQUKM7uoTOI4/ql38kmgUOCMnEHMCL819sag2jJJAxgIs+HNY6PGlpUxXDQWXw5dXjxH8SFZBPf7SyqKrMQLKG7b/OkpmTBJI0BSjbwTGYo6Ni5+ZjMJDj1wkxmQ5iV+VsBh9BzImKbNQFhWjp8wx21c7dKIV9A94IxaJsdplZt9574JQVcUdpr3rzlEHdzLASslpg19EofLMMa3dc0Z9c9YMXT+s7/GCo9FojWWph87+6tmX3XTTzT7XA/F4xutXr4fyOuQZVQUQ0tLphY1nlcn5YqgAuOyyy3inefOtH+36aLJr5Obe3t72o4w68kIsFptuW7pp5d33TPne928hm83yLz+6b9PVb/4niRK9QNfUoquqUaUREEEG+jGd7Zi2Dnpy3qYHGr7OFdcsX2BZFs899ywP/fznu11PLslkMjtHBScSiXrL0m+uXr3mlEWLFrN58+auxD+u2HZWhb0gcvkyShZ/Ax2N+70KPcVvJpMm999NZJ99mi1dzsb3rviLGbNmz6rY0rKFVavubTWG83p6ej4psAbfr66trS03xtlw98p76s+bN5+nnvzFtouevK/s1AnJM+I/vB37j6aDziJeCtxhzUkhTgoYwJpchz3zbJI7fj/pzA829f6iR/bPPW9e9aS6utjbb715YWVl1SOZTMY5DGzb6scXf+OSS6+48kqanntu55+99shkOyLx8uuvIjSuDEzq6Ob5TdzgPJ9GhT2sCbV4W1vK57R+FP9lOrT33PnzKjOZTM2OD7dFB3L5FwaDq6KifGYiXvn95ddey4fbtmWv2fhIiVUqpbpMEao2SH4fiKCMgAbRggSuVkKwEQz22q4iVKtQEYUtJvzdlvX6+bq67PJrr41sbm6+VVv8W1dX7/9oADH6b//0+us1QO/jD6xPhGWSCgsqLJj8PsTdjzj7Ma7fxDkAzn5wjry+H3H2YfL7UGGDCguJEqnPPf3YOoDrrrtOe56+C8CKRqPjotHoN+fMmcObb7zRelsk9W1lC4QFCRlM9yfoKnsoEgOLVWCxDLfYBRwwnXmwDIQVyoMbo6lrfrq5+dCsxsbaaHlkqTFSpUMhvjV79mwLwHvjldewBGxQlqBswXn3Y6T/EDhtiNOGuG2I2444QXPb/WtOGzhtmL7PcN7di7IFFegiJDq3+ZVXAWbMmGlrzRJLKc6/4IJFGGO4MdQ+gxAQEn/2LcH0u+Sa27HO0IRq/V+MSqnBOUZARMAD75DB2w4mq8AKWkggpPiOtJ3dYgznzTuPt996+3xLoc8+vaGBlpaWzFybrygtqCPgeODtcTFtBl1hUBHfGgl+wNGv8FIayWjE6KCfD1UhBVqotPWZO3Zs7506dVpUaT1Lh21rPED7oUNtKH8OUYLSoHTwWRiEAsmBDIA4gCPIAJh8YL3lyw7vi5JAJ7QdamvXWmPbofGW0qEYQL4/0zeYjdTRTQ0Oxp9/Svx9jvKAkBocsCh1dP9AZ76vNwOglI5bnuflAaukPBo9bM8UpMIjvxeiWAUbATHK3/yNJM/h30vKozEAz/Ny2nXddoCKyqrKwc5GDYFMUJmM8peLqyCvkH6FZP1zXP+eGBXIFvQcrquyqroyALdrxGzv7u5i6rTTE3lX0gUL/DIYPPfwFDh+k5xCBhSS1Ui/9s9zQ/cLz0rEGxqEGMWAK92T6yfHu7q6MCLbtSj1UtPzTcTjcfW0E3t5EBSkv0FgPgAMQgtWa/9azpcZHICrhvR48B+52CvRaFS9/PJLKKVe1Mao9e+++zsAtk9rnIwbLBFHIQ5IACWvkJxGBjSSDeDZ4HxAIznty+SV38chGIA/PXumzZoK0PJBCyLq1zqdTn/U2dHxbmtrKxddfmXj1r7QRr9jMH/5Ye4d8OdV+odZ3F+AqyG3F/oFelr62PQnl14667PWVrq6ut5JpVJ7giLBygfWrMYOh3ll/pLx4iojR7p3QMGgpQX4kPUE8OFuF0chrjIvzL78VDsc5sEHH0SLWkmQLuhOp5v27t376tatW7nk8iun/UN8VhM5BblASS5w53BowdXD4L7Lg8EG7Z6SM36z+MILp25p2cqBA/s3dKXTLxRSBeDvtUpL7M0PPfRwYtLken791z9Y++fevmWE/WJBIelbgJbDtz4mePblBksrcPU/ubVrF65Yuayzs50Vt6/ozuXduel0etdhYIB4PH5RVWXy+WeeWR8aV1PDz+6/56W//PDFxbpELGULgwVEcwSYoWXkKExOuatqGl9b8p3vfb2vt5/b/uoWtyfVe0kqlXqpwDpql1lVlbwhUhr52VNPrQ3PPuccNm16PbXrR3f+9pvm0NV+pWEwhQKIqKHnm57iV9nydc6Smxc1zm5MHvj0AHfecUeuv7f/u509PY8N5wyCReRcYCEw6YknHi9bcfvtl9276r7qG2+6Gdd12bhhQ/rghhe3TdmywT4l2zkhEeIUgJTLZ62RygPbT5/rlv/xvLOmnzE9ns/lWb9uPY8++u9tP/3JPzd9e/nyLLAXeE0ptRlAicgk4BZgfDAGc/DgQb1790fWrT+45Zz58xdMue+++0kkk/5N8RO2iPiZ0BiMCMbz8FyXzq4u7l91L5ub3969Zs2/Np/eMM2rrT21YKQBPgPWKBFZAyQA093drTzPobu7uyPV3XNbR2enam5uZu3atdTW1LDsqqtYeMEipk2b5m8GANd12bVzJ69vfI2n1/6Kjo5OvrVsKefOPZeqqkpJJCtXJ5OJinBpRJLxeOF3bI8FZIAYoEN2SHmeJ6GQ2CiMUipUP2UK199wI59+2sp/rVvP6tVryKRTOE4eAcJ2mFg8wfgJE5nZeA4TJ4yntmYcSimUUsaydMi2wxIKKTXM6n4lIuMCV08m2O52dHSQzfbpvkxvZSqTbkinUnWpVDqUzvTS29dHNpvFcfy6aNsWkUgp0fJyYrEYiUTcSybin8RjiZ2lZeXd0WjUra6uDg2L/z3A6uHBNQNYAEwHqvAXTTl4Kp3O9HhOvk+FGMhmHXHdHGLEE8CytNY6rCKRsPY8VRoOh8tisfIkhFxgIAB2AtuA15VS20ZcTsEgEsBM4DTgFKASiAClQAnBig7EC8/8BoAc0AekgE+B/cAWpVTqSMb/AlY1WXIncMcxAAAAAElFTkSuQmCC", - "public": true - }, - { - "link": "/api/images/system/map_marker_image_3.png", - "title": "Map marker image 3", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "map_marker_image_3.png", - "publicResourceKey": "KfPfTuvKCeAnmTcKcrvZQHfdU0TPArWY", - "mediaType": "image/png", - "data": "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", - "public": true } + ], + "scada": false, + "tags": [ + "building", + "interior", + "venue", + "inside", + "room", + "office", + "manufacturing", + "floor", + "plant", + "storage", + "warehouse", + "depot" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/image_map_deprecated.json b/application/src/main/data/json/system/widget_types/image_map_deprecated.json new file mode 100644 index 0000000000..23cf7f7a14 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/image_map_deprecated.json @@ -0,0 +1,102 @@ +{ + "fqn": "maps_v2.image_map", + "name": "Image Map", + "deprecated": true, + "image": "tb-image;/api/images/system/image_map_system_widget_image.png", + "description": "Displays the indoor or relative location of the entities on the image map. Useful to display floor maps, smart parking, etc. Entity coordinates are expected to be in the range from 0 to 1. Highly customizable via custom markers, marker tooltips, and widget actions. ", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "", + "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-map-widget-settings-legacy", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"image-map\",\"mapImageUrl\":\"tb-image;/api/images/system/image_map_system_widget_map_image.svg\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + }, + "tags": [ + "building", + "interior", + "venue", + "inside", + "room", + "office", + "manufacturing", + "floor", + "plant", + "storage", + "warehouse", + "depot" + ], + "resources": [ + { + "link": "/api/images/system/image_map_system_widget_image.png", + "title": "\"Image Map\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "image_map_system_widget_image.png", + "publicResourceKey": "hDdSISQr6elribOYD6T3uePXZI5WvNtM", + "mediaType": "image/png", + "data": "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", + "public": true + }, + { + "link": "/api/images/system/image_map_system_widget_map_image.svg", + "title": "\"Image Map\" system widget map image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "image_map_system_widget_map_image.svg", + "publicResourceKey": "QKRIYhDeBGwjaeIS601VvNLSsvZ25DRj", + "mediaType": "image/svg+xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTEzNC41IiBoZWlnaHQ9Ijc2Mi43OCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI3LjA3MSAtMzA3LjkpIj4KICA8ZyBmaWxsPSJub25lIj4KICAgPHBhdGggZD0ibTkwNi4wMyA3MDYuMTMgMy40MjkyIDE3Ljc5Nm0tODgwLjg5IDQxLjEyMWMxNTAuNDQgNi44MzM0IDE0Ni4zOS0yNi4zMzQgMTY2LjQzLTI5LjMyIDM2LjE0NC01LjM4NDggMTE0LjI5LTYuNTI1NCAxNDguMzMtOC42MjM1IDQzLjM3OC0yLjY3MzggMTQxLjc2LTExLjIzMSAxODguODYtMTkuODM0IDM5LjgxMS03LjI3MjggMjIxLjM3LTAuODYyMzUgMzE5LjA3LTAuODYyMzUgNzAuODI3IDAgMTQ2LjkyLTEuNzI0NyAyMTguMTgtMS43MjQ3LTMxLjYyIDAgMTE3Ljg2LTIuNTg3MSA4Ni4yMzYtMi41ODcxbS0yNS4wOTEtNjguMTI2Yy01Mi44IDM0Ljc4NS02NS44OTUgNTEuNzQ5LTk1LjYzOSA4MS40OTMtMjQuOTMxIDI0LjkzMS0xNDAuNC0xOS4xMzktMTc4Ljk0IDM2LjY1LTEyLjI4MSAxNy43NzctNDcuMDAzIDQ2LjU0Ny02NS4xMDggNTkuMDcxLTIwLjEwNSAxMy45MDgtNTYuMDM3IDQ0Ljk1Ny02Ny43NjkgNzMuMDc4LTQuODAxNSAxMS41MDktMTMuMzggMzUuOTkzLTIzLjQ0OSA0Ni4wNjItMTAuNDk3IDEwLjQ5Ny0zOC4zNzcgNi4zODU3LTQ0LjAyMyAxNy42NDgtMTkuMDA1IDM3LjkwOC0yNS40NjUgMTAwLjkyLTY3LjYxOCAxMDIuMDVtMTkuMjgyLTYyNC4wMWMzNC42NTktMS44NzM4IDg0LjAyNyA3LjM5MTMgMTA5LjktNC4yODU0IDEzLjI4Mi01Ljk5NDEgNDEuNDA3LTIuNDYxNCA2Ni44MjktMi4zMjA1IDM1LjMyMiAwLjE5NTc4IDY0LjM4MiAwLjYzNDc3IDEwMS45MiA1LjAyMzIgMjUuMDMgMi45MjY1IDQ0LjY2MyAzNC4yODcgNTguNTI3IDUwLjY0NCAxNy4wOTkgMjAuMTczIDYyLjc2NC0xLjcxNDcgNjYuMzA2IDMyLjEzNCA1LjEwMjcgNDguNzY2LTYuMzI4NCA3OC42MzcgNi4xNDExIDk3LjM0MiAxOS45NjkgMjkuOTU0IDUwLjQ4NiAxNy44NTYgNDQuNjE5IDgzLjk3MW0tNDcyLjQ1LTM3OC43OWM0LjY0MzUgMjMuNzI5IDE1LjA2OSA3Mi43NzYgMTkuMDYxIDEzMC42NCAwLjg3MjA2IDEyLjY0IDUuNDQ3MiAyNC45OTMgNC4yMjIzIDQ1LjI3OC0yLjUxNzIgNDEuNjg4LTE1LjcxNyA0My42NzctMTUuMDkxIDYwLjM2NSAxLjQzMiAzOC4xODIgMzAuNjE0IDkzLjgzNyAzMC42MTQgMTM5LjcgMCAyNC4xODEtMi42Njk2IDExNS4zOSA3LjMzIDEzNS4zOSAwLjE1OTExIDAuMzE4MjEgMTAuMDY1IDM1Ljg4MyAxMC43NzkgNDkuMTU0IDAuOTQzNzggMTcuNTI1LTI0LjQ3OCAzOS40Ny0yOC4wMjcgNDYuNTY3LTUuNDc3NyAxMC45NTUtMzYuOTczIDEwLjg4Mi00MC4xIDI0LjE0Ni0zLjg2ODggMTYuNDE1LTMuODY2MyA0My43OTcgNC4wNDY1IDU5LjQ0MW05Ny4zMzctNjkxLjAxYy01LjAxMzMgMzUuNTE2LTQzLjY1OSAxMS4zMTctNTguNTM5IDIzLjc4MS0yMS4zMyAxNy44NjktNjIuNSAzMS40MzItNzAuMTI0IDM1LjM2Ny0zNS4wODggMTguMTA4LTExMC40Ny0xNS4xNDItMTI1LjYxIDQuMjY4NC0xNS45NTEgMjAuNDQ3LTAuMDczNSA2MS40NjYtOS4xNDY3IDg0LjE0OS02LjAzNTcgMTUuMDg5LTE4Ljg3NyAyMy4wMTctMjcuNDQgMzIuOTI4LTE5Ljc0OCAyMi44NTYtNjkuOTc0IDY5LjgyNC04NC43NTkgMTAwLTcuNDk3NCAxNS4zMDQtMy4yODQzIDQ0LjQyLTMuNDcwNSA2My4zNDMtMC4xMjc5MyAxMi45OTQtMC44MTAxNSAyMy4xMDQgMi40MDM0IDI4LjI3NiA0Ljk2MTYgNy45ODU4IDIzLjcyIDI4LjExMiAyNC4yMzkgNTAuNjExIDAuMjk0MTEgMTIuNzcxIDAuMDEzMyA3OC41OTEgMy4wNDg5IDg3LjY1NSAyLjMxMjYgNi45MDU1IDQuMjIgMjYuNTY1IDEwLjIxNCAzNi41ODcgMTEuMzU0IDE4Ljk4NCA0LjM4NzQgNDAuMTU3IDI3Ljg5NyA1My41MDggMTkuMDUgMTAuODE5IDQ2Ljg3OCAxMi4yMTkgODEuOTI2IDE0LjQ2MSAzMy43MDMgMi4xNTU5IDYxLjUxMi0xLjQzMDQgNzYuOTIxIDYuMTQxMSAxMS41ODUgNS42OTI3IDguNTgxNSAxNy45MzMgMTQuMjk1IDI5LjM2MSA1LjY0MDQgMTEuMjgxIDMxLjUwMyAxMS4xNTYgNDEuODA0IDQzLjQ1NSA3LjYwNTkgMjMuODQ3IDMuMDg1OSA0NC4xNTcgNi43MDc2IDY1Ljg4NyIgc3Ryb2tlPSIjMzY0ZTU5IiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTQzLjI3OCA1MTcuOTVzMjMwLjg1LTMuNjM4IDI1MC4wMS0zLjY1ODdjNy40ODIyLThlLTMgOC42MTk1IDUuMTUxOSAxNC4wMjEgMTEuNDU5IDI0LjU5NiAyOC43MTkgOTMuOTEgMTEyLjk0IDkzLjkxIDExMi45NCIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogICA8cGF0aCBkPSJtMzUuOTYxIDU3Ny43czE2NS41Mi0xLjY4NDUgMjQ4Ljc4LTEuNjg0NWM0Ljk0NzUgMCA3LjcyOTktMi44ODMzIDEwLjUzOC01LjcyOTggOS42NjExLTkuNzk0MiAyNS42MzItMjguNTkgMjUuNjMyLTI4LjU5IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0zOC40IDY0MS43MyAzOTMuMzEtNC4yNjg0IiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0iIzMzNiIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxwYXRoIGQ9Im0zOS4wMDkgNzA0LjU0IDQ4NC4xNi02LjcwNzYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSIjMzM2IiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2Ij4KICAgPGcgc3Ryb2tlLXdpZHRoPSIxcHgiPgogICAgPHBhdGggZD0ibTMwMy45NiA2ODIuNTkgMTQ2LjggMS44MjkzYzEwLjUzNCAwLjEzMTI3IDE0LjM0NC0yLjYzNzQgMjUuNDg3LTYuMzcyOCAxMC40MTItMy40OTAzIDMxLjQyNC0yLjY5OSA0MS4zODUtMi43NzM4bDQwNS41Ni0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDI2LjIyIDMxNC44OWMyLjA2NzUgOS4wNTI3IDEuODQxOCA1MS43MjggNi41MDc5IDc0LjgzNSAxLjY3NDggOC4yOTM0IDguNjc1MSAxNC4wNjYgMTAuMDU1IDE0Ljg1OSA0LjkwMTUgMi44MTQ2IDEwLjgxNSA4LjE0OTggMTMuMDQ2IDE2LjA4OCA2Ljc1NzggMjQuMDQ2IDAuODc5NzIgNjguNDUyIDAuODc5NzIgMTEwLjY5IDAgNi4wOTc4IDEuNjYwMSAzMC4xNDctMi4xNTU5IDMzLjk2My0yLjU0MDggMi41NDA4LTAuMjgxNjMgMTIuOTkxLTMuNDM2OCAxNi4xNDRsLTkuODQ5NCA5Ljg0MzFjLTEwLjM2NyAxMC4zNi0xMS41OSA2LjUyNjEtMTcuNzM4IDE4LjgyMy0zLjU2NzcgNy4xMzU0IDUuNDAyNCAyMC42NzIgNy4zNTQzIDI0LjU3NiAxLjkzMjEgMy44NjQzLTEuODQyMiA0Ljc3NzctMS43OTI0IDcuNDQ2MyAwLjI1Mjg2IDEzLjU0NSAyLjI5NzUgMzczLjkzIDIuMjk3NSAzNzMuOTMiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzY1LjI0IDUxOS43OCA0LjExNiA1MDIuMTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMTE2LjUzIDUwNC4xOSAzLjg4MDYgMzEwLjk2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTMxNy42OCA1NzYuNDkgMTMwLjE5IDEuNTI0NGM0LjUxMDggMy4yNDE3IDIwLjM0NSA3Ljk2ODUgMjcuNzQ1IDQuMjY4NCAzLjE1NTUtMS41Nzc3IDkuNDE5LTUuMzg4MiAxNC4wMjUtMy45NjM2IDQuMjY3IDEuMzE5OCA2LjAxNjkgMy4xMTYzIDEwLjM2NiAzLjA0ODkgMTAuMzA0LTAuMTU5NzUgMjAuMjEyIDAuMzg3NDEgMzAuNDg5IDAuMzA0ODkgMTc3Ljg5LTEuNDI4MyAzNTYuNTktMi4xMzI1IDUzNC43Ny0zLjA0ODkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDc1LjMxIDU4Mi44OWMtMy40NDQyIDExLjM1MS0yLjEwMzQgMTIuNDM0IDMuNjU4NiAyMS4wMzcgMy43OTQ0IDUuNjY1NiA1MC44NjMgMTMuMDM4IDQxLjQ2NSAyNy4xMzUtMTAuNTM3IDE1LjgwNS0yMi44OTctNS40Nzc3LTMzLjg0My0xLjgyOTMtNS40NTI0IDEuODE3NC03LjM0OSA1LjQ1NjMtMy42NTg3IDkuMTQ2NiAyLjgwNjggMi44MDY4IDQuMDQ4IDEuODA0IDYuNTIwMyA1LjEwMDQiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjAxIDYzNi44NWM4LjMxOSAxMy4xMSAxOC44NDYgMTQuNjM1IDM1LjY3MiAxNC42MzUgMi45Mzg2IDAgNy44Ny0wLjkzMzcxIDEwLjY3MSAwIDExLjM1OSAzLjc4NjQgMjcuMTk0IDEwLjI3NiAzNi4yMDIgMjEuMTI5IDguMjggOS45NzY2IDEwLjI1MyAyMy44ODMgNy43MDIgMzcuMTA0LTYuMTY5OSAzMS45OC0xNi43MTQgNTYuOTg5LTE5LjA0NCA4Ni41NjktMS4zNDggMTcuMTE5IDQuNTA5NiAyMi41MzUgMTEuMDcxIDMzLjkyOSAxMC42NyAxOC41MjcgOC43MjQ1IDE0LjIgOC41NzE0IDM0LjI4Ni0wLjEzOTYzIDE4LjMxOSAwIDYwLjI2NCAwIDgwLjcxNCIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNTEgNjU4Ljk2Yy0xMC42ODEgMC45MDQ1NC03LjEwOC01LjYwMjYtMTAuODI0LTguMDc5Ni00Ljc4NDUtMy4xODk3LTEyLjIyNy0xLjI1MS0xNi43NjktNS43OTI5LTAuNjY2MTItMC42NjYxMi04LjgwOTctNC4xMDg4LTEwLjE3NC0yLjc0NC04LjM2NDYgOC4zNjQ2LTMuMDQ4OSAyMC41NTItMy4wNDg5IDMzLjUzOGwzLjAyMiAzMzkuNyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MTcuOTkgNjUxLjAzYy0wLjIyMTcxLTIuNzAxOCAxLjkwMzUtNS41NjIxIDMuMzUzOC03LjAxMjQgMS43OTk0LTEuNzk5NCA2LjkyMjkgMS4wMDQyIDguODQxOC0wLjkxNDY2IDAuMjg3NjUtMC4yODc2NiAwLjg0MzI5LTExLjE2NCAwLjIyODY2LTEzLjU2OC0yLjA2NDgtOC4wNzQyLTIuMDU4LTI4LjY1Ny0yLjA1OC0zOC43MjF2LTczLjE3MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MjguNjYgNjc1LjQyLTAuNDU3MzMtMzEuNTU2IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc2Ni4zMiA1NzkuNjQgMC40MzExOCAxMy43OThjMy4xMzY0IDQuNjY5MiAzLjAxODIgOS42MDA3IDMuMDE4MiAxNi4zODV2MTU3LjM4IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTExMjIuOSA3NjUuOTFjLTIwMi4zMSA0LjY5MDUtNDAzLjc0LTEuMTEzOC02MDUuOTUgMy4zNTM5LTEwLjg2NCAwLjI0MDAyLTMuMzYxNS04LjU4NjMtMjguNTM3LTguNTg2MyIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im04NjAuMDEgNzM3LjA3cy05Ny40NDggMC44NTgwNi0xNDcuNTcgMC44NTgwNmMtNS4yNjg2IDAtNC41MTU1LTguMzI5OS03LjMwMDktOC4zMjk5LTMuOTc0NCAwLTguNjI5MiAwLjAyMDEtMTAuNTA5IDAuMDM1OS0yLjMzNDggMC4wMTk3LTEuODEwOSA4LjM2Ni00LjE0NTggOC4zNjY5LTQ2LjE2OSAwLjAxODgtMTY3LjQxLTEuMzA4LTE3NS4wNS0xLjMwOC00LjQyOTYgMC04LjU3NjMtNi40Mzk3LTEzLjEzMi02LjQzOTdoLTE0LjM5NSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im02NzUuMDEgODMxLjE3LTAuNjA5NzgtNTIxLjc3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTc5OS40IDMxMy4wNiAxLjIxOTYgNDk1Ljg3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTczNi41OSAzMTIuNDUtMS4yMTk2IDcxNi40OSIgY29sb3I9IiMwMDAwMDAiLz4KICAgIDxwYXRoIGQ9Im01MzAuMDMgNjQzLjQ2IDM5Mi4zNy0zLjAxODIiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtODU5LjQ1IDMxNC45IDEuMjkzNSA1MDcuOTgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICAgPHBhdGggZD0ibTkyMS41NCAzMTAuNTkgMS43MjQ3IDUzMS43NSIgY29sb3I9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgIDxnIHN0cm9rZS13aWR0aD0iMXB4Ij4KICAgIDxwYXRoIGQ9Im03MzYuMjkgNDUzLjMxIDE4NS42OC0wLjMwNDg5IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTEwNjAuOCA1MTQuOTdzLTM2My4yOC01LjYyNjItNTQ0LjY1IDIuNTIxOGMtNC4xNzc4IDAuMTg3NjktMTIuNSAxLjA2NzEtMTIuNSAxLjA2NzEtMS41NzEgMC4xMzQxLTIuMDAwOS0yLjMyNS0yLjU5MTYtMy41MDYyLTAuMDk2Ny0wLjE5MzQzLTcuMDYwOC0xLjkzMzQtNy42MjIyLTEuMzcyLTIuODkzMSAyLjg5MzEtNy42MzE3IDQuMjQ4Ny0xMi4xOTYgNC4xMTZsLTExMi4wNS0zLjI1NzgiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtMzk5LjgyIDQ3OS42MSAxMS42NDIgNS42MDUzYzIuOTg0MSAxLjQzNjggNi41Mjg4LTAuNDc3MTIgOS45MTcxLTAuNDMxMThsMTI3LjIgMS43MjQ3IiBjb2xvcj0iIzAwMDAwMCIvPgogICAgPHBhdGggZD0ibTUxOS4yNSA1MTcuMTItMC40MzExOS0yMDguNjkiIGNvbG9yPSIjMDAwMDAwIi8+CiAgICA8cGF0aCBkPSJtNDMyLjkzIDM4OS43MWMxMS4wNDUgMCAzNS41MzMgMC42MTkyNyA0Mi41OC0xLjAwNCA4LjQwNTItMS45MzYyIDcuMDY2LTYuOTUzOCAxNC4xOTctNi45NTM4IDcuODA5NSAwIDYuNTQyOSA4LjA2MjQgMjAuMTQyIDguMDYyNCAxMy45OTEgMCA0NC45NzcgMC4zNzg4NiA2My45NCAwLjM3ODg2IDEyLjA4NCAwIDgyLjAwMyAwLjMwNDg5IDkzLjYwMSAwLjMwNDg5IDguNzYwNSAwIDEzLjE2LTIuMjg4MyAyMS4zNDItNy4wMTI0IDcuMTk1Mi00LjE1NDEgMi4wNTQ2LTkuNDkxNCAyMC40MjgtOC44NDE4IDIzLjE0NSAwLjgxODMzIDEyLjY0MyAxNC4wMjUgMzIuMzE4IDE0LjAyNWgxNTAuOTJjMTQuMzMyIDAtNC4xMTkxLTEzLjExIDI5LjI2OS0xMy40MTUiIGNvbG9yPSIjMDAwMDAwIi8+CiAgIDwvZz4KICA8L2c+CiAgPGcgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4Ij4KICAgPHRleHQgeD0iNTg4LjY3OTU3IiB5PSI3MzUuODA0NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU4OC42Nzk1NyIgeT0iNzM1LjgwNDYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TGluY29sbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI2ODYuMzk4NSIgeT0iNzY1LjYyODQyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3MDkuODcxODMiIHk9Ii04MDIuMzc3MzgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjcwOS44NzE4MyIgeT0iLTgwMi4zNzczOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldvb2RsYXduPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTYyLjExOTI2IiB5PSItNzcxLjk2ODE0IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjIuMTE5MjYiIHk9Ii03NzEuOTY4MTQiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5FZGdlbW9vcjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5OC4zMDQ4NyIgeT0iLTczOC4zNjY0NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk4LjMwNDg3IiB5PSItNzM4LjM2NjQ2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNTkyLjEyMjg2IiB5PSItNjc3LjIwMzk4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1OTIuMTIyODYiIHk9Ii02NzcuMjAzOTgiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5IaWxsc2lkZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU5Ny4zMjcwOSIgeT0iLTg2Mi42MTQwNyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTk3LjMyNzA5IiB5PSItODYyLjYxNDA3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Um9jazwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjU4Ny4zNzAxOCIgeT0iLTkyNi4xMzY2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1ODcuMzcwMTgiIHk9Ii05MjYuMTM2NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODcxLjE2MTAxIiB5PSI2MzcuNTc1MiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkNlbnRyYWw8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iODczLjgzMjI4IiB5PSI1NzcuMDMyNDciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijg3My44MzIyOCIgeT0iNTc3LjAzMjQ3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI4NzUuOTY2NDkiIHk9IjUxMC4yNjE4MSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iODc1Ljk2NjQ5IiB5PSI1MTAuMjYxODEiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yMXN0PC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9Ijg4MS4zMTY1OSIgeT0iNDUwLjE5ODc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI4ODEuMzE2NTkiIHk9IjQ1MC4xOTg3NiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjI5dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNjE1Ljc5MjQ4IiB5PSIzODcuNzQ3MTYiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjYxNS43OTI0OCIgeT0iMzg3Ljc0NzE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Mzd0aDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI0ODQuNjkwMzciIHk9IjQ4MS42NTI4NiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDg0LjY5MDM3IiB5PSI0ODEuNjUyODYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij4yNXRoPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU2My4wNDY3NSIgeT0iNTEzLjM2MTMzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI1NjMuMDQ2NzUiIHk9IjUxMy4zNjEzMyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTY1Ljk3MTUiIHk9IjU3Ny44OTQ4NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI0MzMuNTgwNzUiIHk9Ii00NjAuNzMzMTIiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjQzMy41ODA3NSIgeT0iLTQ2MC43MzMxMiIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPkFtaWRvbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjQwNS41MzA5OCIgeT0iLTUyMy41NDAxNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNDA1LjUzMDk4IiB5PSItNTIzLjU0MDE2IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QXJrYW5zYXM8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI3NDUuNDg0NjIiIHk9Ii0zNzIuNTg1OTQiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc0NS40ODQ2MiIgeT0iLTM3Mi41ODU5NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldlc3Q8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTYuNzI4MzMiIHk9Ii01MzEuMjU5MjgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5Ni43MjgzMyIgeT0iLTUzMS4yNTkyOCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPldhY288L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1OTUuNDM0ODEiIHk9Ii0xMjIuNTAyOTUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU5NS40MzQ4MSIgeT0iLTEyMi41MDI5NSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hemllPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgeD0iNjk1Ljc3Mjk1IiB5PSIxNjIuMDY4NzciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY5NS43NzI5NSIgeT0iMTYyLjA2ODc3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+Wm9vPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjI0MC41ODk5NyIgeT0iNTc0LjQ0NTQzIiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIyNDAuNTg5OTciIHk9IjU3NC40NDU0MyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjA2LjAzMTc1IiB5PSI1MTEuNjM2NjMiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIwNi4wMzE3NSIgeT0iNTExLjYzNjYzIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9IjYyMC40NDMxMiIgeT0iLTUwNi42ODIxOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNjIwLjQ0MzEyIiB5PSItNTA2LjY4MjE5IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TmltczwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzNzAuMjE2ODYiIHk9IjY5OC44NDAwOSIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iMzcwLjIxNjg2IiB5PSI2OTguODQwMDkiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NYXBsZTwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzODQuMDg0MiIgeT0iNjgwLjg1MTM4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCBkPSJtMzY3LjkxIDEwMTBoMjYzLjAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDxnIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCI+CiAgIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzM2LjI2NzQ2IiB5PSItNDMzLjEzNzc2IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI3MzYuMjY3NDYiIHk9Ii00MzMuMTM3NzYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5NZXJpZGlhbjwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB4PSI1NzIuODMyMTUiIHk9IjY0MC4yMDUyNiIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTcyLjgzMjE1IiB5PSI2NDAuMjA1MjYiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjU3NS4wODk2NiIgeT0iNjcwLjkwMzUiIGZvbnQtc2l6ZT0iOS42NTg0cHgiIHN0eWxlPSJsaW5lLWhlaWdodDoxLjI1Ij5Eb3VnbGFzPC90c3Bhbj48L3RleHQ+CiAgIDx0ZXh0IHg9IjQ5OS40ODk2MiIgeT0iMTAwOC42MDY5IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSI0OTkuNDg5NjIiIHk9IjEwMDguNjA2OSIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPjQ3dGg8L3RzcGFuPjwvdGV4dD4KICAgPHRleHQgeD0iMjE2LjY0NTQzIiB5PSI3MjUuOTgyOTciIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjIxNi42NDU0MyIgeT0iNzI1Ljk4Mjk3IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICA8dGV4dCB0cmFuc2Zvcm09InJvdGF0ZSg5MCkiIHg9Ijc3NC44NzU2MSIgeT0iLTUwOC4xODk3MyIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNzc0Ljg3NTYxIiB5PSItNTA4LjE4OTczIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+TWNDbGVhbjwvdHNwYW4+PC90ZXh0PgogIDwvZz4KICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDI4Ny4zNikiIGQ9Im0zNjQuMTYgNjU4LjQzIDI5OS41MS0xLjAxMDJjNi40OTg3LTAuMDIxOSA2Ljk3NzIgOS4yNTQxIDE2LjU5NiA5LjM5MjUgMTIuMDU0IDAuMTczMzkgMjkuMTExLTAuNTM1NzIgNTQuMTE0LTAuMzAxMSIgY29sb3I9IiMwMDAwMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzNiIgc3Ryb2tlLXdpZHRoPSIxcHgiLz4KICA8dGV4dCB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGxldHRlci1zcGFjaW5nPSIwcHgiIHdvcmQtc3BhY2luZz0iMHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MCUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbiB4PSIzNzMuOTkzMDQiIHk9Ijk0NC4zNTc1NCIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPk1hY0FydGh1cjwvdHNwYW4+PC90ZXh0PgogIDx0ZXh0IHRyYW5zZm9ybT0icm90YXRlKDkwKSIgeD0iNzgwLjg0NjA3IiB5PSItNDkwLjI0NTk3IiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9Ijc4MC44NDYwNyIgeT0iLTQ5MC4yNDU5NyIgZm9udC1zaXplPSI5LjY1ODRweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjEuMjUiPlNlbmVjYTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgMjg3LjM2KSIgZD0ibTM2Ny43IDUzNy4yMSAxNDEuMjgtMS4wMTAyYzYuNDktMC4wNDY0IDEyLjc4MSA3LjIzNTQgMTkuMTkzIDcuMzIzNiA1NS45MjQgMC43Njg5IDE1OC42OS0wLjE3MzMzIDIzNi41MS0xLjAxMDIgNy44Mzk2LTAuMDg0MyAyMi42MzEtMTkuODU0IDMwLjMwNS0yMC40NTYgMjIuMjY2LTEuMzUxOCA0NS4xNzktMC41MDUwNyA2Ny42OC0wLjUwNTA3IDE2LjE0Ny0wLjYzMjQxIDMuNjEwMiAyMC43MDggMjYuNzY5IDIwLjcwOGwyNDMuNDUtMS4wMTAyIiBjb2xvcj0iIzAwMDAwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzM2IiBzdHJva2Utd2lkdGg9IjFweCIvPgogIDx0ZXh0IHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgbGV0dGVyLXNwYWNpbmc9IjBweCIgd29yZC1zcGFjaW5nPSIwcHgiIHN0eWxlPSJsaW5lLWhlaWdodDowJSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuIHg9IjY4NS4yMDgxMyIgeT0iODI3LjUzMDgyIiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+UGF3bmVlPC90c3Bhbj48L3RleHQ+CiAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAyODcuMzYpIiBkPSJtNTU0LjI5IDcyMS40My00LjI4NTctMTc4LjIxLTIuODU3MS00NDAuNzEtMC4zNTcxNC03OS4yODYiIGNvbG9yPSIjMDAwMDAwIiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzYiIHN0cm9rZS13aWR0aD0iMXB4Ii8+CiAgPHRleHQgdHJhbnNmb3JtPSJyb3RhdGUoOTApIiB4PSI1MjkuNjI1MzEiIHk9Ii01NTAuODQ3NzgiIGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBsZXR0ZXItc3BhY2luZz0iMHB4IiB3b3JkLXNwYWNpbmc9IjBweCIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4geD0iNTI5LjYyNTMxIiB5PSItNTUwLjg0Nzc4IiBmb250LXNpemU9IjkuNjU4NHB4IiBzdHlsZT0ibGluZS1oZWlnaHQ6MS4yNSI+QnJvYWR3YXk8L3RzcGFuPjwvdGV4dD4KIDwvZz4KPC9zdmc+Cg==", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_0.png", + "title": "Map marker image 0", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_0.png", + "publicResourceKey": "CdCrVxsjA4EAiFaXK4a7K2MZFMeEuGeD", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAwgSURBVGiB7Zt5cBT3lce/v18fc89oRoPEIRBCHIUxp2ywCAgIxLExvoidZIFNxXE2VXHirIO3aqtSseM43qpNeZfYKecox3bhpJykYgdjDkU2mBAB5vCamMNYAgQyURBCoxnNPd39O/aP7hGSEUR24L/uqqf+zfR77/Pe69/Rv6kWwcgPLRIJfZUAa7xez2xd90QBwDSNZKlkHJHAK+l09mUA7BP4vPpRUVExMVoRef+L998njxx9X57vPi/PnTsnO850yPaT7XLXrrflqjtWymhF+HA0Gp0wEp/kHymEQqG4ptJDGzf+um5RUxMSiV7Z3Lyt88L5nozgHJWj4pGmpqZav99PWve04onHHuswmViQzWb7ruZX+Udgv8/z3A+f/NGye1evxssvb+wo5PMfTZs6bfqcuXNHL7hlweh58+ZVAOTUpk2b0p9dvjyqqmrs/b8ejpUMc+unzjgUCsXjsYruE+2n1JY/NedM0zCi0VjA7/d7/f4AAgE//H4/vF4fOjvP9h5695C/oaEhcN/q1SyTzVdnMpnklXzTq4EplUsXfmaRCgC7du3cOn78+KfGj59Add3z1Md1vV7vqPa2D1sA4MYbZ6qUiqVX9X21i4TQcfX19QCA6urquN/vn0kAPRQKpYbTnzRpUhgAampqAEFrPjVYSql7fD4AgK5r2tV0AcDj8WkAoOk6JJGeTw2+nocLdsEu2AW7YBfsgl2wC3bBLtgFu2AX7IJdsAt2wS7YBbtgF+yCXbALdsEu2AW7YBfsgl2wC76mh/ppjIQgXVloPxVSBRV0rBe455P6+kTKBYF3tonxY/IWarry7DvI298Tgp0PR9RzACaN1NeIS100+EdvKXW3cMZvF8wCK10Sq2it2NAzakmukP/wmoP/KuId3BRUMg5uCfCSNVSKVn1rNto7Un8jLrUVqJ4Fi2eEQiEYBzOsy3SYL37TNQdzi8Q5FxkqJIQBsNLlYMGF/zqAJWBxSEogDAY+DJibYqTuRg4WFgO3OKhCYTExbKk5G/mbkSPP2DQhLA5IO/NhSz1MMP882BDgnAFQwdiVSs2vPVhYDIJLUMkBgw1favM6lJoZDDAYhKbAYsOX+rqAhcXAuQSIAKzhSy2vS8YmB7NYH4WCfM7kw5VaWtdpOO3bfWZJZVXgPxMX898bVsm6RhkTIseX29yyIErm/J5z5vwr6pvmsLYjBgeDwSpVJS/OmT1n1de+9qANZgLc4q9Dyj2qQhUhSSUAUCL7GBcchCymTEYBYNWqVXj30MGHT586PZEJ+WAul7ts8bjspd9QKDRNU2nz4z94YtI3H3oI+XwB//3j/9m77eRUUJ9/0eh4APGoDz6vCi4ksgUTmYyBC4k8RLGwtzF+EGu+tHqRqqrYtm0rXnzhhQ7G5cpsNnvyiuBIJFKnqvSd55772eilS5fhwIH9ye+/dPaEf1T9otW3T8GtiyYgGNBBymYEgLSbvakidu8/h01vnkYhcab1gcVs5tx5c6PHjh7DU0/9qFsINPb3939UZg28X11dXR0Qwtr9g8efqGtc+Bn89re/O7FhR9BXNaFm+n98uxHTZ1SDKQqKAihweZlITUVtXQwNs8fg+Bmzdk+bnmPdf/7bwsbGeO2ECaED+9/5XCxWuTGbzVpDwJpGNtx+28o77rr7bmzZsu3k7z+cMlHzeiPrvnoTwtVhFAVQHAZY4HBEoiAAeDXUjI/gyJGeQEd6TFj2tHYuXNgYy2azVe0fngiWDLNloHNFo4FZkXDsoTVr1+KD4x8U/3Ci1qP5PV7N74FeFUbClKDEriy57A5JANL5a68hnqoINL8OAPqbXbNp7clTxTVr1/oOHjr0MFXxq2Qy9wEFACnoY//6la9QAHj+9Q/eUL2RWkVXoWgqkhZBypRImkDKBFIWkLIk+h1JWdL+zrmeNCWSDFB0DYquQvWG637TcnozAKxbt45yTr8PAGowGBwVDAbvmT9/Pvbu3dddijV9WdUUUE0BUQm6kwaCYe+ljK/w8ruUdsYCBLlMEUQhoJoCygWM+LIvHTx4sGfevIbqYMD3BSFkJVUUrG5oaFABoPXwhd1UVUBVahtpKtoOnEV/gSHHgBwDso5c6XO6yNF24CNQTbV9qBRUUenuwz1/BoCZM2dplOJeSggWL1myFEII9IeXziIKBVUUW1QKo2Ci41Anei9kkWcY6Ex5R8qfc0wi0ZPF6QNnYeQNB2j7IQpFOtg0WwiBxoWNIBKLVQI6Z8rUqTh69FiWaFNmEIWgLFShoM5TZbIzgVxvFp6ID5rfA6JQgBAIxsGLJkrpAsycAcH4gN1gX0QPTW9vP5Grr58cJJTOpbqmjgWAnp6ei4QSEEJAKAGh1BbHCS2DLAFmMAgmICwObjDnyYMMAtJL9oN89vRc7KWUQtOUsSqhSggA8sWivSEh9qBxTiCEAGRwQARUVaB67Hf5pZAQlA0Ayrq2LTCogVyhlLURNEw55yYABP2+4ED3vHSClBKQ9jiFdHqvEBCMQzAOKYSt6/RqSGnbDPJRbgT93hAAcM4NyhjrBYDKylhswEEZJgYJFxDchnGTwSqasIomuMnsIDiH5GKIzUAQTsCVlZUxB9xLIUVbKpVEff3kiLTMfimEA7HP5bZgHMJ07mnJAiuaYEXT3jcZDMLkTgBD7exgBKRp9NfVTQwnk0kIKduoJGRH8/ZmhMNh4skc3DnEkDlAi4GbtjDDguVAmZM1M6yB68JyKsCGBqD373s7GAySnTt3gBDyFhWCvPHee/8HAJhTU5g0BMg4uMXBTT4AZSUTrGjBKpiwCnablQbDbZuyfTmAuRPMegA4euQopCRbaCaTOd2XSLzX3d2Nu+64bR7PnP3LJSCDMBm4YW9FWcmyQYMytsW+Zpfdsm1MdimAdMc7K29bMedCdzeSyeS76XT6jLNI4PGf/+w5aLqOu25IjOOWKcSg0jJjcLZ2ecsZD5TdybqsOxC0ZYpbJ58frek6nn/+eVBJHgecjXkqk2nu7Ozcdfz4cdx556rJN5C3m8v3jBt2xpdnazjysawNy5lUbKkrbmtZsWL5pGNHj6Or62+7k5lMy5CFNRQKTfN6tAMvvvhSRe3EOqx/4oXXLvia7qO6CsVZrey5154KB5YpKSG5tHs+5/ZsZnEIk6Ei1fLH73373i/09fXi0fWPpgyTLchkMqeGgAEgHA5/vjJWsf2PmzYr1dXV+K8fP7vjLxduWkY8ilpetQZPg+UJxh63lzqlNDi7gTa3fuPraz6bzxXw79/5FutP51am0+kdZdaQ/2kzDKNDUci51179w8pbP3er8sAD6+pnVCWy+/fs21LAqBnlMT50qJXFLq2a2L/5gaVy7N133j69u7sb67/7iFHIFf4tlU6/Ppg1kLGU8hYAywBMeOWV33gfXb9+1Q+ffDL+4Ne/AcYY/tS8PbV5++4Dhy+MopY2ZrLiidQDgDBSp5TS+Y7psS65ZOHsW26++eYosxje2PwGNm586eKzz/x027+sXWsBOAfgbULIQQAgUspaAA8BGAfnsamrq4u0tZ0Q333kkdGmZS3f8JNnlBXLV0AOilRKCS7sWYlxjlKxgHw+j5Y3W/C/Tz/NQ6Hgjp9seKZ31py5ajwe4wAtz9zdAH5OpJTPAqgEgL5USkpu4eLFHloqFXniYh9t3bunauuWrStisSi5//4vYnHTEkyZOhWqokBICcuy0N7ehr2trXjt1VeRzqTl3ffc81bjgsZELF4pQ6EAqa4eI6UEicfj5dhTKoCikynx6Bop5C14dJ2XcjmouipvvGFGoSJaWfr738/7tmzdjl/88pfIZjKwnH2SpmkIhSMYW1ODhvmNGFcztjhudFXR69Wgck58Hg+XEorH5ylDJYA8kVKOckpdB0ADIBOJhOzv70OhUFILuTzPZLNcSE6SfSlvJp0O5A1DN0qGDxLS4/OUAh6PGQqHC5XxeJEQgkgoRH1+L/wBP6LRuIjH4+Uf8gSAUwB+MbhzzQSwCMA0p/QUQADgNJ/PJ/v7+wnnnFiWkJZhKCYzKADoqiZUXeW67iGcSxKPx2QoFAo7AybnuE8COAZgHyHkxGXjeFAQEQCzANQCqAIQBeAH4AXgcex052w45TMcyQHIAOgBcBbAUUJI5uOM/wcaHmf3g9UM7QAAAABJRU5ErkJggg==", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_1.png", + "title": "Map marker image 1", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_1.png", + "publicResourceKey": "DF3fuPXua9Vi3o3d9Nz2I1LXDTwEs2Tv", + "mediaType": "image/png", + "data": "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", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_2.png", + "title": "Map marker image 2", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_2.png", + "publicResourceKey": "rz5SFAw2Sg5T2EyXNdwLycoDwf4QbMiZ", + "mediaType": "image/png", + "data": "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", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_3.png", + "title": "Map marker image 3", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_3.png", + "publicResourceKey": "KfPfTuvKCeAnmTcKcrvZQHfdU0TPArWY", + "mediaType": "image/png", + "data": "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", + "public": true + } + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json new file mode 100644 index 0000000000..9f246a215a --- /dev/null +++ b/application/src/main/data/json/system/widget_types/map.json @@ -0,0 +1,44 @@ +{ + "fqn": "map", + "name": "Map", + "deprecated": false, + "image": "tb-image;/api/images/system/openstreet_map_system_widget_image.png", + "description": "Displays the location of the entities on Map. Allows to choose among existing tile providers or configure own server. Some providers require additional licenses. Highly customizable via custom markers, marker tooltips, and widget actions. ", + "descriptor": { + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n", + "settingsForm": [], + "dataKeySettingsForm": [], + "settingsDirective": "tb-map-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-map-basic-config", + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8239425680406081,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\"},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var temperature = data.temperature;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\\n\"}},\"markerIcon\":{\"icon\":\"mdi:lightbulb-on\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xOTEuMzUgLTM1MS4xOCAxMDgzLjU4IDE3MzAuNDYiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjZmU3NTY5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMzciIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgZD0iTTM1MS44MzMgMTM2MC43OGMtMzguNzY2LTE5MC4zLTEwNy4xMTYtMzQ4LjY2NS0xODkuOTAzLTQ5NS40NEMxMDAuNTIzIDc1Ni40NjkgMjkuMzg2IDY1NS45NzgtMzYuNDM0IDU1MC40MDRjLTIxLjk3Mi0zNS4yNDQtNDAuOTM0LTcyLjQ3Ny02Mi4wNDctMTA5LjA1NC00Mi4yMTYtNzMuMTM3LTc2LjQ0NC0xNTcuOTM1LTc0LjI2OS0yNjcuOTMyIDIuMTI1LTEwNy40NzMgMzMuMjA4LTE5My42ODUgNzguMDMtMjY0LjE3M0MtMjEtMjA2LjY5IDEwMi40ODEtMzAxLjc0NSAyNjguMTY0LTMyNi43MjRjMTM1LjQ2Ni0yMC40MjUgMjYyLjQ3NSAxNC4wODIgMzUyLjU0MyA2Ni43NDcgNzMuNiA0My4wMzggMTMwLjU5NiAxMDAuNTI4IDE3My45MiAxNjguMjggNDUuMjIgNzAuNzE2IDc2LjM2IDE1NC4yNiA3OC45NzEgMjYzLjIzMyAxLjMzNyA1NS44My03LjgwNSAxMDcuNTMyLTIwLjY4NCAxNTAuNDE3LTEzLjAzNCA0My40MS0zMy45OTYgNzkuNjk1LTUyLjY0NiAxMTguNDU1LTM2LjQwNiA3NS42NTktODIuMDQ5IDE0NC45ODEtMTI3Ljg1NSAyMTQuMzQ1LTEzNi40MzcgMjA2LjYwNi0yNjQuNDk2IDQxNy4zMS0zMjAuNTggNzA2LjAyOHoiLz48Y2lyY2xlIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBjeD0iMzUyLjg5MSIgY3k9IjIyNS43NzkiIHI9IjE4My4zMzIiLz48L3N2Zz4=\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":1},{\"dsType\":\"function\",\"dsLabel\":\"Second point\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7826299113906372,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details\",\"offsetX\":0,\"offsetY\":-1},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"edit\":{\"enabledActions\":[],\"snappable\":false},\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 500 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"icon\",\"markerShape\":{\"shape\":\"markerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"size\":40,\"color\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var colors = ['#488bc7','#549c5d','#ed7546','#be2b29'];\\nvar temperature = data.temperature;\\nvar res = colors[0];\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120;\\n var index = Math.min(3, Math.floor(4 * percent));\\n res = colors[index];\\n}\\nreturn res;\"},\"icon\":\"thermostat\"},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/shape1.svg\",\"imageSize\":34,\"imageFunction\":\"\\n\",\"images\":[]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null}}],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[]},\"title\":\"Map\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\",\"titleIcon\":\"map\",\"iconColor\":\"#1F6BDD\",\"actions\":{}}" + }, + "resources": [ + { + "link": "/api/images/system/openstreet_map_system_widget_image.png", + "title": "\"OpenStreet Map\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "openstreet_map_system_widget_image.png", + "publicResourceKey": "Uyd9JmVKUcCF6PchzgNnAOjx9WvVN5ZE", + "mediaType": "image/png", + "data": "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", + "public": true + } + ], + "scada": false, + "tags": [ + "mapping", + "gps", + "navigation", + "geolocation", + "satellite", + "directions" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json b/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json index e9f492b7fc..bf721b05b7 100644 --- a/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json +++ b/application/src/main/data/json/system/widget_types/markers_placement___google_maps.json @@ -1,7 +1,7 @@ { "fqn": "input_widgets.markers_placement_google_maps", "name": "Markers Placement - Google Maps", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/markers_placement_google_maps_system_widget_image.png", "description": "Allows configuring the location of the selected entities on Google Maps. By default, store the location using 'latitude' and 'longitude' server-side attributes.", "descriptor": { @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", + "settingsDirective": "tb-map-widget-settings-legacy", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"defaultZoomLevel\":5,\"provider\":\"google-map\",\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\",\"showPolygonTooltip\":false},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"46bf69cd-8906-234c-a879-e2e4c92f5b67\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" }, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/markers_placement___image_map.json b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json index 5544471bc3..c4bddab86d 100644 --- a/application/src/main/data/json/system/widget_types/markers_placement___image_map.json +++ b/application/src/main/data/json/system/widget_types/markers_placement___image_map.json @@ -1,7 +1,7 @@ { "fqn": "input_widgets.markers_placement_image_map", "name": "Markers Placement - Image Map", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/markers_placement_image_map_system_widget_image.png", "description": "Allows configuring the location of the selected entities on the Image map. By default, store the location using 'xPos' and 'yPos' server-side attributes with values of 0.0 to 1.0.", "descriptor": { @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", + "settingsDirective": "tb-map-widget-settings-legacy", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"tb-image;/api/images/system/markers_placement_image_map_system_widget_map_image.svg\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":\"0,0\",\"provider\":\"image-map\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapProvider\":\"HERE.normalDay\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"94bf5ffd-b526-c6c3-ae3b-ab42191217d9\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" }, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json b/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json index a351a75564..da96abb49e 100644 --- a/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json +++ b/application/src/main/data/json/system/widget_types/markers_placement___openstreetmap.json @@ -1,7 +1,7 @@ { "fqn": "input_widgets.markers_placement_openstreetmap", "name": "Markers Placement - OpenStreetMap", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/markers_placement_openstreetmap_system_widget_image.png", "description": "Allows configuring the location of the selected entities on OpenStreetMap. By default, store the location using 'latitude' and 'longitude' server-side attributes.", "descriptor": { @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, true);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", + "settingsDirective": "tb-map-widget-settings-legacy", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}

Delete\",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":\"0,0\",\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5,\"provider\":\"openstreet-map\",\"draggableMarker\":true,\"editablePolygon\":true,\"mapPageSize\":16384,\"showPolygon\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${coordinates|ts:7}

Delete\"},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id;\\n });\\n\\nwidgetContext.map.setMarkerLocation(entityDatasource[0], null, null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\"},{\"name\":\"delete_polygon\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var entityDatasource = widgetContext.map.map.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.savePolygonLocation(entityDatasource[0], null).subscribe(() => widgetContext.updateAliases());\",\"id\":\"6beb7bed-dfd8-388d-b60c-82988ab52f06\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}" }, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/openstreet_map.json b/application/src/main/data/json/system/widget_types/openstreet_map.json index 15454743f3..b0294e0988 100644 --- a/application/src/main/data/json/system/widget_types/openstreet_map.json +++ b/application/src/main/data/json/system/widget_types/openstreet_map.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.openstreetmap", "name": "OpenStreet Map", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/openstreet_map_system_widget_image.png", "description": "Displays the location of the entities on OpenStreetMap. Allows to choose among existing tile providers or configure own server. Some providers require additional licenses. Highly customizable via custom markers, marker tooltips, and widget actions. ", "descriptor": { @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", + "settingsDirective": "tb-map-widget-settings-legacy", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.9430343126300238,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.1784452363910778,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.773875863339494,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.405822538899673,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"openstreet-map\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"useCustomProvider\":false,\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"perimeter\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.2,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":3,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"OpenStreet Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" }, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/route_map.json b/application/src/main/data/json/system/widget_types/route_map.json new file mode 100644 index 0000000000..6d9590fe90 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/route_map.json @@ -0,0 +1,80 @@ +{ + "fqn": "route_map", + "name": "Route Map", + "deprecated": false, + "image": "tb-image;/api/images/system/route_map_openstreet_system_widget_image.png", + "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Highly customizable via custom markers, marker tooltips, and widget actions.", + "descriptor": { + "type": "timeseries", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}", + "settingsForm": [], + "dataKeySettingsForm": [], + "latestDataKeySettingsForm": [], + "settingsDirective": "tb-map-widget-settings", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "", + "hasBasicMode": true, + "basicModeDirective": "tb-map-basic-config", + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First route\",\"dsDeviceId\":null,\"dsEntityAliasId\":null,\"dsFilterId\":null,\"additionalDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.17490048149347315,\"funcBody\":\"var value = prevValue;\\nif (!value) {\\n value = 45;\\n}\\nif (time % 500 < 500) {\\n value = value + Math.random() * 40 - 20;\\n if (value < 45) {\\n \\tvalue = 45;\\n } else if (value > 130) {\\n \\tvalue = 130;\\n }\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH\",\"offsetX\":0,\"offsetY\":-1,\"patternFunction\":null,\"tagActions\":null},\"click\":{\"type\":\"doNothing\"},\"groups\":null,\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"image\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"function\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34,\"imageFunction\":\"var speed = data.Speed;\\nvar res = {\\n url: images[0],\\n size: 55\\n};\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n var index = Math.min(2, Math.floor(3 * percent));\\n res.url = images[index];\\n}\\nreturn res;\",\"images\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\"]},\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":4,\"pathStrokeColor\":{\"type\":\"function\",\"color\":\"#307FE5\",\"colorFunction\":\"var speed = data.Speed;\\nif (typeof speed !== undefined) {\\n var percent = (speed - 45)/85;\\n if (percent < 0.5) {\\n percent *=2*100; \\n return tinycolor.mix('green', 'yellow', percent).setAlpha(0.65).toRgbString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', percent).setAlpha(0.65).toRgbString();\\n }\\n}\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"tripTimeline\":{\"showTimelineControl\":false}},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}" + }, + "resources": [ + { + "link": "/api/images/system/map_marker_image_0.png", + "title": "Map marker image 0", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_0.png", + "publicResourceKey": "LPbcriZ2v053mkWb33T5JdK7Agkt1jGg", + "mediaType": "image/png", + "data": "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", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_1.png", + "title": "Map marker image 1", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_1.png", + "publicResourceKey": "TwKYnwJfaCIgDbJsetgcj3q7AYK4HUSA", + "mediaType": "image/png", + "data": "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", + "public": true + }, + { + "link": "/api/images/system/map_marker_image_2.png", + "title": "Map marker image 2", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "map_marker_image_2.png", + "publicResourceKey": "FazBQsEp1uSeIsT1XL31o2npLAx5s3zJ", + "mediaType": "image/png", + "data": "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", + "public": true + }, + { + "link": "/api/images/system/route_map_openstreet_system_widget_image.png", + "title": "\"Route Map - OpenStreet\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "route_map_openstreet_system_widget_image.png", + "publicResourceKey": "VTTAajdw2NRpCUbQLGVFCMBybEtJfwFK", + "mediaType": "image/png", + "data": "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", + "public": true + } + ], + "scada": false, + "tags": [ + "mapping", + "gps", + "navigation", + "geolocation", + "satellite", + "directions" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/route_map___google.json b/application/src/main/data/json/system/widget_types/route_map___google.json index ac7f511687..6eda23ef00 100644 --- a/application/src/main/data/json/system/widget_types/route_map___google.json +++ b/application/src/main/data/json/system/widget_types/route_map___google.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.route_map", "name": "Route Map - Google", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/route_map_google_system_widget_image.png", "description": "Visualize the entity trip on Google Maps. Allows to visualize location history. Use the Trip Animation widget for advanced features.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/route_map___openstreet.json b/application/src/main/data/json/system/widget_types/route_map___openstreet.json index 9021fac8aa..92bb2516a9 100644 --- a/application/src/main/data/json/system/widget_types/route_map___openstreet.json +++ b/application/src/main/data/json/system/widget_types/route_map___openstreet.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.route_map_openstreetmap", "name": "Route Map - OpenStreet", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/route_map_openstreet_system_widget_image.png", "description": "Visualize the entity trip on OpenStreetMap. Allows to visualize location history. Use the Trip Animation widget for advanced features.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/route_map___tencent.json b/application/src/main/data/json/system/widget_types/route_map___tencent.json index f2f01a5b57..3de304c8fc 100644 --- a/application/src/main/data/json/system/widget_types/route_map___tencent.json +++ b/application/src/main/data/json/system/widget_types/route_map___tencent.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.route_map_tencent_maps", "name": "Route Map - Tencent", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/route_map_tencent_system_widget_image.png", "description": "Visualize the entity trip on Tencent Maps. Allows to visualize location history. Use the Trip Animation widget for advanced features.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/tencent_map.json b/application/src/main/data/json/system/widget_types/tencent_map.json index a3e480f836..ec41717ca4 100644 --- a/application/src/main/data/json/system/widget_types/tencent_map.json +++ b/application/src/main/data/json/system/widget_types/tencent_map.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.tencent_maps", "name": "Tencent Map", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/tencent_map_system_widget_image.png", "description": "Displays the location of the entities on Tencent maps. Requires the Tencent map key to work properly. Highly customizable via custom markers, marker tooltips, and widget actions.", "descriptor": { @@ -14,7 +14,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('tencent-map', false, self.ctx);\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n self.ctx.map.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "tb-map-widget-settings", + "settingsDirective": "tb-map-widget-settings-legacy", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.24727730589425012,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.8437014651129422,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7558240907832925,\"funcBody\":\"return \\\"colorpin\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]},{\"type\":\"function\",\"name\":\"Second Point\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.19266205227372524,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7995830793603149,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#8bc34a\",\"settings\":{},\"_hash\":0.04902495467943502,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Type\",\"color\":\"#3f51b5\",\"settings\":{},\"_hash\":0.44120841439482095,\"funcBody\":\"return \\\"thermometer\\\";\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"provider\":\"tencent-map\",\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"tmDefaultMapType\":\"roadmap\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"defaultCenterPosition\":\"0,0\",\"disableScrollZooming\":false,\"disableDoubleClickZooming\":false,\"disableZoomControl\":false,\"fitMapBounds\":true,\"useDefaultCenterPosition\":false,\"mapPageSize\":16384,\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"draggableMarker\":false,\"showLabel\":true,\"useLabelFunction\":false,\"label\":\"${entityName}\",\"showTooltip\":true,\"showTooltipAction\":\"click\",\"autocloseTooltip\":true,\"useTooltipFunction\":false,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"tooltipOffsetX\":0,\"tooltipOffsetY\":-1,\"color\":\"#fe7569\",\"useColorFunction\":true,\"colorFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'colorpin') {\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120 * 100;\\n\\t return tinycolor.mix('blue', 'red', percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"markerImageSize\":34,\"markerImageFunction\":\"var type = dsData[dsIndex]['Type'];\\nif (type == 'thermometer') {\\n\\tvar res = {\\n\\t url: images[0],\\n\\t size: 40\\n\\t}\\n\\tvar temperature = dsData[dsIndex]['temperature'];\\n\\tif (typeof temperature !== undefined) {\\n\\t var percent = (temperature + 60)/120;\\n\\t var index = Math.min(3, Math.floor(4 * percent));\\n\\t res.url = images[index];\\n\\t}\\n\\treturn res;\\n}\",\"markerImages\":[\"tb-image;/api/images/system/map_marker_image_0.png\",\"tb-image;/api/images/system/map_marker_image_1.png\",\"tb-image;/api/images/system/map_marker_image_2.png\",\"tb-image;/api/images/system/map_marker_image_3.png\"],\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"editablePolygon\":false,\"showPolygonLabel\":false,\"usePolygonLabelFunction\":false,\"polygonLabel\":\"${entityName}\",\"showPolygonTooltip\":false,\"showPolygonTooltipAction\":\"click\",\"autoClosePolygonTooltip\":true,\"usePolygonTooltipFunction\":false,\"polygonTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"polygonColor\":\"#3388ff\",\"polygonOpacity\":0.5,\"usePolygonColorFunction\":false,\"polygonStrokeColor\":\"#3388ff\",\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"usePolygonStrokeColorFunction\":false,\"showCircle\":false,\"circleKeyName\":\"perimeter\",\"editableCircle\":false,\"showCircleLabel\":false,\"useCircleLabelFunction\":false,\"circleLabel\":\"${entityName}\",\"showCircleTooltip\":false,\"showCircleTooltipAction\":\"click\",\"autoCloseCircleTooltip\":true,\"useCircleTooltipFunction\":false,\"circleTooltipPattern\":\"${entityName}

TimeStamp: ${ts:7}\",\"circleFillColor\":\"#3388ff\",\"circleFillColorOpacity\":0.2,\"useCircleFillColorFunction\":false,\"circleStrokeColor\":\"#3388ff\",\"circleStrokeOpacity\":1,\"circleStrokeWeight\":3,\"useCircleStrokeColorFunction\":false,\"useClusterMarkers\":false,\"zoomOnClick\":true,\"maxClusterRadius\":80,\"animate\":true,\"spiderfyOnMaxZoom\":false,\"showCoverageOnHover\":true,\"chunkedLoading\":false,\"removeOutsideVisibleBounds\":true,\"useIconCreateFunction\":false},\"title\":\"Tencent Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" }, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/trip_animation.json b/application/src/main/data/json/system/widget_types/trip_animation.json index e4429dd0b5..52eb7b8503 100644 --- a/application/src/main/data/json/system/widget_types/trip_animation.json +++ b/application/src/main/data/json/system/widget_types/trip_animation.json @@ -1,7 +1,7 @@ { "fqn": "maps_v2.test", "name": "Trip Animation", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/trip_animation_system_widget_image.png", "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. Highly customizable via custom markers, marker tooltips, and widget actions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/trip_map.json b/application/src/main/data/json/system/widget_types/trip_map.json new file mode 100644 index 0000000000..09eba54978 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/trip_map.json @@ -0,0 +1,47 @@ +{ + "fqn": "trip_map", + "name": "Trip Map", + "deprecated": false, + "image": "tb-image;/api/images/system/trip_animation_system_widget_image.png", + "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. Highly customizable via custom markers, marker tooltips, and widget actions.", + "descriptor": { + "type": "timeseries", + "sizeX": 8.5, + "sizeY": 6, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n trip: true,\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}", + "settingsForm": [], + "dataKeySettingsForm": [], + "latestDataKeySettingsForm": [], + "settingsDirective": "tb-map-widget-settings", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "", + "hasBasicMode": true, + "basicModeDirective": "tb-map-basic-config", + "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"mapType\":\"geoMap\",\"markers\":[],\"polygons\":[],\"circles\":[],\"additionalDataSources\":[],\"trips\":[{\"dsType\":\"function\",\"dsLabel\":\"First point\",\"xKey\":{\"name\":\"f(x)\",\"label\":\"latitude\",\"type\":\"function\",\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : (value + 2) % gpsData.length)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"yKey\":{\"name\":\"f(x)\",\"label\":\"longitude\",\"type\":\"function\",\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : (value + 2) % gpsData.length)];\",\"settings\":{},\"color\":\"#2196f3\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},\"markerType\":\"shape\",\"markerShape\":{\"shape\":\"tripMarkerShape1\",\"size\":34,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerIcon\":{\"icon\":\"arrow_forward\",\"size\":48,\"color\":{\"type\":\"constant\",\"color\":\"#307FE5\"}},\"markerImage\":{\"type\":\"image\",\"image\":\"/assets/markers/tripShape1.svg\",\"imageSize\":34},\"markerOffsetX\":0.5,\"markerOffsetY\":0.5,\"positionFunction\":\"return {x: origXPos, y: origYPos};\",\"markerClustering\":{\"enable\":false,\"zoomOnClick\":true,\"maxZoom\":null,\"maxClusterRadius\":80,\"zoomAnimation\":true,\"showCoverageOnHover\":true,\"spiderfyOnMaxZoom\":false,\"chunkedLoad\":false,\"lazyLoad\":true,\"useClusterMarkerColorFunction\":false,\"clusterMarkerColorFunction\":null},\"label\":{\"show\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}\"},\"tooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-0.5},\"click\":{\"type\":\"doNothing\"},\"edit\":{\"enabledActions\":[],\"snappable\":false},\"rotateMarker\":true,\"offsetAngle\":0,\"showPath\":true,\"pathStrokeWeight\":2,\"pathStrokeColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"usePathDecorator\":false,\"pathDecoratorSymbol\":\"arrowHead\",\"pathDecoratorSymbolSize\":10,\"pathDecoratorSymbolColor\":\"#307FE5\",\"pathDecoratorOffset\":20,\"pathEndDecoratorOffset\":20,\"pathDecoratorRepeat\":20,\"showPoints\":false,\"pointSize\":10,\"pointColor\":{\"type\":\"constant\",\"color\":\"#307FE5\"},\"pointTooltip\":{\"show\":true,\"trigger\":\"click\",\"autoclose\":true,\"type\":\"pattern\",\"pattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}\",\"offsetX\":0,\"offsetY\":-1}}],\"tripTimeline\":{\"showTimelineControl\":true}},\"title\":\"Trip Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"assistant_navigation\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":null,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":null},\"titleColor\":null,\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}" + }, + "resources": [ + { + "link": "/api/images/system/trip_animation_system_widget_image.png", + "title": "\"Trip Animation\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "trip_animation_system_widget_image.png", + "publicResourceKey": "3UKAE6mZvW6bnIhr7NAtYTG8FbIDFY1e", + "mediaType": "image/png", + "data": "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", + "public": true + } + ], + "scada": false, + "tags": [ + "mapping", + "gps", + "navigation", + "geolocation", + "satellite", + "directions" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index 0f2473cde6..305dc04961 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -35,8 +35,11 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "configurationVersion": 2, + "configurationVersion": 3, "configuration": { + "processingSettings": { + "type": "ON_EVERY_MESSAGE" + }, "scope": "CLIENT_SCOPE", "notifyDevice": false, "sendAttributesUpdatedNotification": false, diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index 8efda98c5b..a988c9d5eb 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -34,8 +34,11 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "configurationVersion": 2, + "configurationVersion": 3, "configuration": { + "processingSettings": { + "type": "ON_EVERY_MESSAGE" + }, "scope": "CLIENT_SCOPE", "notifyDevice": false, "sendAttributesUpdatedNotification": false, diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e9cbf8ef8e..29c7a084f4 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -16,52 +16,54 @@ -- UPDATE SAVE TIME SERIES NODES START -DO $$ - BEGIN - -- Check if the rule_node table exists - IF EXISTS ( - SELECT 1 - FROM information_schema.tables - WHERE table_name = 'rule_node' - ) THEN +UPDATE rule_node +SET configuration = ( + (configuration::jsonb - 'skipLatestPersistence') + || jsonb_build_object( + 'processingSettings', jsonb_build_object( + 'type', 'ADVANCED', + 'timeseries', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), + 'latest', jsonb_build_object('type', 'SKIP'), + 'webSockets', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), + 'calculatedFields', jsonb_build_object('type', 'ON_EVERY_MESSAGE') + ) + ) + )::text, + configuration_version = 1 +WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' + AND configuration_version = 0 + AND configuration::jsonb ->> 'skipLatestPersistence' = 'true'; - UPDATE rule_node - SET configuration = ( - (configuration::jsonb - 'skipLatestPersistence') - || jsonb_build_object( - 'processingSettings', jsonb_build_object( - 'type', 'ADVANCED', - 'timeseries', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), - 'latest', jsonb_build_object('type', 'SKIP'), - 'webSockets', jsonb_build_object('type', 'ON_EVERY_MESSAGE'), - 'calculatedFields', jsonb_build_object('type', 'ON_EVERY_MESSAGE') - ) - ) - )::text, - configuration_version = 1 - WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' - AND configuration_version = 0 - AND configuration::jsonb ->> 'skipLatestPersistence' = 'true'; +UPDATE rule_node +SET configuration = ( + (configuration::jsonb - 'skipLatestPersistence') + || jsonb_build_object( + 'processingSettings', jsonb_build_object( + 'type', 'ON_EVERY_MESSAGE' + ) + ) + )::text, + configuration_version = 1 +WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' + AND configuration_version = 0 + AND (configuration::jsonb ->> 'skipLatestPersistence' != 'true' OR configuration::jsonb ->> 'skipLatestPersistence' IS NULL); - UPDATE rule_node - SET configuration = ( - (configuration::jsonb - 'skipLatestPersistence') - || jsonb_build_object( - 'processingSettings', jsonb_build_object( - 'type', 'ON_EVERY_MESSAGE' - ) - ) - )::text, - configuration_version = 1 - WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode' - AND configuration_version = 0 - AND (configuration::jsonb ->> 'skipLatestPersistence' != 'true' OR configuration::jsonb ->> 'skipLatestPersistence' IS NULL); +-- UPDATE SAVE TIME SERIES NODES END - END IF; - END; -$$; +-- UPDATE SAVE ATTRIBUTES NODES START --- UPDATE SAVE TIME SERIES NODES END +UPDATE rule_node +SET configuration = ( + configuration::jsonb + || jsonb_build_object( + 'processingSettings', jsonb_build_object('type', 'ON_EVERY_MESSAGE') + ) + )::text, + configuration_version = 3 +WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' + AND configuration_version = 2; + +-- UPDATE SAVE ATTRIBUTES NODES END ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 2c22941c2f..9df613353d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; @@ -432,10 +431,6 @@ public class ActorSystemContext { @Getter private ClaimDevicesService claimDevicesService; - @Autowired - @Getter - private JsInvokeStats jsInvokeStats; - //TODO: separate context for TbCore and TbRuleEngine @Autowired(required = false) @Getter @@ -595,17 +590,6 @@ public class ActorSystemContext { this.localCacheType = "caffeine".equals(cacheType); } - @Scheduled(fixedDelayString = "${actors.statistics.js_print_interval_ms}") - public void printStats() { - if (statisticsEnabled) { - if (jsInvokeStats.getRequests() > 0 || jsInvokeStats.getResponses() > 0 || jsInvokeStats.getFailures() > 0) { - log.info("Rule Engine JS Invoke Stats: requests [{}] responses [{}] failures [{}]", - jsInvokeStats.getRequests(), jsInvokeStats.getResponses(), jsInvokeStats.getFailures()); - jsInvokeStats.reset(); - } - } - } - @Value("${actors.tenant.create_components_on_init:true}") @Getter private boolean tenantComponentsInitEnabled; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 61f46b7901..31cb159229 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; @@ -74,7 +76,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM final EntityId entityId; final CalculatedFieldProcessingService cfService; final CalculatedFieldStateService cfStateService; - final int partition; TbActorCtx ctx; Map states = new HashMap<>(); @@ -85,7 +86,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM this.entityId = entityId; this.cfService = systemContext.getCalculatedFieldProcessingService(); this.cfStateService = systemContext.getCalculatedFieldStateService(); - this.partition = systemContext.getCalculatedFieldEntityProfileCache().getEntityIdPartition(tenantId, entityId); } void init(TbActorCtx ctx) { @@ -93,8 +93,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(CalculatedFieldPartitionChangeMsg msg) { - if (!msg.getPartitions()[partition]) { - log.info("[{}][{}] Stopping entity actor due to change partition event.", partition, entityId); + if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { + log.info("[{}] Stopping entity actor due to change partition event.", entityId); ctx.stop(ctx.getSelf()); } } @@ -134,20 +134,26 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM public void process(CalculatedFieldEntityDeleteMsg msg) { log.info("[{}] Processing CF entity delete msg.", msg.getEntityId()); if (this.entityId.equals(msg.getEntityId())) { - MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); - states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); - ctx.stop(ctx.getSelf()); + if (states.isEmpty()) { + msg.getCallback().onSuccess(); + } else { + MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); + states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); + ctx.stop(ctx.getSelf()); + } } else { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var state = states.remove(cfId); if (state != null) { cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); + } else { + msg.getCallback().onSuccess(); } } } public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { - log.info("[{}] Processing CF telemetry msg.", msg.getEntityId()); + log.debug("[{}] Processing CF telemetry msg.", msg.getEntityId()); var proto = msg.getProto(); var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); @@ -162,7 +168,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException { - log.info("[{}] Processing CF link telemetry msg.", msg.getEntityId()); + log.debug("[{}] Processing CF link telemetry msg.", msg.getEntityId()); var proto = msg.getProto(); var ctx = msg.getCtx(); var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index e7c963a4a7..b0185fd555 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -199,7 +199,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (fieldsCount > 0) { MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); var entityId = msg.getEntityId(); - oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), callback)); + oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback)); newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); } else { callback.onSuccess(); @@ -306,7 +306,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (!entityIds.isEmpty()) { //TODO: no need to do this if we cache all created actors and know which one belong to us; var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> deleteCfForEntity(entityId, cfId, multiCallback)); + entityIds.forEach(id -> deleteCfForEntity(id, cfId, multiCallback)); } else { callback.onSuccess(); } @@ -318,14 +318,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); - log.info("Received telemetry msg from entity [{}]", entityId); + log.debug("Received telemetry msg from entity [{}]", entityId); // 2 = 1 for CF processing + 1 for links processing MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback()); // process all cfs related to entity, or it's profile; var entityIdFields = getCalculatedFieldsByEntityId(entityId); var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); if (!entityIdFields.isEmpty() || !profileIdFields.isEmpty()) { - log.info("Pushing telemetry msg to specific actor [{}]", entityId); + log.debug("Pushing telemetry msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new EntityCalculatedFieldTelemetryMsg(msg, entityIdFields, profileIdFields, callback)); } else { callback.onSuccess(); @@ -342,7 +342,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) { EntityId sourceEntityId = msg.getEntityId(); - log.info("Received linked telemetry msg from entity [{}]", sourceEntityId); + log.debug("Received linked telemetry msg from entity [{}]", sourceEntityId); var proto = msg.getProto(); var linksList = proto.getLinksList(); for (var linkProto : linksList) { @@ -357,14 +357,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware MultipleTbCallback callback = new MultipleTbCallback(entityIds.size(), msg.getCallback()); var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback); entityIds.forEach(entityId -> { - log.info("Pushing linked telemetry msg to specific actor [{}]", entityId); + log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(newMsg); }); } else { msg.getCallback().onSuccess(); } } else { - log.info("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); + log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, msg.getCallback()); getOrCreateActor(targetEntityId).tell(newMsg); } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ee6758df6a..033e10ca9a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -654,27 +654,6 @@ public class DefaultTbContext implements TbContext { } } - @Override - public void logJsEvalRequest() { - if (mainCtx.isStatisticsEnabled()) { - mainCtx.getJsInvokeStats().incrementRequests(); - } - } - - @Override - public void logJsEvalResponse() { - if (mainCtx.isStatisticsEnabled()) { - mainCtx.getJsInvokeStats().incrementResponses(); - } - } - - @Override - public void logJsEvalFailure() { - if (mainCtx.isStatisticsEnabled()) { - mainCtx.getJsInvokeStats().incrementFailures(); - } - } - @Override public String getServiceId() { return mainCtx.getServiceInfoProvider().getServiceId(); diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 4be2024680..f9eec324fc 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -91,6 +91,15 @@ public class TenantActor extends RuleChainManagerActor { isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE); if (isRuleEngine) { if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) { + try { + //TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0; + cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId), + () -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME, + () -> new CalculatedFieldManagerActorCreator(systemContext, tenantId), + () -> true); + } catch (Exception e) { + log.info("[{}] Failed to init CF Actor.", tenantId, e); + } try { if (getApiUsageState().isReExecEnabled()) { log.debug("[{}] Going to init rule chains", tenantId); @@ -98,11 +107,6 @@ public class TenantActor extends RuleChainManagerActor { } else { log.info("[{}] Skip init of the rule chains due to API limits", tenantId); } - //TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0; - cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId), - () -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME, - () -> new CalculatedFieldManagerActorCreator(systemContext, tenantId), - () -> true); } catch (Exception e) { log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e); cantFindTenant = true; @@ -185,6 +189,10 @@ public class TenantActor extends RuleChainManagerActor { } private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + if (cfActor == null) { + log.warn("[{}] CF Actor is not initialized.", tenantId); + return; + } if (priority) { cfActor.tellWithHighPriority(msg); } else { @@ -251,11 +259,25 @@ public class TenantActor extends RuleChainManagerActor { ServiceType serviceType = msg.getServiceType(); if (ServiceType.TB_RULE_ENGINE.equals(serviceType)) { if (systemContext.getPartitionService().isManagedByCurrentService(tenantId)) { + if (cfActor == null) { + try { + //TODO: IM - extend API usage to have CF Exec Enabled? Not in 4.0; + cfActor = ctx.getOrCreateChildActor(new TbStringActorId("CFM|" + tenantId), + () -> DefaultActorService.CF_MANAGER_DISPATCHER_NAME, + () -> new CalculatedFieldManagerActorCreator(systemContext, tenantId), + () -> true); + } catch (Exception e) { + log.info("[{}] Failed to init CF Actor.", tenantId, e); + } + } if (!ruleChainsInitialized) { log.info("Tenant {} is now managed by this service, initializing rule chains", tenantId); initRuleChains(); } } else { + if (cfActor != null) { + ctx.stop(cfActor.getActorId()); + } if (ruleChainsInitialized) { log.info("Tenant {} is no longer managed by this service, stopping rule chains", tenantId); destroyRuleChains(); diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index fe85cf3f87..f899d0f480 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -34,6 +34,8 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; @@ -216,11 +218,14 @@ public class CalculatedFieldController extends BaseController { @RequestBody JsonNode inputParams) { String expression = inputParams.get("expression").asText(); Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference>() { + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() { }), Collections.emptyMap() ); - ArrayList argNames = new ArrayList<>(arguments.keySet()); + + ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(arguments.keySet()); String output = ""; String errorText = ""; @@ -234,12 +239,20 @@ public class CalculatedFieldController extends BaseController { getTenantId(), tbelInvokeService, expression, - argNames.toArray(String[]::new) + ctxAndArgNames.toArray(String[]::new) ); - Object[] args = argNames.stream() - .map(arguments::get) - .toArray(); + + Object[] args = new Object[ctxAndArgNames.size()]; + args[0] = new TbelCfCtx(arguments); + for (int i = 1; i < ctxAndArgNames.size(); i++) { + var arg = arguments.get(ctxAndArgNames.get(i)); + if (arg instanceof TbelCfSingleValueArg svArg) { + args[i] = svArg.getValue(); + } else { + args[i] = arg; + } + } JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); output = JacksonUtil.toString(json); @@ -260,7 +273,8 @@ public class CalculatedFieldController extends BaseController { EntityType entityType = referencedEntityId.getEntityType(); switch (entityType) { case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); + default -> + throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index ff4de0ce0c..1a3bdce1f6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -160,7 +161,12 @@ public class TenantProfileController extends BaseController { " \"rpcTtlDays\": 0,\n" + " \"queueStatsTtlDays\": 0,\n" + " \"ruleEngineExceptionsTtlDays\": 0,\n" + - " \"warnThreshold\": 0\n" + + " \"warnThreshold\": 0,\n" + + " \"maxCalculatedFieldsPerEntity\": 5,\n" + + " \"maxArgumentsPerCF\": 10,\n" + + " \"maxDataPointsPerRollingArg\": 1000,\n" + + " \"maxStateSizeInKBytes\": 32,\n" + + " \"maxSingleValueArgumentSizeInKBytes\": 2" + " }\n" + " },\n" + " \"default\": false\n" + @@ -172,7 +178,7 @@ public class TenantProfileController extends BaseController { @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) @ResponseBody public TenantProfile saveTenantProfile(@Parameter(description = "A JSON value representing the tenant profile.") - @RequestBody TenantProfile tenantProfile) throws ThingsboardException { + @Valid @RequestBody TenantProfile tenantProfile) throws ThingsboardException { TenantProfile oldProfile; if (tenantProfile.getId() == null) { accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 4976045b84..e9a6cb09aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -30,6 +30,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -51,6 +52,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; @@ -200,7 +202,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP if (broadcast) { broadcasts.add(link); } else { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, link.entityId()); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, link.tenantId(), link.entityId()); unicasts.computeIfAbsent(tpi, k -> new ArrayList<>()).add(link); } } @@ -273,7 +275,8 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); long startTs = currentTime - timeWindow; long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - int limit = argument.getLimit() == 0 ? (int) maxDataPoints : argument.getLimit(); + int argumentLimit = argument.getLimit(); + int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit(); ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE); ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java b/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java index 4cf62c01b3..2f5772ae50 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/cache/DefaultCalculatedFieldEntityProfileCache.java @@ -18,8 +18,10 @@ package org.thingsboard.server.service.cf.cache; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; @@ -57,7 +59,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent @Override public void add(TenantId tenantId, EntityId profileId, EntityId entityId) { - var tpi = partitionService.resolve(QueueKey.CF, entityId); + var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); var partition = tpi.getPartition().orElse(UNKNOWN); tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache()) .add(profileId, entityId, partition, tpi.isMyPartition()); @@ -65,7 +67,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent @Override public void update(TenantId tenantId, EntityId oldProfileId, EntityId newProfileId, EntityId entityId) { - var tpi = partitionService.resolve(QueueKey.CF, entityId); + var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); var partition = tpi.getPartition().orElse(UNKNOWN); var cache = tenantCache.computeIfAbsent(tenantId, id -> new TenantEntityProfileCache()); //TODO: make this method atomic; @@ -86,7 +88,7 @@ public class DefaultCalculatedFieldEntityProfileCache extends TbApplicationEvent @Override public int getEntityIdPartition(TenantId tenantId, EntityId entityId) { - var tpi = partitionService.resolve(QueueKey.CF, entityId); + var tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); return tpi.getPartition().orElse(UNKNOWN); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 399e0d9f84..c51aaa2e72 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -44,7 +43,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Data public class CalculatedFieldCtx { @@ -58,8 +56,6 @@ public class CalculatedFieldCtx { private final Map arguments; private final Map mainEntityArguments; private final Map> linkedEntityArguments; - - private final Map, String> referencedEntityKeys; private final List argNames; private Output output; private String expression; @@ -93,11 +89,6 @@ public class CalculatedFieldCtx { linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); } } - this.referencedEntityKeys = arguments.entrySet().stream() - .collect(Collectors.toMap( - entry -> new TbPair<>(entry.getValue().getRefEntityId() == null ? entityId : entry.getValue().getRefEntityId(), entry.getValue().getRefEntityKey()), - Map.Entry::getKey - )); this.argNames = new ArrayList<>(arguments.keySet()); this.output = configuration.getOutput(); this.expression = configuration.getExpression(); @@ -136,11 +127,14 @@ public class CalculatedFieldCtx { throw new IllegalArgumentException("TBEL script engine is disabled!"); } + List ctxAndArgNames = new ArrayList<>(argNames.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(argNames); return new CalculatedFieldTbelScriptEngine( tenantId, tbelInvokeService, expression, - argNames.toArray(String[]::new) + ctxAndArgNames.toArray(String[]::new) ); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index e81fa4d1dc..557768e9c9 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -20,10 +20,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; @@ -67,9 +69,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override public void init(PartitionedQueueConsumerManager> eventConsumer) { super.init(eventConsumer); + + var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME); this.stateConsumer = PartitionedQueueConsumerManager.>create() - .queueKey(QueueKey.CF_STATES) - .topic(partitionService.getTopic(QueueKey.CF_STATES)) + .queueKey(queueKey) + .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg msg : msgs) { @@ -101,7 +105,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF_STATES, stateId.entityId()); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId()); TbProtoQueueMsg msg = new TbProtoQueueMsg<>(stateId.entityId().getId(), stateMsgProto); if (stateMsgProto == null) { putStateId(msg.getHeaders(), stateId); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 00d9ccbe5a..bf00f1b0b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -23,11 +23,17 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; @Data @Slf4j @@ -49,11 +55,20 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { @Override public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { - Object[] args = ctx.getArgNames().stream() - .map(this::toTbelArgument) - .toArray(); - - ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args); + Map arguments = new LinkedHashMap<>(); + List args = new ArrayList<>(ctx.getArgNames().size() + 1); + args.add(new Object()); // first element is a ctx, but we will set it later; + for (String argName : ctx.getArgNames()) { + var arg = toTbelArgument(argName); + arguments.put(argName, arg); + if (arg instanceof TbelCfSingleValueArg svArg) { + args.add(svArg.getValue()); + } else { + args.add(arg); + } + } + args.set(0, new TbelCfCtx(arguments)); + ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); Output output = ctx.getOutput(); return Futures.transform(resultFuture, result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 0f4cf30fac..a0012a6fb3 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions which are compatible for the upgrade. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.9.0"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("3.9.0", "3.9.1"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index e76a2be9be..ad248cc3d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -27,6 +27,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.QueueConfig; @@ -79,8 +80,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer private long pollInterval; @Value("${queue.calculated_fields.pack_processing_timeout:60000}") private long packProcessingTimeout; - @Value("${queue.calculated_fields.pool_size:8}") - private int poolSize; private final TbRuleEngineQueueFactory queueFactory; private final CalculatedFieldStateService stateService; @@ -108,9 +107,10 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer public void init() { super.init("tb-cf"); + var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); this.eventConsumer = PartitionedQueueConsumerManager.>create() - .queueKey(QueueKey.CF) - .topic(partitionService.getTopic(QueueKey.CF)) + .queueKey(queueKey) + .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) .msgPackProcessor(this::processMsgs) .consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer()) @@ -140,20 +140,12 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer // Cleanup old entities after corresponding consumers are stopped. // Any periodic tasks need to check that the entity is still managed by the current server before processing. - actorContext.tell(new CalculatedFieldPartitionChangeMsg(partitionsToBooleanIndexArray(partitions))); + actorContext.tell(new CalculatedFieldPartitionChangeMsg()); } catch (Throwable t) { log.error("Failed to process partition change event: {}", event, t); } } - private boolean[] partitionsToBooleanIndexArray(Set partitions) { - boolean[] myPartitions = new boolean[partitionService.getTotalCalculatedFieldPartitions()]; - for (var tpi : partitions) { - tpi.getPartition().ifPresent(partition -> myPartitions[partition] = true); - } - return myPartitions; - } - private void processMsgs(List> msgs, TbQueueConsumer> consumer, QueueConfig config) throws Exception { List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); ConcurrentMap> pendingMap = orderedMsgList.stream().collect( diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 800578ace4..c7174469b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -358,7 +358,7 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldMsg msg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, entityId); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); pushMsgToCalculatedFields(tpi, UUID.randomUUID(), msg, callback); } @@ -371,7 +371,7 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF, entityId); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); producerProvider.getCalculatedFieldsNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); toRuleEngineNfs.incrementAndGet(); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 50989ea5a0..a3003ba6ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -552,10 +552,19 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService { - if (CollectionsUtil.isOneOf(queueKey, QueueKey.CF, QueueKey.CF_STATES)) { + if (DataConstants.CF_QUEUE_NAME.equals(queueKey.getQueueName()) || DataConstants.CF_STATES_QUEUE_NAME.equals(queueKey.getQueueName())) { return; } if (partitionService.isManagedByCurrentService(queueKey.getTenantId())) { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index a57debf012..cc476d377d 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -661,16 +661,14 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService activityTimeseries = Futures.getDone(timeseriesActivityDataFuture); Optional inactivityTimeoutAttribute = Futures.getDone(inactivityTimeoutAttributeFuture); - List result; if (inactivityTimeoutAttribute.isPresent()) { - result = new ArrayList<>(activityTimeseries.size() + 1); + List result = new ArrayList<>(activityTimeseries.size() + 1); result.addAll(activityTimeseries); - inactivityTimeoutAttribute.ifPresent(result::add); + result.add(inactivityTimeoutAttribute.get()); + return result; } else { return activityTimeseries; } - - return result; }, deviceStateCallbackExecutor); future = Futures.transform(fullActivityDataFuture, extractDeviceStateData(device), MoreExecutors.directExecutor()); diff --git a/application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java b/application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java deleted file mode 100644 index e160e0e5b5..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/stats/DefaultJsInvokeStats.java +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright © 2016-2025 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. - */ -package org.thingsboard.server.service.stats; - -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.thingsboard.server.actors.JsInvokeStats; -import org.thingsboard.server.common.stats.StatsCounter; -import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.common.stats.StatsType; - -@Service -public class DefaultJsInvokeStats implements JsInvokeStats { - private static final String REQUESTS = "requests"; - private static final String RESPONSES = "responses"; - private static final String FAILURES = "failures"; - - private StatsCounter requestsCounter; - private StatsCounter responsesCounter; - private StatsCounter failuresCounter; - - @Autowired - private StatsFactory statsFactory; - - @PostConstruct - public void init() { - String key = StatsType.JS_INVOKE.getName(); - this.requestsCounter = statsFactory.createStatsCounter(key, REQUESTS); - this.responsesCounter = statsFactory.createStatsCounter(key, RESPONSES); - this.failuresCounter = statsFactory.createStatsCounter(key, FAILURES); - } - - @Override - public void incrementRequests(int amount) { - requestsCounter.add(amount); - } - - @Override - public void incrementResponses(int amount) { - responsesCounter.add(amount); - } - - @Override - public void incrementFailures(int amount) { - failuresCounter.add(amount); - } - - @Override - public int getRequests() { - return requestsCounter.get(); - } - - @Override - public int getResponses() { - return responsesCounter.get(); - } - - @Override - public int getFailures() { - return failuresCounter.get(); - } - - @Override - public void reset() { - requestsCounter.clear(); - responsesCounter.clear(); - failuresCounter.clear(); - } -} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 2cdee7b7d0..1fafb562c1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.DeviceId; @@ -49,8 +48,6 @@ import org.thingsboard.server.queue.discovery.event.OtherServiceShutdownEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.state.DefaultDeviceStateService; -import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; @@ -76,7 +73,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueProducerProvider producerProvider; private final TbLocalSubscriptionService localSubscriptionService; - private final DeviceStateService deviceStateService; private final TbClusterService clusterService; private final SubscriptionSchedulerComponent scheduler; @@ -171,7 +167,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene callback.onSuccess(); } - public void onTimeSeriesUpdate(EntityId entityId, List update) { + private void onTimeSeriesUpdate(EntityId entityId, List update) { getEntityUpdatesInfo(entityId).timeSeriesUpdateTs = System.currentTimeMillis(); TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId); if (subInfo != null) { @@ -201,42 +197,27 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene @Override public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, TbCallback callback) { - onAttributesUpdate(tenantId, entityId, scope, attributes, true, callback); - } - - @Override - public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback) { getEntityUpdatesInfo(entityId).attributesUpdateTs = System.currentTimeMillis(); processAttributesUpdate(entityId, scope, attributes); - if (entityId.getEntityType() == EntityType.DEVICE) { - if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { - updateDeviceInactivityTimeout(tenantId, entityId, attributes); - } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { - clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, - new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) - , null); - } - } callback.onSuccess(); } + @Override + public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback callback) { + onAttributesDelete(tenantId, entityId, scope, keys, false, callback); + } + @Override public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback callback) { processAttributesUpdate(entityId, scope, keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); - if (entityId.getEntityType() == EntityType.DEVICE) { - if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope) - || TbAttributeSubscriptionScope.ANY_SCOPE.name().equalsIgnoreCase(scope)) { - deleteDeviceInactivityTimeout(tenantId, entityId, keys); - } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { - clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, - new DeviceId(entityId.getId()), scope, keys), null); - } + if (entityId.getEntityType() == EntityType.DEVICE && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { + clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), scope, keys), null); } callback.onSuccess(); } - public void processAttributesUpdate(EntityId entityId, String scope, List update) { + private void processAttributesUpdate(EntityId entityId, String scope, List update) { TbEntityRemoteSubsInfo subInfo = entitySubscriptions.get(entityId); if (subInfo != null) { log.trace("[{}] Handling attributes update: {}", entityId, update); @@ -264,22 +245,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } - private void updateDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List kvEntries) { - for (KvEntry kvEntry : kvEntries) { - if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { - deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), getLongValue(kvEntry)); - } - } - } - - private void deleteDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List keys) { - for (String key : keys) { - if (key.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { - deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), 0); - } - } - } - @Override public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onAlarmSubUpdate(tenantId, entityId, alarm, false, callback); @@ -349,29 +314,6 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } - private static long getLongValue(KvEntry kve) { - switch (kve.getDataType()) { - case LONG: - return kve.getLongValue().orElse(0L); - case DOUBLE: - return kve.getDoubleValue().orElse(0.0).longValue(); - case STRING: - try { - return Long.parseLong(kve.getStrValue().orElse("0")); - } catch (NumberFormatException e) { - return 0L; - } - case JSON: - try { - return Long.parseLong(kve.getJsonValue().orElse("0")); - } catch (NumberFormatException e) { - return 0L; - } - default: - return 0L; - } - } - private static List getSubList(List ts, Set keys) { List update = null; for (T entry : ts) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index d199f18b75..57d4fda5f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -39,8 +39,15 @@ public interface SubscriptionManagerService extends ApplicationListener attributes, TbCallback callback); - void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback); - + void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback empty); + + /** + * This method is retained solely for backwards compatibility, specifically to handle + * legacy proto messages that include the notifyDevice field. + * + * @deprecated as of 4.0, this method will be removed in future releases. + */ + @Deprecated(forRemoval = true, since = "4.0") void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback empty); void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 68c0f52f71..1d5e85cc22 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -209,7 +209,7 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } - public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice) { + public static ToCoreMsg toAttributesDeleteProto(TenantId tenantId, EntityId entityId, String scope, List keys) { TbAttributeDeleteProto.Builder builder = TbAttributeDeleteProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); @@ -218,7 +218,6 @@ public class TbSubscriptionUtils { builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); builder.setScope(scope); builder.addAllKeys(keys); - builder.setNotifyDevice(notifyDevice); SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); msgBuilder.setAttrDelete(builder); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 89b51ce84a..8ef6465d4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -529,6 +529,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public ListenableFuture deleteVersionControlSettings(TenantId tenantId) { + log.debug("[{}] Deleting version control settings", tenantId); + repositorySettingsService.delete(tenantId); return gitServiceQueue.clearRepository(tenantId); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java index 7225156008..d6fa89a7a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java @@ -111,8 +111,7 @@ public abstract class AbstractSubscriptionService extends TbApplicationEventList } @Override - public void onFailure(Throwable t) { - } + public void onFailure(Throwable t) {} }, executor); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 36cd5516a8..2302446b6b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -31,20 +31,26 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -52,10 +58,11 @@ import org.thingsboard.server.dao.util.KvUtils; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; +import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.ArrayList; -import java.util.Comparator; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,6 +72,11 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; +import static java.util.Comparator.comparing; +import static java.util.Comparator.comparingLong; +import static java.util.Comparator.naturalOrder; +import static java.util.Comparator.nullsFirst; + /** * Created by ashvayka on 27.03.18. */ @@ -78,6 +90,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageStateService apiUsageStateService; private final CalculatedFieldQueueService calculatedFieldQueueService; + private final DeviceStateManager deviceStateManager; private ExecutorService tsCallBackExecutor; @@ -89,13 +102,15 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Lazy TbEntityViewService tbEntityViewService, TbApiUsageReportClient apiUsageClient, TbApiUsageStateService apiUsageStateService, - CalculatedFieldQueueService calculatedFieldQueueService) { + CalculatedFieldQueueService calculatedFieldQueueService, + DeviceStateManager deviceStateManager) { this.attrService = attrService; this.tsService = tsService; this.tbEntityViewService = tbEntityViewService; this.apiUsageClient = apiUsageClient; this.apiUsageStateService = apiUsageStateService; this.calculatedFieldQueueService = calculatedFieldQueueService; + this.deviceStateManager = deviceStateManager; } @PostConstruct @@ -140,6 +155,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer EntityId entityId = request.getEntityId(); TimeseriesSaveRequest.Strategy strategy = request.getStrategy(); ListenableFuture resultFuture; + if (strategy.saveTimeseries() && strategy.saveLatest()) { resultFuture = tsService.save(tenantId, entityId, request.getEntries(), request.getTtl()); } else if (strategy.saveLatest()) { @@ -176,11 +192,68 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void saveAttributesInternal(AttributesSaveRequest request) { log.trace("Executing saveInternal [{}]", request); - ListenableFuture> saveFuture = attrService.save(request.getTenantId(), request.getEntityId(), request.getScope(), request.getEntries()); - DonAsynchron.withCallback(saveFuture, result -> { - calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()); - }, safeCallback(request.getCallback()), tsCallBackExecutor); - addWsCallback(saveFuture, success -> onAttributesUpdate(request.getTenantId(), request.getEntityId(), request.getScope().name(), request.getEntries(), request.isNotifyDevice())); + TenantId tenantId = request.getTenantId(); + EntityId entityId = request.getEntityId(); + AttributesSaveRequest.Strategy strategy = request.getStrategy(); + ListenableFuture> resultFuture; + + if (strategy.saveAttributes()) { + resultFuture = attrService.save(tenantId, entityId, request.getScope(), request.getEntries()); + } else { + resultFuture = Futures.immediateFuture(Collections.emptyList()); + } + + addMainCallback(resultFuture, result -> { + if (strategy.processCalculatedFields()) { + calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()); + } else { + request.getCallback().onSuccess(null); + } + }, t -> request.getCallback().onFailure(t)); + + if (shouldSendSharedAttributesUpdatedNotification(request)) { + addMainCallback(resultFuture, success -> clusterService.pushMsgToCore( + DeviceAttributesEventNotificationMsg.onUpdate(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getEntries()), null + )); + } + + if (shouldCheckForInactivityTimeoutUpdates(request)) { + findNewInactivityTimeout(request.getEntries()).ifPresent(newInactivityTimeout -> + addMainCallback(resultFuture, success -> deviceStateManager.onDeviceInactivityTimeoutUpdate( + tenantId, new DeviceId(entityId.getId()), newInactivityTimeout, TbCallback.EMPTY) + ) + ); + } + + if (strategy.sendWsUpdate()) { + addWsCallback(resultFuture, success -> onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries())); + } + } + + private static boolean shouldSendSharedAttributesUpdatedNotification(AttributesSaveRequest request) { + return request.getStrategy().saveAttributes() && shouldSendSharedAttributesNotification(request.getEntityId(), request.getScope(), request.isNotifyDevice()); + } + + private static boolean shouldCheckForInactivityTimeoutUpdates(AttributesSaveRequest request) { + return request.getStrategy().saveAttributes() + && request.getEntityId().getEntityType() == EntityType.DEVICE + && request.getScope() == AttributeScope.SERVER_SCOPE; + } + + private static Optional findNewInactivityTimeout(List entries) { + return entries.stream() + .filter(entry -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, entry.getKey())) + // Select the entry with the highest version, or if the versions are equal, the one with the most recent update timestamp + .max(comparing(AttributeKvEntry::getVersion, nullsFirst(naturalOrder())).thenComparingLong(AttributeKvEntry::getLastUpdateTs)) + .map(DefaultTelemetrySubscriptionService::parseAsLong); + } + + private static long parseAsLong(KvEntry kve) { + try { + return Long.parseLong(kve.getValueAsString()); + } catch (NumberFormatException e) { + return 0L; + } } @Override @@ -191,11 +264,45 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void deleteAttributesInternal(AttributesDeleteRequest request) { - ListenableFuture> deleteFuture = attrService.removeAll(request.getTenantId(), request.getEntityId(), request.getScope(), request.getKeys()); - DonAsynchron.withCallback(deleteFuture, result -> { - calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()); - }, safeCallback(request.getCallback()), tsCallBackExecutor); - addWsCallback(deleteFuture, success -> onAttributesDelete(request.getTenantId(), request.getEntityId(), request.getScope().name(), request.getKeys(), request.isNotifyDevice())); + TenantId tenantId = request.getTenantId(); + EntityId entityId = request.getEntityId(); + + ListenableFuture> deleteFuture = attrService.removeAll(tenantId, entityId, request.getScope(), request.getKeys()); + + addMainCallback(deleteFuture, + result -> calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback()), + t -> request.getCallback().onFailure(t) + ); + + if (shouldSendSharedAttributesDeletedNotification(request)) { + addMainCallback(deleteFuture, success -> clusterService.pushMsgToCore( + DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, request.getKeys()), null + )); + } + + if (inactivityTimeoutDeleted(request)) { + addMainCallback(deleteFuture, success -> deviceStateManager.onDeviceInactivityTimeoutUpdate( + tenantId, new DeviceId(entityId.getId()), 0L, TbCallback.EMPTY) + ); + } + + addWsCallback(deleteFuture, success -> onAttributesDelete(tenantId, entityId, request.getScope().name(), request.getKeys())); + } + + private static boolean shouldSendSharedAttributesDeletedNotification(AttributesDeleteRequest request) { + return shouldSendSharedAttributesNotification(request.getEntityId(), request.getScope(), request.isNotifyDevice()); + } + + private static boolean shouldSendSharedAttributesNotification(EntityId entityId, AttributeScope scope, boolean notifyDevice) { + return entityId.getEntityType() == EntityType.DEVICE + && scope == AttributeScope.SHARED_SCOPE + && notifyDevice; + } + + private static boolean inactivityTimeoutDeleted(AttributesDeleteRequest request) { + return request.getEntityId().getEntityType() == EntityType.DEVICE + && request.getScope() == AttributeScope.SERVER_SCOPE + && request.getKeys().stream().anyMatch(key -> Objects.equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT, key)); } @Override @@ -247,7 +354,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer if (entries != null) { Optional tsKvEntry = entries.stream() .filter(entry -> entry.getTs() > startTs && entry.getTs() <= endTs) - .max(Comparator.comparingLong(TsKvEntry::getTs)); + .max(comparingLong(TsKvEntry::getTs)); tsKvEntry.ifPresent(entityViewLatest::add); } } @@ -280,16 +387,16 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } } - private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice) { + private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes) { forwardToSubscriptionManagerService(tenantId, entityId, - subscriptionManagerService -> subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY), + subscriptionManagerService -> subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, TbCallback.EMPTY), () -> TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes)); } - private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice) { + private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys) { forwardToSubscriptionManagerService(tenantId, entityId, - subscriptionManagerService -> subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY), - () -> TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice)); + subscriptionManagerService -> subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, TbCallback.EMPTY), + () -> TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys)); } private void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index b01a0cf55e..b3022fb9d3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -502,8 +502,6 @@ actors: statistics: # Enable/disable actor statistics enabled: "${ACTORS_STATISTICS_ENABLED:true}" - # Frequency of printing the JS executor statistics - js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}" # Actors statistic persistence frequency in milliseconds persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}" calculated_fields: @@ -817,7 +815,7 @@ spring: events: # Enable dedicated datasource (a separate database) for events and audit logs. # Before enabling this, make sure you have set up the following tables in the new DB: - # error_event, lc_event, rule_chain_debug_event, rule_node_debug_event, stats_event, audit_log + # error_event, lc_event, rule_chain_debug_event, rule_node_debug_event, stats_event, audit_log, cf_debug_event enabled: "${SPRING_DEDICATED_EVENTS_DATASOURCE_ENABLED:false}" # Database driver for Spring JPA for events datasource driverClassName: "${SPRING_EVENTS_DATASOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver}" @@ -858,6 +856,7 @@ audit-log: "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation + "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" @@ -1283,7 +1282,7 @@ transport: # URL of gateways dashboard repository repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}" # Branch of gateways dashboard repository to work with - branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:}" + branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.0.0}" # Fetch frequency in hours for gateways dashboard repository fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" @@ -1758,9 +1757,6 @@ queue: stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" - # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" - vc: # Default topic name topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" @@ -1817,9 +1813,7 @@ queue: # For high-priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}" # Interval in milliseconds to poll messages by CF (Rule Engine) microservices - poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}" - # Amount of partitions used by CF microservices - partitions: "${TB_QUEUE_CF_PARTITIONS:10}" + poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:1000}" # Timeout for processing a message pack by CF microservices pack_processing_timeout: "${TB_QUEUE_CF_PACK_PROCESSING_TIMEOUT_MS:60000}" # Thread pool size for processing of the incoming messages diff --git a/application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java b/application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java new file mode 100644 index 0000000000..9cd75e9a3c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/RepositorySettingsTest.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2025 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. + */ +package org.thingsboard.server.controller; + +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; +import org.thingsboard.server.common.data.sync.vc.RepositorySettings; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@DaoSqlTest +public class RepositorySettingsTest extends AbstractControllerTest { + + @MockBean + private GitVersionControlQueueService gitVersionControlQueueService; + + @Test + public void testFindRepositorySettings() throws Exception { + loginTenantAdmin(); + doGet("/api/admin/repositorySettings") + .andExpect(status().isNotFound()); + + String testRepositoryUri = "https://github.com/test/version-control-test-repository.git"; + + SettableFuture successFuture = SettableFuture.create(); + successFuture.set(null); + when(gitVersionControlQueueService.initRepository(any(), any())) + .thenReturn(successFuture); + + RepositorySettings repositorySettings = new RepositorySettings(); + repositorySettings.setPassword("test"); + repositorySettings.setAuthMethod(RepositoryAuthMethod.USERNAME_PASSWORD); + repositorySettings.setRepositoryUri(testRepositoryUri); + repositorySettings.setDefaultBranch("main"); + doPost("/api/admin/repositorySettings", repositorySettings) + .andExpect(status().isOk()); + + // check repository settings + doGet("/api/admin/repositorySettings") + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.repositoryUri", is(testRepositoryUri))); + + // delete settings + when(gitVersionControlQueueService.clearRepository(any())) + .thenReturn(successFuture); + doDelete("/api/admin/repositorySettings") + .andExpect(status().isOk()); + + // check repository settings + doGet("/api/admin/repositorySettings") + .andExpect(status().isNotFound()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index dee32e7d1f..91eced64f6 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.cf.ctx.state; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -51,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -@SpringBootTest(classes = DefaultTbelInvokeService.class) +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class}) public class ScriptCalculatedFieldStateTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("5b18e321-3327-4290-b996-d72a65e90382")); @@ -191,7 +193,7 @@ public class ScriptCalculatedFieldStateTest { config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2)); - config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity.value}"); + config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}"); Output output = new Output(); output.setType(OutputType.ATTRIBUTES); diff --git a/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java b/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java index f8563e5e2a..e3383e32a8 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.script; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.thingsboard.common.util.JacksonUtil; @@ -22,7 +23,7 @@ import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.common.stats.DefaultStatsFactory; import java.util.Map; import java.util.UUID; @@ -30,7 +31,7 @@ import java.util.concurrent.ExecutionException; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; -@SpringBootTest(classes = DefaultTbelInvokeService.class) +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class }) public abstract class AbstractTbelInvokeTest { @Autowired diff --git a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java index 7404427f92..363f21fa10 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java @@ -21,9 +21,13 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.script.api.ScriptType; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.DefaultStatsFactory; +import org.thingsboard.server.common.stats.StatsCounter; +import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.gen.js.JsInvokeProtos; @@ -68,6 +72,10 @@ class RemoteJsInvokeServiceTest { remoteJsInvokeService = new RemoteJsInvokeService(Optional.of(apiUsageStateClient), Optional.of(apiUsageReportClient)); jsRequestTemplate = mock(TbQueueRequestTemplate.class); remoteJsInvokeService.requestTemplate = jsRequestTemplate; + StatsFactory statsFactory = mock(StatsFactory.class); + when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class)); + ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory); + remoteJsInvokeService.init(); } @AfterEach diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java index f0276d2b62..26e913eacc 100644 --- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java @@ -69,7 +69,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -1074,7 +1074,7 @@ public class DefaultDeviceStateServiceTest { final long defaultTimeout = 1000; initStateService(defaultTimeout); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); - given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) @@ -1156,7 +1156,7 @@ public class DefaultDeviceStateServiceTest { final long defaultTimeout = 1000; initStateService(defaultTimeout); given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId)); - given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList())); + given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyCollection())).willReturn(Futures.immediateFuture(Collections.emptyList())); long currentTime = System.currentTimeMillis(); DeviceState deviceState = DeviceState.builder() diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 6bb4f0e2fe..64845a12cd 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -24,25 +24,36 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.rule.engine.api.AttributesDeleteRequest; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.objects.AttributesEntityView; @@ -50,6 +61,7 @@ import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -73,14 +85,17 @@ import java.util.concurrent.ExecutorService; import java.util.stream.LongStream; import java.util.stream.Stream; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) class DefaultTelemetrySubscriptionServiceTest { @@ -91,7 +106,7 @@ class DefaultTelemetrySubscriptionServiceTest { final long sampleTtl = 10_000L; - final List sampleTelemetry = List.of( + final List sampleTimeseries = List.of( new BasicTsKvEntry(100L, new DoubleDataEntry("temperature", 65.2)), new BasicTsKvEntry(100L, new DoubleDataEntry("humidity", 33.1)) ); @@ -124,12 +139,14 @@ class DefaultTelemetrySubscriptionServiceTest { TbApiUsageStateService apiUsageStateService; @Mock CalculatedFieldQueueService calculatedFieldQueueService; + @Mock + DeviceStateManager deviceStateManager; DefaultTelemetrySubscriptionService telemetryService; @BeforeEach void setup() { - telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService); + telemetryService = new DefaultTelemetrySubscriptionService(attrService, tsService, tbEntityViewService, apiUsageClient, apiUsageStateService, calculatedFieldQueueService, deviceStateManager); ReflectionTestUtils.setField(telemetryService, "clusterService", clusterService); ReflectionTestUtils.setField(telemetryService, "partitionService", partitionService); ReflectionTestUtils.setField(telemetryService, "subscriptionManagerService", Optional.of(subscriptionManagerService)); @@ -146,9 +163,9 @@ class DefaultTelemetrySubscriptionServiceTest { lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi); - lenient().when(tsService.save(tenantId, entityId, sampleTelemetry, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), listOfNNumbers(sampleTelemetry.size())))); - lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), null))); - lenient().when(tsService.saveLatest(tenantId, entityId, sampleTelemetry)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), listOfNNumbers(sampleTelemetry.size())))); + lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); + lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null))); + lenient().when(tsService.saveLatest(tenantId, entityId, sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); // mock no entity views lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(Collections.emptyList())); @@ -179,7 +196,7 @@ class DefaultTelemetrySubscriptionServiceTest { .tenantId(tenantId) .customerId(customerId) .entityId(new ApiUsageStateId(UUID.randomUUID())) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL) .build(); @@ -199,7 +216,7 @@ class DefaultTelemetrySubscriptionServiceTest { .tenantId(tenantId) .customerId(customerId) .entityId(entityId) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .ttl(sampleTtl) .strategy(new TimeseriesSaveRequest.Strategy(true, false, false, false)) .build(); @@ -208,7 +225,7 @@ class DefaultTelemetrySubscriptionServiceTest { telemetryService.saveTimeseries(request); // THEN - then(apiUsageClient).should().report(tenantId, customerId, ApiUsageRecordKey.STORAGE_DP_COUNT, sampleTelemetry.size()); + then(apiUsageClient).should().report(tenantId, customerId, ApiUsageRecordKey.STORAGE_DP_COUNT, sampleTimeseries.size()); } @Test @@ -218,7 +235,7 @@ class DefaultTelemetrySubscriptionServiceTest { .tenantId(tenantId) .customerId(customerId) .entityId(entityId) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .ttl(sampleTtl) .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS) .build(); @@ -240,7 +257,7 @@ class DefaultTelemetrySubscriptionServiceTest { .tenantId(tenantId) .customerId(customerId) .entityId(entityId) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .ttl(sampleTtl) .strategy(TimeseriesSaveRequest.Strategy.PROCESS_ALL) .future(future) @@ -266,7 +283,7 @@ class DefaultTelemetrySubscriptionServiceTest { .tenantId(tenantId) .customerId(customerId) .entityId(entityId) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .ttl(sampleTtl) .strategy(TimeseriesSaveRequest.Strategy.LATEST_AND_WS) .future(future) @@ -286,12 +303,12 @@ class DefaultTelemetrySubscriptionServiceTest { entityView.setTenantId(tenantId); entityView.setCustomerId(customerId); entityView.setEntityId(entityId); - entityView.setKeys(new TelemetryEntityView(sampleTelemetry.stream().map(KvEntry::getKey).toList(), new AttributesEntityView())); + entityView.setKeys(new TelemetryEntityView(sampleTimeseries.stream().map(KvEntry::getKey).toList(), new AttributesEntityView())); // mock that there is one entity view given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView))); // mock that save latest call for entity view is successful - given(tsService.saveLatest(tenantId, entityView.getId(), sampleTelemetry)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTelemetry.size(), listOfNNumbers(sampleTelemetry.size())))); + given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); // mock TPI for entity view given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi); @@ -299,7 +316,7 @@ class DefaultTelemetrySubscriptionServiceTest { .tenantId(tenantId) .customerId(customerId) .entityId(entityId) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .ttl(sampleTtl) .strategy(new TimeseriesSaveRequest.Strategy(false, true, false, false)) .build(); @@ -309,12 +326,12 @@ class DefaultTelemetrySubscriptionServiceTest { // THEN // should save latest to both the main entity and it's entity view - then(tsService).should().saveLatest(tenantId, entityId, sampleTelemetry); - then(tsService).should().saveLatest(tenantId, entityView.getId(), sampleTelemetry); + then(tsService).should().saveLatest(tenantId, entityId, sampleTimeseries); + then(tsService).should().saveLatest(tenantId, entityView.getId(), sampleTimeseries); then(tsService).shouldHaveNoMoreInteractions(); // should send WS update only for entity view (WS update for the main entity is disabled in the save request) - then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityView.getId(), sampleTelemetry, TbCallback.EMPTY); + then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityView.getId(), sampleTimeseries, TbCallback.EMPTY); then(subscriptionManagerService).shouldHaveNoMoreInteractions(); } @@ -325,7 +342,7 @@ class DefaultTelemetrySubscriptionServiceTest { .tenantId(tenantId) .customerId(customerId) .entityId(entityId) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .ttl(sampleTtl) .strategy(new TimeseriesSaveRequest.Strategy(true, false, false, false)) .build(); @@ -335,7 +352,7 @@ class DefaultTelemetrySubscriptionServiceTest { // THEN // should save only time series for the main entity - then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl); + then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl); then(tsService).shouldHaveNoMoreInteractions(); // should not send any WS updates @@ -343,14 +360,14 @@ class DefaultTelemetrySubscriptionServiceTest { } @ParameterizedTest - @MethodSource("booleanCombinations") - void shouldCallCorrectApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveTimeseries, boolean saveLatest, boolean sendWsUpdate, boolean processCalculatedFields) { + @MethodSource("allCombinationsOfFourBooleans") + void shouldCallCorrectSaveTimeseriesApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveTimeseries, boolean saveLatest, boolean sendWsUpdate, boolean processCalculatedFields) { // GIVEN var request = TimeseriesSaveRequest.builder() .tenantId(tenantId) .customerId(customerId) .entityId(entityId) - .entries(sampleTelemetry) + .entries(sampleTimeseries) .ttl(sampleTtl) .strategy(new TimeseriesSaveRequest.Strategy(saveTimeseries, saveLatest, sendWsUpdate, processCalculatedFields)) .build(); @@ -360,11 +377,11 @@ class DefaultTelemetrySubscriptionServiceTest { // THEN if (saveTimeseries && saveLatest) { - then(tsService).should().save(tenantId, entityId, sampleTelemetry, sampleTtl); + then(tsService).should().save(tenantId, entityId, sampleTimeseries, sampleTtl); } else if (saveLatest) { - then(tsService).should().saveLatest(tenantId, entityId, sampleTelemetry); + then(tsService).should().saveLatest(tenantId, entityId, sampleTimeseries); } else if (saveTimeseries) { - then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTelemetry, sampleTtl); + then(tsService).should().saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl); } if (processCalculatedFields) { @@ -374,13 +391,13 @@ class DefaultTelemetrySubscriptionServiceTest { then(tsService).shouldHaveNoMoreInteractions(); if (sendWsUpdate) { - then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityId, sampleTelemetry, TbCallback.EMPTY); + then(subscriptionManagerService).should().onTimeSeriesUpdate(tenantId, entityId, sampleTimeseries, TbCallback.EMPTY); } else { then(subscriptionManagerService).shouldHaveNoInteractions(); } } - private static Stream booleanCombinations() { + private static Stream allCombinationsOfFourBooleans() { return Stream.of( Arguments.of(true, true, true, true), Arguments.of(true, true, true, false), @@ -401,7 +418,693 @@ class DefaultTelemetrySubscriptionServiceTest { ); } - // used to emulate sequence numbers returned by save latest API + /* --- Save attributes API --- */ + + @ParameterizedTest + @MethodSource("allCombinationsOfThreeBooleans") + void shouldCallCorrectSaveAttributesApiBasedOnBooleanFlagsInTheSaveRequest(boolean saveAttributes, boolean sendWsUpdate, boolean processCalculatedFields) { + // GIVEN + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(new DoubleDataEntry("temperature", 65.2)) + .notifyDevice(false) + .strategy(new AttributesSaveRequest.Strategy(saveAttributes, sendWsUpdate, processCalculatedFields)) + .build(); + + lenient().when(attrService.save(tenantId, entityId, request.getScope(), request.getEntries())).thenReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + if (saveAttributes) { + then(attrService).should().save(tenantId, entityId, request.getScope(), request.getEntries()); + } else { + then(attrService).shouldHaveNoInteractions(); + } + + if (processCalculatedFields) { + then(calculatedFieldQueueService).should().pushRequestToQueue(eq(request), any(), eq(request.getCallback())); + } + + if (sendWsUpdate) { + then(subscriptionManagerService).should().onAttributesUpdate(tenantId, entityId, request.getScope().name(), request.getEntries(), TbCallback.EMPTY); + } else { + then(subscriptionManagerService).shouldHaveNoInteractions(); + } + } + + static Stream allCombinationsOfThreeBooleans() { + return Stream.of( + Arguments.of(true, true, true), + Arguments.of(true, true, false), + Arguments.of(true, false, true), + Arguments.of(true, false, false), + Arguments.of(false, true, true), + Arguments.of(false, true, false), + Arguments.of(false, false, true), + Arguments.of(false, false, false) + ); + } + + @Test + void shouldThrowErrorWhenTryingToSaveAttributesForApiUsageState() { + // GIVEN + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(new ApiUsageStateId(UUID.randomUUID())) + .scope(AttributeScope.SHARED_SCOPE) + .entry(new DoubleDataEntry("temperature", 65.2)) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + // WHEN + assertThatThrownBy(() -> telemetryService.saveAttributes(request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Can't update API Usage State!"); + + // THEN + then(attrService).shouldHaveNoInteractions(); + } + + @Test + void shouldSendAttributesUpdateNotificationWhenDeviceSharedAttributesAreSavedAndNotifyDeviceIsTrue() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + var expectedAttributesUpdateMsg = DeviceAttributesEventNotificationMsg.onUpdate(tenantId, deviceId, "SHARED_SCOPE", entries); + + then(clusterService).should().pushMsgToCore(eq(expectedAttributesUpdateMsg), isNull()); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesUpdateNotificationWhenEntityIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, nonDeviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = "SHARED_SCOPE", + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesUpdateNotificationWhenAttributesAreNotShared(AttributeScope notSharedScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(notSharedScope) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesUpdateNotificationWhenNotifyDeviceIsFalse() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(false) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFuture(listOfNNumbers(entries.size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesUpdateNotificationWhenAttributesSaveWasSkipped() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(false, false, false)) + .build(); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesUpdateNotificationWhenAttributesSaveFailed() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(123L, new DoubleDataEntry("shared1", 65.2)), + new BaseAttributeKvEntry(456L, new StringDataEntry("shared2", "test")) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .entries(entries) + .notifyDevice(true) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), entries)).willReturn(immediateFailedFuture(new RuntimeException("failed to save"))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutWasUpdated() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 5000L, TbCallback.EMPTY); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutSaveWasSkipped() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(false, true, true)) + .build(); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasUpdatedButEntityTypeIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, nonDeviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = {"SERVER_SCOPE"}, + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasUpdatedButAttributeScopeIsNotServer(AttributeScope nonServerScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("inactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(nonServerScope) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenUpdatedAttributesDoNotContainInactivityTimeout() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + var inactivityTimeout = new BaseAttributeKvEntry(123L, new LongDataEntry("notInactivityTimeout", 5000L)); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entry(inactivityTimeout) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldUseInactivityTimeoutEntryWithTheGreatestVersion() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 0L), 0L, null), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 1000L), 3L, 1L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 2000L), 2L, 2L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 3000L), 1L, 3L) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entries(entries) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 3000L, TbCallback.EMPTY); + } + + @Test + void shouldUseInactivityTimeoutEntryWithTheGreatestLastUpdateTsWhenVersionsAreTheSame() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List entries = List.of( + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 1000L), 1L, 1L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 2000L), 2L, 1L), + new BaseAttributeKvEntry(new LongDataEntry("inactivityTimeout", 3000L), 3L, 1L) + ); + + var request = AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .entries(entries) + .strategy(new AttributesSaveRequest.Strategy(true, false, false)) + .build(); + + given(attrService.save(tenantId, deviceId, request.getScope(), request.getEntries())).willReturn(immediateFuture(listOfNNumbers(request.getEntries().size()))); + + // WHEN + telemetryService.saveAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 3000L, TbCallback.EMPTY); + } + + /* --- Delete attributes API --- */ + + @Test + void shouldThrowErrorWhenTryingToDeleteAttributesForApiUsageState() { + // GIVEN + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(new ApiUsageStateId(UUID.randomUUID())) + .scope(AttributeScope.SHARED_SCOPE) + .keys(List.of("attributeKeyToDelete1", "attributeKeyToDelete2")) + .notifyDevice(true) + .build(); + + // WHEN + assertThatThrownBy(() -> telemetryService.deleteAttributes(request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Can't update API Usage State!"); + + // THEN + then(attrService).shouldHaveNoInteractions(); + } + + @Test + void shouldSendAttributesDeletedNotificationWhenDeviceSharedAttributesAreDeletedAndNotifyDeviceIsTrue() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + var expectedAttributesDeletedMsg = DeviceAttributesEventNotificationMsg.onDelete(tenantId, deviceId, "SHARED_SCOPE", List.of("attributeKeyToDelete1", "attributeKeyToDelete2")); + + then(clusterService).should().pushMsgToCore(eq(expectedAttributesDeletedMsg), isNull()); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesDeletedNotificationWhenEntityIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, nonDeviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = "SHARED_SCOPE", + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotSendAttributesDeletedNotificationWhenAttributesAreNotShared(AttributeScope notSharedScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(notSharedScope) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesDeletedNotificationWhenNotifyDeviceIsFalse() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(false) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFuture(keys)); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotSendAttributesDeletedNotificationWhenAttributesDeleteFailed() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + List keys = List.of("attributeKeyToDelete1", "attributeKeyToDelete2"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SHARED_SCOPE) + .keys(keys) + .notifyDevice(true) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), keys)).willReturn(immediateFailedFuture(new RuntimeException("failed to delete"))); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(clusterService).should(never()).pushMsgToCore(any(), any()); + } + + @Test + void shouldNotifyDeviceStateManagerWhenDeviceInactivityTimeoutWasDeleted() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).should().onDeviceInactivityTimeoutUpdate(tenantId, deviceId, 0L, TbCallback.EMPTY); + } + + @ParameterizedTest + @EnumSource( + value = EntityType.class, + names = {"DEVICE", "API_USAGE_STATE"}, // API usage state excluded due to coverage in another test + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasDeletedButEntityTypeIsNotDevice(EntityType entityType) { + // GIVEN + var nonDeviceId = EntityIdFactory.getByTypeAndUuid(entityType, "cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(nonDeviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, nonDeviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @ParameterizedTest + @EnumSource( + value = AttributeScope.class, + names = {"SERVER_SCOPE"}, + mode = EnumSource.Mode.EXCLUDE + ) + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasDeletedButAttributeScopeIsNotServer(AttributeScope nonServerScope) { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(nonServerScope) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenInactivityTimeoutWasNotDeleted() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFuture(request.getKeys())); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldNotNotifyDeviceStateManagerWhenDeviceInactivityTimeoutDeleteFailed() { + // GIVEN + var deviceId = DeviceId.fromString("cc51e450-53e1-11ee-883e-e56b48fd2088"); + + var request = AttributesDeleteRequest.builder() + .tenantId(tenantId) + .entityId(deviceId) + .scope(AttributeScope.SERVER_SCOPE) + .keys(List.of("inactivityTimeout", "someOtherDeletedAttribute")) + .build(); + + given(attrService.removeAll(tenantId, deviceId, request.getScope(), request.getKeys())).willReturn(immediateFailedFuture(new RuntimeException("failed to delete"))); + + // WHEN + telemetryService.deleteAttributes(request); + + // THEN + then(deviceStateManager).shouldHaveNoInteractions(); + } + + // used to emulate versions returned by save APIs private static List listOfNNumbers(int N) { return LongStream.range(0, N).boxed().toList(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index b7c7584931..5c7f0bb53d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -58,6 +59,7 @@ public class TenantProfile extends BaseData implements HasName @Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false") private boolean isolatedTbRuleEngine; + @Valid @Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.") private transient TenantProfileData profileData; @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java index 7674fba200..ba3c105a87 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/EntityViewFields.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.common.data.edqs.fields; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import java.util.UUID; +import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; + @Data @NoArgsConstructor @SuperBuilder @@ -29,9 +32,9 @@ public class EntityViewFields extends AbstractEntityFields { private String type; private String additionalInfo; - public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, String additionalInfo, Long version) { + public EntityViewFields(UUID id, long createdTime, UUID tenantId, UUID customerId, String name, String type, JsonNode additionalInfo, Long version) { super(id, createdTime, tenantId, customerId, name, version); this.type = type; - this.additionalInfo = additionalInfo; + this.additionalInfo = getText(additionalInfo); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java index a36514248c..9ba6c20188 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java @@ -289,7 +289,7 @@ public class FieldsUtil { } public static String getText(JsonNode node) { - return node != null ? node.asText() : ""; + return node != null ? node.toString() : ""; } private static UUID getCustomerId(CustomerId customerId) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 9c86785a5e..f256b02d9a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.tenant.profile; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -135,10 +136,17 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private double warnThreshold; + @Schema(example = "5") private long maxCalculatedFieldsPerEntity = 5; + @Schema(example = "10") private long maxArgumentsPerCF = 10; + @Builder.Default + @Min(value = 1, message = "must be at least 1") + @Schema(example = "1000") private long maxDataPointsPerRollingArg = 1000; + @Schema(example = "32") private long maxStateSizeInKBytes = 32; + @Schema(example = "2") private long maxSingleValueArgumentSizeInKBytes = 2; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java index b1f6c27fd8..44ca79cabb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.tenant.profile; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import lombok.Data; import java.io.Serializable; @@ -27,6 +28,7 @@ public class TenantProfileData implements Serializable { private static final long serialVersionUID = -3642550257035920976L; + @Valid @Schema(description = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") private TenantProfileConfiguration configuration; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 0c55bc50dd..59255b11cc 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -430,6 +430,9 @@ public class TenantRepo { private String getEntityName(EntityId entityId) { EntityType entityType = entityId.getEntityType(); + if (entityType == EntityType.TENANT && entityId.getId().equals(TenantId.NULL_UUID)) { + return ""; + } return switch (entityType) { case CUSTOMER, TENANT -> getEntityMap(entityType).get(entityId.getId()).getFields().getName(); default -> throw new RuntimeException("Unsupported entity type: " + entityType); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java index a12a12dbe3..442453fc93 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java @@ -20,7 +20,6 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -43,18 +42,6 @@ public class EdqsStatsService { private final ConcurrentHashMap statsMap = new ConcurrentHashMap<>(); private final StatsFactory statsFactory; - @Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:300000}", - fixedDelayString = "${queue.edqs.stats.print-interval-ms:300000}") - private void reportStats() { - if (statsMap.isEmpty()) { - return; - } - String values = statsMap.entrySet().stream() - .map(kv -> "TenantId [" + kv.getKey() + "] stats [" + kv.getValue() + "]") - .collect(Collectors.joining(System.lineSeparator())); - log.info("EDQS Stats: {}", values); - } - public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory)) .reportEvent(objectType, eventType); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java index 38a4853219..44756013ca 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldPartitionChangeMsg.java @@ -26,8 +26,6 @@ import java.util.Set; @Data public class CalculatedFieldPartitionChangeMsg implements ToCalculatedFieldSystemMsg { - private final boolean[] partitions; - @Override public TenantId getTenantId() { return TenantId.SYS_TENANT_ID; diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 38f86bf9db..6ecdf13981 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1122,7 +1122,11 @@ message TbAttributeDeleteProto { int64 tenantIdLSB = 5; string scope = 6; repeated string keys = 7; - bool notifyDevice = 8; + // DEPRECATED. FOR REMOVAL + // Since 4.0, this field is no longer used. + // Device notifications are now handled directly by DefaultTelemetrySubscriptionService, + // eliminating the need to pass this parameter through the queue and proto to DefaultSubscriptionManagerService. + optional bool notifyDevice = 8 [deprecated = true]; } message TbTimeSeriesDeleteProto { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index b42f8cc380..345b44e764 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.exception.TenantNotFoundException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -67,8 +68,6 @@ public class HashPartitionService implements PartitionService { private String cfEventTopic; @Value("${queue.calculated_fields.state_topic:tb_cf_state}") private String cfStateTopic; - @Value("${queue.calculated_fields.partitions:10}") - private Integer cfPartitions; @Value("${queue.vc.topic:tb_version_control}") private String vcTopic; @Value("${queue.vc.partitions:10}") @@ -122,11 +121,6 @@ public class HashPartitionService implements PartitionService { partitionSizesMap.put(coreKey, corePartitions); partitionTopicsMap.put(coreKey, coreTopic); - partitionSizesMap.put(QueueKey.CF, cfPartitions); - partitionTopicsMap.put(QueueKey.CF, cfEventTopic); - partitionSizesMap.put(QueueKey.CF_STATES, cfPartitions); - partitionTopicsMap.put(QueueKey.CF_STATES, cfStateTopic); - QueueKey vcKey = new QueueKey(ServiceType.TB_VC_EXECUTOR); partitionSizesMap.put(vcKey, vcPartitions); partitionTopicsMap.put(vcKey, vcTopic); @@ -165,6 +159,14 @@ public class HashPartitionService implements PartitionService { List queueRoutingInfoList = getQueueRoutingInfos(); queueRoutingInfoList.forEach(queue -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionSizesMap.put(cfQueueKey, queue.getPartitions()); + partitionTopicsMap.put(cfQueueKey, cfEventTopic); + QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionSizesMap.put(cfQueueStatesKey, queue.getPartitions()); + partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); + } partitionTopicsMap.put(queueKey, queue.getQueueTopic()); partitionSizesMap.put(queueKey, queue.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queue)); @@ -213,6 +215,14 @@ public class HashPartitionService implements PartitionService { QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg); TenantId tenantId = queueRoutingInfo.getTenantId(); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId); + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionSizesMap.put(cfQueueKey, queueRoutingInfo.getPartitions()); + partitionTopicsMap.put(cfQueueKey, cfEventTopic); + QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionSizesMap.put(cfQueueStatesKey, queueRoutingInfo.getPartitions()); + partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); + } partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic()); partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo)); @@ -252,6 +262,15 @@ public class HashPartitionService implements PartitionService { partitionTopicsMap.remove(queueKey); partitionSizesMap.remove(queueKey); queueConfigs.remove(queueKey); + + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionSizesMap.remove(cfQueueKey); + partitionTopicsMap.remove(cfQueueKey); + QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionSizesMap.remove(cfQueueStatesKey); + partitionTopicsMap.remove(cfQueueStatesKey); + } } @Override @@ -336,8 +355,7 @@ public class HashPartitionService implements PartitionService { } } - @Override - public TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { + private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { Integer partitionSize = partitionSizesMap.get(queueKey); if (partitionSize == null) { throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing"); @@ -552,11 +570,6 @@ public class HashPartitionService implements PartitionService { return list == null ? 0 : list.size(); } - @Override - public int getTotalCalculatedFieldPartitions() { - return cfPartitions; - } - private Map> getServiceKeyListMap(List services) { final Map> currentMap = new HashMap<>(); services.forEach(serviceInfo -> { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index 404b0258c0..7abd68e25f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -37,8 +37,6 @@ public interface PartitionService { TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId); - TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId); - List resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId); boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId); @@ -81,6 +79,4 @@ public interface PartitionService { int resolvePartitionIndex(UUID entityId, int partitions); - int getTotalCalculatedFieldPartitions(); - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java index ca38959fdd..6720a9d71e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java @@ -35,9 +35,6 @@ public class QueueKey { private final String queueName; private final TenantId tenantId; - public static final QueueKey CF = new QueueKey(ServiceType.TB_RULE_ENGINE).withQueueName(CF_QUEUE_NAME); - public static final QueueKey CF_STATES = new QueueKey(ServiceType.TB_RULE_ENGINE).withQueueName(CF_STATES_QUEUE_NAME); - public QueueKey(ServiceType type, Queue queue) { this.type = type; this.queueName = queue.getName(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index 597463300a..f165f60be7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -64,10 +64,10 @@ public class PartitionChangeEvent extends TbApplicationEvent { } public Set getCfPartitions() { - return newPartitions.getOrDefault(QueueKey.CF, Collections.emptySet()); + return getPartitionsByServiceTypeAndQueueName(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); } - private Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { + public Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { return newPartitions.entrySet() .stream() .filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName())) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index bb9c1a028e..f3d1e2d158 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -494,7 +494,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi public TbQueueConsumer> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); + consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic()); consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index f3adb266a4..3c6d144a0c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -440,7 +440,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { public TbQueueConsumer> createEdgeEventMsgConsumer(TenantId tenantId, EdgeId edgeId) { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); + consumerBuilder.topic(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId).getTopic()); consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java index 19643f4ad4..c8d91b0c0f 100644 --- a/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java +++ b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java @@ -42,7 +42,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class ZkDiscoveryServiceTest { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index 7cbac0401d..3d9b855064 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -20,12 +20,16 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfObject; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsCounter; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import java.util.Map; import java.util.UUID; @@ -34,22 +38,31 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import static java.lang.String.format; @Slf4j public abstract class AbstractScriptInvokeService implements ScriptInvokeService { + private static final String REQUESTS = "requests"; + private static final String INVOKE_RESPONSES = "invoke_responses"; + private static final String EVAL_RESPONSES = "eval_responses"; + private static final String FAILURES = "failures"; + private static final String TIMEOUTS = "timeouts"; + protected final Map disabledScripts = new ConcurrentHashMap<>(); - private final AtomicInteger pushedMsgs = new AtomicInteger(0); - private final AtomicInteger invokeMsgs = new AtomicInteger(0); - private final AtomicInteger evalMsgs = new AtomicInteger(0); - protected final AtomicInteger failedMsgs = new AtomicInteger(0); - protected final AtomicInteger timeoutMsgs = new AtomicInteger(0); - private final FutureCallback evalCallback = new ScriptStatCallback<>(evalMsgs, timeoutMsgs, failedMsgs); - private final FutureCallback invokeCallback = new ScriptStatCallback<>(invokeMsgs, timeoutMsgs, failedMsgs); + private StatsCounter requestsCounter; + private StatsCounter invokeResponsesCounter; + private StatsCounter evalResponsesCounter; + private StatsCounter failuresCounter; + private StatsCounter timeoutsCounter; + + private FutureCallback evalCallback; + private FutureCallback invokeCallback; + + @Autowired + private StatsFactory statsFactory; protected ScheduledExecutorService timeoutExecutorService; @@ -78,6 +91,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService protected abstract boolean isScriptPresent(UUID scriptId); protected abstract boolean isExecEnabled(TenantId tenantId); + protected abstract void reportExecution(TenantId tenantId, CustomerId customerId); protected abstract ListenableFuture doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames); @@ -87,6 +101,14 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService protected abstract void doRelease(UUID scriptId) throws Exception; public void init() { + String key = getStatsType().getName(); + this.requestsCounter = statsFactory.createStatsCounter(key, REQUESTS); + this.invokeResponsesCounter = statsFactory.createStatsCounter(key, INVOKE_RESPONSES); + this.evalResponsesCounter = statsFactory.createStatsCounter(key, EVAL_RESPONSES); + this.failuresCounter = statsFactory.createStatsCounter(key, FAILURES); + this.timeoutsCounter = statsFactory.createStatsCounter(key, TIMEOUTS); + this.evalCallback = new ScriptStatCallback<>(evalResponsesCounter, timeoutsCounter, failuresCounter); + this.invokeCallback = new ScriptStatCallback<>(invokeResponsesCounter, timeoutsCounter, failuresCounter); if (getMaxEvalRequestsTimeout() > 0 || getMaxInvokeRequestsTimeout() > 0) { timeoutExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("script-timeout"); } @@ -100,11 +122,11 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService public void printStats() { if (isStatsEnabled()) { - int pushed = pushedMsgs.getAndSet(0); - int invoked = invokeMsgs.getAndSet(0); - int evaluated = evalMsgs.getAndSet(0); - int failed = failedMsgs.getAndSet(0); - int timedOut = timeoutMsgs.getAndSet(0); + int pushed = requestsCounter.getAndClear(); + int invoked = invokeResponsesCounter.getAndClear(); + int evaluated = evalResponsesCounter.getAndClear(); + int failed = failuresCounter.getAndClear(); + int timedOut = timeoutsCounter.getAndClear(); if (pushed > 0 || invoked > 0 || evaluated > 0 || failed > 0 || timedOut > 0) { log.info("{}: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]", getStatsName(), pushed, invoked + evaluated, invoked, evaluated, failed, timedOut); @@ -119,7 +141,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); } UUID scriptId = UUID.randomUUID(); - pushedMsgs.incrementAndGet(); + requestsCounter.increment(); return withTimeoutAndStatsCallback(scriptId, null, doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); } else { @@ -141,7 +163,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService return Futures.immediateFailedFuture(handleScriptException(scriptId, null, t)); } reportExecution(tenantId, customerId); - pushedMsgs.incrementAndGet(); + requestsCounter.increment(); log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout()); var task = doInvokeFunction(scriptId, args); @@ -278,4 +300,6 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService private ListenableFuture error(String message) { return Futures.immediateFailedFuture(new RuntimeException(message)); } + + protected abstract StatsType getStatsType(); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java index 33812b7852..fd6f5bf7d7 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java @@ -19,29 +19,29 @@ import com.google.common.util.concurrent.FutureCallback; import jakarta.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.stats.StatsCounter; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; @Slf4j @AllArgsConstructor public class ScriptStatCallback implements FutureCallback { - private final AtomicInteger successMsgs; - private final AtomicInteger timeoutMsgs; - private final AtomicInteger failedMsgs; + private final StatsCounter successMsgs; + private final StatsCounter timeoutMsgs; + private final StatsCounter failedMsgs; @Override public void onSuccess(@Nullable T result) { - successMsgs.incrementAndGet(); + successMsgs.increment(); } @Override public void onFailure(Throwable t) { if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { - timeoutMsgs.incrementAndGet(); + timeoutMsgs.increment(); } else { - failedMsgs.incrementAndGet(); + failedMsgs.increment(); } } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java index 4049040c94..b6ddf16e66 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java @@ -26,6 +26,7 @@ import org.thingsboard.script.api.ScriptType; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; @@ -117,4 +118,8 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic .hash().toString(); } + @Override + protected StatsType getStatsType() { + return StatsType.JS_INVOKE; + } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index 38fed7a2aa..25a7ede547 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -44,6 +44,7 @@ import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; @@ -139,6 +140,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize); + parserConfig.registerDataType("TbelCfCtx", TbelCfCtx.class, TbelCfCtx::memorySize); TbUtils.register(parserConfig); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); @@ -262,4 +264,9 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem protected long getMaxEvalRequestsTimeout() { return maxInvokeRequestsTimeout * 2; } + + @Override + protected StatsType getStatsType() { + return StatsType.TBEL_INVOKE; + } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 20a753da14..95fff48883 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -257,6 +257,8 @@ public class TbUtils { float.class, int.class))); parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", double.class))); + parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN", + double.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", ExecutionContext.class, String.class))); parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray", @@ -1161,6 +1163,10 @@ public class TbUtils { return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); } + public static boolean isNaN(double value) { + return Double.isNaN(value); + } + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json) { return toFlatMap(ctx, json, new ArrayList<>(), true); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfigurationTest.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java similarity index 60% rename from rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfigurationTest.java rename to common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java index 5c4d341fda..ce42e2cf3b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfigurationTest.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfCtx.java @@ -13,17 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.telemetry; +package org.thingsboard.script.api.tbel; -import org.junit.jupiter.api.Test; +import lombok.Getter; -import static org.assertj.core.api.Assertions.assertThat; +import java.util.Collections; +import java.util.Map; -class TbMsgAttributesNodeConfigurationTest { +public class TbelCfCtx implements TbelCfObject { - @Test - void testDefaultConfig_givenUpdateAttributesOnlyOnValueChange_thenTrue_sinceVersion1() { - assertThat(new TbMsgAttributesNodeConfiguration().defaultConfiguration().isUpdateAttributesOnlyOnValueChange()).isTrue(); + @Getter + private final Map args; + + public TbelCfCtx(Map args) { + this.args = Collections.unmodifiableMap(args); } + @Override + public long memorySize() { + return OBJ_SIZE; + } } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java index 3af6198a22..cf575bd9d0 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfObject.java @@ -17,6 +17,8 @@ package org.thingsboard.script.api.tbel; public interface TbelCfObject { + long OBJ_SIZE = 32L; // Approximate calculation; + long memorySize(); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java index 193b6ea1ae..84227a8d80 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfSingleValueArg.java @@ -22,8 +22,6 @@ import lombok.Data; @Data public class TbelCfSingleValueArg implements TbelCfArg { - public static final long OBJ_SIZE = 32L; // Approximate calculation; - private final long ts; private final Object value; diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 69987d3132..e3860ed89a 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1138,12 +1138,18 @@ public class TbUtilsTest { } @Test - void toInt() { + public void toInt() { Assertions.assertEquals(1729, TbUtils.toInt(doubleVal)); Assertions.assertEquals(13, TbUtils.toInt(12.8)); Assertions.assertEquals(28, TbUtils.toInt(28.0)); } + @Test + public void isNaN() { + Assertions.assertFalse(TbUtils.isNaN(doubleVal)); + Assertions.assertTrue(TbUtils.isNaN(Double.NaN)); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java index ef9041fab5..126d4dac95 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultCounter.java @@ -41,6 +41,10 @@ public class DefaultCounter { return aiCounter.get(); } + public int getAndClear() { + return aiCounter.getAndSet(0); + } + public void add(int delta){ aiCounter.addAndGet(delta); micrometerCounter.increment(delta); diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java index de64d4fe54..90bf1bec32 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsType.java @@ -20,6 +20,7 @@ public enum StatsType { CORE("core"), TRANSPORT("transport"), JS_INVOKE("jsInvoke"), + TBEL_INVOKE("tbelInvoke"), RATE_EXECUTOR("rateExecutor"), HOUSEKEEPER("housekeeper"), EDGE("edge"), diff --git a/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java index 5c24c45c16..258bac8dbd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java @@ -29,6 +29,7 @@ import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.dao.model.sql.AuditLogEntity; +import org.thingsboard.server.dao.model.sql.CalculatedFieldDebugEventEntity; import org.thingsboard.server.dao.model.sql.ErrorEventEntity; import org.thingsboard.server.dao.model.sql.LifecycleEventEntity; import org.thingsboard.server.dao.model.sql.RuleChainDebugEventEntity; @@ -68,7 +69,7 @@ public class DedicatedEventsJpaDaoConfig { EntityManagerFactoryBuilder builder) { return builder .dataSource(eventsDataSource) - .packages(LifecycleEventEntity.class, StatisticsEventEntity.class, ErrorEventEntity.class, RuleNodeDebugEventEntity.class, RuleChainDebugEventEntity.class, AuditLogEntity.class) + .packages(LifecycleEventEntity.class, StatisticsEventEntity.class, ErrorEventEntity.class, RuleNodeDebugEventEntity.class, RuleChainDebugEventEntity.class, AuditLogEntity.class, CalculatedFieldDebugEventEntity.class) .persistenceUnit(EVENTS_PERSISTENCE_UNIT) .build(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index e91c1939f6..3be7f5b91c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -379,6 +379,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); if (defaultConfig != null) { updated |= convertToImageUrlsByMapping(tenantId, WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig, imagesLinks); + updated |= convertToImageUrls(tenantId, prefix, defaultConfig, imagesLinks); widgetTypeDetails.setDefaultConfig(defaultConfig); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index 71d2f04b81..6094e9b171 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -147,7 +147,8 @@ public interface EntityViewRepository extends JpaRepository :id ORDER BY e.id") + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.EntityViewFields(e.id, e.createdTime, e.tenantId, " + + "e.customerId, e.name, e.type, e.additionalInfo, e.version) " + + "FROM EntityViewEntity e WHERE e.id > :id ORDER BY e.id") List findNextBatch(@Param("id") UUID id, Limit limit); } diff --git a/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql b/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql index b0c69ef6f3..b756d282f8 100644 --- a/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql +++ b/dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql @@ -36,3 +36,5 @@ CREATE INDEX IF NOT EXISTS idx_lc_event_main CREATE INDEX IF NOT EXISTS idx_error_event_main ON error_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); +CREATE INDEX IF NOT EXISTS idx_cf_debug_event_main + ON cf_debug_event (tenant_id ASC, entity_id ASC, ts DESC NULLS LAST) WITH (FILLFACTOR=95); diff --git a/docker/docker-compose.edqs.volumes.yml b/docker/docker-compose.edqs.volumes.yml index 9d2ce946c8..89b4b3a59c 100644 --- a/docker/docker-compose.edqs.volumes.yml +++ b/docker/docker-compose.edqs.volumes.yml @@ -19,10 +19,10 @@ version: '3.0' services: tb-edqs-1: volumes: - - tb-edqs-log-volume:/var/log/edqs + - tb-edqs-log-volume:/var/log/tb-edqs tb-edqs-2: volumes: - - tb-edqs-log-volume:/var/log/edqs + - tb-edqs-log-volume:/var/log/tb-edqs volumes: tb-edqs-log-volume: diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index 67e9c987e3..6dd9606ee6 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -33,10 +33,10 @@ services: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: - - edqs.env + - tb-edqs.env volumes: - - ./edqs/conf:/usr/share/edqs/conf - - ./edqs/log:/var/log/edqs + - ./tb-edqs/conf:/usr/share/tb-edqs/conf + - ./tb-edqs/log:/var/log/tb-edqs ports: - "8080" depends_on: @@ -46,10 +46,10 @@ services: restart: always image: "${DOCKER_REPO}/${EDQS_DOCKER_NAME}:${TB_VERSION}" env_file: - - edqs.env + - tb-edqs.env volumes: - - ./edqs/conf:/usr/share/edqs/conf - - ./edqs/log:/var/log/edqs + - ./tb-edqs/conf:/usr/share/tb-edqs/conf + - ./tb-edqs/log:/var/log/tb-edqs ports: - "8080" depends_on: diff --git a/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json b/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json index e04a5c2d84..15ff06ab8c 100644 --- a/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json +++ b/docker/monitoring/grafana/provisioning/dashboards/core_and_js_metrics.json @@ -223,8 +223,8 @@ "fill": 1, "fillGradient": 0, "gridPos": { - "h": 12, - "w": 24, + "h": 10, + "w": 12, "x": 0, "y": 10 }, @@ -303,6 +303,100 @@ "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 10 + }, + "hiddenSeries": false, + "id": 19, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "sum by(statsName) (increase(tbelInvoke_total[1m]))", + "interval": "", + "legendFormat": "{{statsName}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "TbelInvoke Stats", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "schemaVersion": 27, diff --git a/docker/edqs.env b/docker/tb-edqs.env similarity index 100% rename from docker/edqs.env rename to docker/tb-edqs.env diff --git a/docker/edqs/conf/logback.xml b/docker/tb-edqs/conf/logback.xml similarity index 91% rename from docker/edqs/conf/logback.xml rename to docker/tb-edqs/conf/logback.xml index 40481a8c35..14b5b0f04b 100644 --- a/docker/edqs/conf/logback.xml +++ b/docker/tb-edqs/conf/logback.xml @@ -21,10 +21,10 @@ - /var/log/edqs/${TB_SERVICE_ID}/tb-edqs.log + /var/log/tb-edqs/${TB_SERVICE_ID}/tb-edqs.log - /var/log/edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log + /var/log/tb-edqs/tb-edqs.%d{yyyy-MM-dd}.%i.log 100MB 30 3GB diff --git a/docker/edqs/conf/edqs.conf b/docker/tb-edqs/conf/tb-edqs.conf similarity index 88% rename from docker/edqs/conf/edqs.conf rename to docker/tb-edqs/conf/tb-edqs.conf index 8c6b5d1826..a5a4e6a10c 100644 --- a/docker/edqs/conf/edqs.conf +++ b/docker/tb-edqs/conf/tb-edqs.conf @@ -14,9 +14,9 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/edqs/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-edqs/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" export LOG_FILENAME=tb-edqs.out -export LOADER_PATH=/usr/share/edqs/conf +export LOADER_PATH=/usr/share/tb-edqs/conf diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index 1d7f111e9a..f7d0eda841 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -70,8 +70,6 @@ queue: stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" - # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" kafka: # Kafka Bootstrap nodes in "host:port" format diff --git a/monitoring/src/main/resources/root_rule_chain.json b/monitoring/src/main/resources/root_rule_chain.json index 46bdc72d9f..a1c12c8e9d 100644 --- a/monitoring/src/main/resources/root_rule_chain.json +++ b/monitoring/src/main/resources/root_rule_chain.json @@ -39,8 +39,11 @@ "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Attributes", "singletonMode": false, - "configurationVersion": 1, + "configurationVersion": 3, "configuration": { + "processingSettings": { + "type": "ON_EVERY_MESSAGE" + }, "scope": "CLIENT_SCOPE", "notifyDevice": false, "sendAttributesUpdatedNotification": false, diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java index 2ab6923899..374fcc45f6 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesDeleteRequest.java @@ -21,6 +21,7 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; +import org.thingsboard.common.util.NoOpFutureCallback; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -30,6 +31,8 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import java.util.List; import java.util.UUID; +import static java.util.Objects.requireNonNullElse; + @Getter @ToString @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -61,8 +64,7 @@ public class AttributesDeleteRequest implements CalculatedFieldSystemAwareReques private TbMsgType tbMsgType; private FutureCallback callback; - Builder() { - } + Builder() {} public Builder tenantId(TenantId tenantId) { this.tenantId = tenantId; @@ -134,7 +136,9 @@ public class AttributesDeleteRequest implements CalculatedFieldSystemAwareReques } public AttributesDeleteRequest build() { - return new AttributesDeleteRequest(tenantId, entityId, scope, keys, notifyDevice, previousCalculatedFieldIds, tbMsgId, tbMsgType, callback); + return new AttributesDeleteRequest( + tenantId, entityId, scope, keys, notifyDevice, previousCalculatedFieldIds, tbMsgId, tbMsgType, requireNonNullElse(callback, NoOpFutureCallback.instance()) + ); } } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesSaveRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesSaveRequest.java index 7c8e9d317e..c3095836bd 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesSaveRequest.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/AttributesSaveRequest.java @@ -21,6 +21,7 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.ToString; +import org.thingsboard.common.util.NoOpFutureCallback; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -33,6 +34,8 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import java.util.List; import java.util.UUID; +import static java.util.Objects.requireNonNullElse; + @Getter @ToString @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -43,11 +46,20 @@ public class AttributesSaveRequest implements CalculatedFieldSystemAwareRequest private final AttributeScope scope; private final List entries; private final boolean notifyDevice; + private final Strategy strategy; private final List previousCalculatedFieldIds; private final UUID tbMsgId; private final TbMsgType tbMsgType; private final FutureCallback callback; + public record Strategy(boolean saveAttributes, boolean sendWsUpdate, boolean processCalculatedFields) { + + public static final Strategy PROCESS_ALL = new Strategy(true, true, true); + public static final Strategy WS_ONLY = new Strategy(false, true, false); + public static final Strategy SKIP_ALL = new Strategy(false, false, false); + + } + public static Builder builder() { return new Builder(); } @@ -59,6 +71,7 @@ public class AttributesSaveRequest implements CalculatedFieldSystemAwareRequest private AttributeScope scope; private List entries; private boolean notifyDevice = true; + private Strategy strategy; private List previousCalculatedFieldIds; private UUID tbMsgId; private TbMsgType tbMsgType; @@ -109,6 +122,11 @@ public class AttributesSaveRequest implements CalculatedFieldSystemAwareRequest return this; } + public Builder strategy(Strategy strategy) { + this.strategy = strategy; + return this; + } + public Builder previousCalculatedFieldIds(List previousCalculatedFieldIds) { this.previousCalculatedFieldIds = previousCalculatedFieldIds; return this; @@ -144,7 +162,10 @@ public class AttributesSaveRequest implements CalculatedFieldSystemAwareRequest } public AttributesSaveRequest build() { - return new AttributesSaveRequest(tenantId, entityId, scope, entries, notifyDevice, previousCalculatedFieldIds, tbMsgId, tbMsgType, callback); + return new AttributesSaveRequest( + tenantId, entityId, scope, entries, notifyDevice, requireNonNullElse(strategy, Strategy.PROCESS_ALL), + previousCalculatedFieldIds, tbMsgId, tbMsgType, requireNonNullElse(callback, NoOpFutureCallback.instance()) + ); } } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index e473df4c54..b66c9e13d5 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -375,12 +375,6 @@ public interface TbContext { ScriptEngine createScriptEngine(ScriptLanguage scriptLang, String script, String... argNames); - void logJsEvalRequest(); - - void logJsEvalResponse(); - - void logJsEvalFailure(); - String getServiceId(); EventLoopGroup getSharedEventLoop(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java index 01af278efb..c402a0c984 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequest.java @@ -70,7 +70,7 @@ public class TimeseriesSaveRequest implements CalculatedFieldSystemAwareRequest private EntityId entityId; private List entries; private long ttl; - private Strategy strategy = Strategy.PROCESS_ALL; + private Strategy strategy; private List previousCalculatedFieldIds; private UUID tbMsgId; private TbMsgType tbMsgType; @@ -152,7 +152,7 @@ public class TimeseriesSaveRequest implements CalculatedFieldSystemAwareRequest public TimeseriesSaveRequest build() { return new TimeseriesSaveRequest( - tenantId, customerId, entityId, entries, ttl, strategy, + tenantId, customerId, entityId, entries, ttl, requireNonNullElse(strategy, Strategy.PROCESS_ALL), previousCalculatedFieldIds, tbMsgId, tbMsgType, requireNonNullElse(callback, NoOpFutureCallback.instance()) ); } diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/JsInvokeStats.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesDeleteRequestTest.java similarity index 50% rename from common/actor/src/main/java/org/thingsboard/server/actors/JsInvokeStats.java rename to rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesDeleteRequestTest.java index bf81e04b71..9b4a825a66 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/JsInvokeStats.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesDeleteRequestTest.java @@ -13,32 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors; +package org.thingsboard.rule.engine.api; -public interface JsInvokeStats { - default void incrementRequests() { - incrementRequests(1); - } +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.NoOpFutureCallback; - void incrementRequests(int amount); +import static org.assertj.core.api.Assertions.assertThat; - default void incrementResponses() { - incrementResponses(1); - } +class AttributesDeleteRequestTest { - void incrementResponses(int amount); + @Test + void testDefaultCallbackIsNoOp() { + var request = AttributesDeleteRequest.builder().build(); - default void incrementFailures() { - incrementFailures(1); + assertThat(request.getCallback()).isEqualTo(NoOpFutureCallback.instance()); } - void incrementFailures(int amount); - - int getRequests(); + @Test + void testNullCallbackIsNoOp() { + var request = AttributesDeleteRequest.builder().callback(null).build(); - int getResponses(); - - int getFailures(); + assertThat(request.getCallback()).isEqualTo(NoOpFutureCallback.instance()); + } - void reset(); } diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesSaveRequestTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesSaveRequestTest.java new file mode 100644 index 0000000000..d632edfbf9 --- /dev/null +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/AttributesSaveRequestTest.java @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2025 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. + */ +package org.thingsboard.rule.engine.api; + +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.NoOpFutureCallback; + +import static org.assertj.core.api.Assertions.assertThat; + +class AttributesSaveRequestTest { + + @Test + void testDefaultProcessingStrategyIsProcessAll() { + var request = AttributesSaveRequest.builder().build(); + + assertThat(request.getStrategy()).isEqualTo(AttributesSaveRequest.Strategy.PROCESS_ALL); + } + + @Test + void testNullProcessingStrategyIsProcessAll() { + var request = AttributesSaveRequest.builder().strategy(null).build(); + + assertThat(request.getStrategy()).isEqualTo(AttributesSaveRequest.Strategy.PROCESS_ALL); + } + + @Test + void testProcessAllStrategy() { + assertThat(AttributesSaveRequest.Strategy.PROCESS_ALL).isEqualTo(new AttributesSaveRequest.Strategy(true, true, true)); + } + + @Test + void testWsOnlyStrategy() { + assertThat(AttributesSaveRequest.Strategy.WS_ONLY).isEqualTo(new AttributesSaveRequest.Strategy(false, true, false)); + } + + @Test + void testSkipAllStrategy() { + assertThat(AttributesSaveRequest.Strategy.SKIP_ALL).isEqualTo(new AttributesSaveRequest.Strategy(false, false, false)); + } + + @Test + void testDefaultCallbackIsNoOp() { + var request = AttributesSaveRequest.builder().build(); + + assertThat(request.getCallback()).isEqualTo(NoOpFutureCallback.instance()); + } + + @Test + void testNullCallbackIsNoOp() { + var request = AttributesSaveRequest.builder().callback(null).build(); + + assertThat(request.getCallback()).isEqualTo(NoOpFutureCallback.instance()); + } + +} diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequestTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequestTest.java index a67a11ad3d..c0eb04152a 100644 --- a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequestTest.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/TimeseriesSaveRequestTest.java @@ -23,14 +23,21 @@ import static org.assertj.core.api.Assertions.assertThat; class TimeseriesSaveRequestTest { @Test - void testDefaultSaveStrategyIsProcessAll() { + void testDefaultProcessingStrategyIsProcessAll() { var request = TimeseriesSaveRequest.builder().build(); assertThat(request.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.PROCESS_ALL); } @Test - void testSaveAllStrategy() { + void testNullProcessingStrategyIsProcessAll() { + var request = TimeseriesSaveRequest.builder().strategy(null).build(); + + assertThat(request.getStrategy()).isEqualTo(TimeseriesSaveRequest.Strategy.PROCESS_ALL); + } + + @Test + void testProcessAllStrategy() { assertThat(TimeseriesSaveRequest.Strategy.PROCESS_ALL).isEqualTo(new TimeseriesSaveRequest.Strategy(true, true, true, true)); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index c78bc77d72..6a806e7bc1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -69,10 +69,8 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { - ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); return Futures.transform(asyncDetails, details -> { - ctx.logJsEvalResponse(); AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); if (result.isSuccessful()) { return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index 837c5ba692..78411d7cec 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -118,15 +118,11 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncDetails; boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails(); if (buildDetails) { - ctx.logJsEvalRequest(); asyncDetails = buildAlarmDetails(msg, null); } else { asyncDetails = Futures.immediateFuture(null); } ListenableFuture asyncAlarm = Futures.transform(asyncDetails, details -> { - if (buildDetails) { - ctx.logJsEvalResponse(); - } Alarm newAlarm; if (msgAlarm != null) { newAlarm = msgAlarm; @@ -147,15 +143,11 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncDetails; boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails(); if (buildDetails) { - ctx.logJsEvalRequest(); asyncDetails = buildAlarmDetails(msg, existingAlarm.getDetails()); } else { asyncDetails = Futures.immediateFuture(null); } ListenableFuture asyncUpdated = Futures.transform(asyncDetails, details -> { - if (buildDetails) { - ctx.logJsEvalResponse(); - } if (msgAlarm != null) { existingAlarm.setSeverity(msgAlarm.getSeverity()); existingAlarm.setPropagate(msgAlarm.isPropagate()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index 7e241832cf..47ecac154f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -75,18 +75,15 @@ public class TbLogNode implements TbNode { return; } - ctx.logJsEvalRequest(); Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback() { @Override public void onSuccess(@Nullable String result) { - ctx.logJsEvalResponse(); log.info(result); ctx.tellSuccess(msg); } @Override public void onFailure(Throwable t) { - ctx.logJsEvalResponse(); ctx.tellFailure(msg, t); } }, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 2848e7c45a..90ee0c9048 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -160,10 +160,8 @@ public class TbMsgGeneratorNode implements TbNode { prevMsg = ctx.newMsg(queueName, TbMsg.EMPTY_STRING, originatorId, msg.getCustomerId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); } if (initialized.get()) { - ctx.logJsEvalRequest(); return Futures.transformAsync(scriptEngine.executeGenerateAsync(prevMsg), generated -> { log.trace("generate process response, generated {}, config {}", generated, config); - ctx.logJsEvalResponse(); prevMsg = ctx.newMsg(queueName, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); return Futures.immediateFuture(prevMsg); }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 50f168a5af..e36494a2b6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -60,15 +60,12 @@ public class TbJsFilterNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.logJsEvalRequest(); withCallback(scriptEngine.executeFilterAsync(msg), filterResult -> { - ctx.logJsEvalResponse(); ctx.tellNext(msg, filterResult ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); }, t -> { ctx.tellFailure(msg, t); - ctx.logJsEvalFailure(); }, ctx.getDbCallbackExecutor()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index 3c27740c84..e039eaeb14 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -61,17 +61,14 @@ public class TbJsSwitchNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.logJsEvalRequest(); Futures.addCallback(scriptEngine.executeSwitchAsync(msg), new FutureCallback<>() { @Override public void onSuccess(@Nullable Set result) { - ctx.logJsEvalResponse(); processSwitch(ctx, msg, result); } @Override public void onFailure(Throwable t) { - ctx.logJsEvalFailure(); ctx.tellFailure(msg, t); } }, MoreExecutors.directExecutor()); //usually runs in a callbackExecutor diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index 6008305570..d544d0647d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.kafka; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; import org.apache.kafka.clients.producer.KafkaProducer; @@ -26,6 +28,7 @@ import org.apache.kafka.common.config.SslConfigs; import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.header.internals.RecordHeader; import org.apache.kafka.common.header.internals.RecordHeaders; +import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.util.ReflectionUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -35,6 +38,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -48,6 +52,7 @@ import java.util.Properties; type = ComponentType.EXTERNAL, name = "kafka", configClazz = TbKafkaNodeConfiguration.class, + version = 1, nodeDescription = "Publish messages to Kafka server", nodeDetails = "Will send record via Kafka producer to Kafka server. " + "Outbound message will contain response fields (offset, partition, topic)" + @@ -83,8 +88,8 @@ public class TbKafkaNode extends TbAbstractExternalNode { Properties properties = new Properties(); properties.put(ProducerConfig.CLIENT_ID_CONFIG, "producer-tb-kafka-node-" + ctx.getSelfId().getId().toString() + "-" + ctx.getServiceId()); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); - properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer()); - properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer()); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); properties.put(ProducerConfig.ACKS_CONFIG, config.getAcks()); properties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries()); properties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize()); @@ -200,4 +205,22 @@ public class TbKafkaNode extends TbAbstractExternalNode { .build(); } + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0 -> { + if (oldConfiguration.has("keySerializer") || oldConfiguration.has("valueSerializer")) { + ObjectNode objectConfiguration = (ObjectNode) oldConfiguration; + objectConfiguration.remove("keySerializer"); + objectConfiguration.remove("valueSerializer"); + hasChanges = true; + } + } + default -> { + } + } + return new TbPair<>(hasChanges, oldConfiguration); + } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java index 867dd2e54e..9a31e58ce7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNodeConfiguration.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.kafka; import lombok.Data; -import org.apache.kafka.common.serialization.StringSerializer; import org.thingsboard.rule.engine.api.NodeConfiguration; import java.util.Collections; @@ -33,8 +32,6 @@ public class TbKafkaNodeConfiguration implements NodeConfiguration otherProperties; private boolean addMetadataKeyValuesAsKafkaHeaders; @@ -50,8 +47,6 @@ public class TbKafkaNodeConfiguration implements NodeConfigurationSuccess chain, otherwise, Failure chain is used. " + - "Additionally if checkbox Send attributes updated notification is set to true, rule node will put the \"Attributes Updated\" " + - "event for SHARED_SCOPE and SERVER_SCOPE attributes updates to the corresponding rule engine queue." + - "Performance checkbox 'Save attributes only if the value changes' will skip attributes overwrites for values with no changes (avoid concurrent writes because this check is not transactional; will not update 'Last updated time' for skipped attributes).", + version = 3, + nodeDescription = """ + Saves attribute data with a configurable scope and according to configured processing strategies. + """, + nodeDetails = """ + Node performs three actions: +
    +
  • Attributes: save attribute data to a database.
  • +
  • WebSockets: notify WebSockets subscriptions about attribute data updates.
  • +
  • Calculated fields: notify calculated fields about attribute data updates.
  • +
+ + For each action, three processing strategies are available: +
    +
  • On every message: perform the action for every message.
  • +
  • Deduplicate: perform the action only for the first message from a particular originator within a configurable interval.
  • +
  • Skip: never perform the action.
  • +
+ + Processing strategies are configured using processing settings, which support two modes: +
    +
  • Basic +
      +
    • On every message: applies the "On every message" strategy to all actions.
    • +
    • Deduplicate: applies the "Deduplicate" strategy (with a specified interval) to all actions.
    • +
    • WebSockets only: for all actions except WebSocket notifications, the "Skip" strategy is applied, while WebSocket notifications use the "On every message" strategy.
    • +
    +
  • +
  • Advanced: configure each action’s strategy independently.
  • +
+ + The node supports three attribute scopes: Client attributes, Shared attributes, and Server attributes. + You can set the default scope in the node configuration, or override it by specifying a valid scope property in the message metadata. +

+ Additionally: +
    +
  • If Save attributes only if the value changes is enabled, the rule node compares the received attribute value with the current stored value and skips the save operation if they match.
  • +
  • If Send attributes updated notification is enabled, the rule node will put the Attributes Updated event for SHARED_SCOPE and SERVER_SCOPE attribute updates to the queue named Main.
  • +
  • If Force notification to the device is enabled, then rule node will always notify device about SHARED_SCOPE attribute updates, regardless of the value of notifyDevice metadata property.
  • +
+ + This node expects messages of type POST_ATTRIBUTES_REQUEST. +

+ Output connections: Success, Failure. + """, configDirective = "tbActionNodeAttributesConfig", icon = "file_upload" ) @@ -73,9 +118,12 @@ public class TbMsgAttributesNode implements TbNode { private TbMsgAttributesNodeConfiguration config; + private AttributesProcessingSettings processingSettings; + @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbMsgAttributesNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbMsgAttributesNodeConfiguration.class); + processingSettings = config.getProcessingSettings(); } @Override @@ -90,11 +138,20 @@ public class TbMsgAttributesNode implements TbNode { ctx.tellSuccess(msg); return; } + + AttributesSaveRequest.Strategy strategy = determineSaveStrategy(msg.getMetaDataTs(), msg.getOriginator().getId()); + + // short-circuit + if (!strategy.saveAttributes() && !strategy.sendWsUpdate() && !strategy.processCalculatedFields()) { + ctx.tellSuccess(msg); + return; + } + AttributeScope scope = getScope(msg.getMetaData().getValue(SCOPE)); boolean sendAttributesUpdateNotification = checkSendNotification(scope); if (!config.isUpdateAttributesOnlyOnValueChange()) { - saveAttr(newAttributes, ctx, msg, scope, sendAttributesUpdateNotification); + saveAttr(newAttributes, ctx, msg, scope, sendAttributesUpdateNotification, strategy); return; } @@ -104,13 +161,42 @@ public class TbMsgAttributesNode implements TbNode { DonAsynchron.withCallback(findFuture, currentAttributes -> { List attributesChanged = filterChangedAttr(currentAttributes, newAttributes); - saveAttr(attributesChanged, ctx, msg, scope, sendAttributesUpdateNotification); + saveAttr(attributesChanged, ctx, msg, scope, sendAttributesUpdateNotification, strategy); }, throwable -> ctx.tellFailure(msg, throwable), MoreExecutors.directExecutor()); } - void saveAttr(List attributes, TbContext ctx, TbMsg msg, AttributeScope scope, boolean sendAttributesUpdateNotification) { + private AttributesSaveRequest.Strategy determineSaveStrategy(long ts, UUID originatorUuid) { + if (processingSettings instanceof OnEveryMessage) { + return AttributesSaveRequest.Strategy.PROCESS_ALL; + } + if (processingSettings instanceof WebSocketsOnly) { + return AttributesSaveRequest.Strategy.WS_ONLY; + } + if (processingSettings instanceof Deduplicate deduplicate) { + boolean isFirstMsgInInterval = deduplicate.getProcessingStrategy().shouldProcess(ts, originatorUuid); + return isFirstMsgInInterval ? AttributesSaveRequest.Strategy.PROCESS_ALL : AttributesSaveRequest.Strategy.SKIP_ALL; + } + if (processingSettings instanceof Advanced advanced) { + return new AttributesSaveRequest.Strategy( + advanced.attributes().shouldProcess(ts, originatorUuid), + advanced.webSockets().shouldProcess(ts, originatorUuid), + advanced.calculatedFields().shouldProcess(ts, originatorUuid) + ); + } + // should not happen + throw new IllegalArgumentException("Unknown processing settings type: " + processingSettings.getClass().getSimpleName()); + } + + private void saveAttr( + List attributes, + TbContext ctx, + TbMsg msg, + AttributeScope scope, + boolean sendAttributesUpdateNotification, + AttributesSaveRequest.Strategy strategy + ) { if (attributes.isEmpty()) { ctx.tellSuccess(msg); return; @@ -124,6 +210,7 @@ public class TbMsgAttributesNode implements TbNode { .scope(scope) .entries(attributes) .notifyDevice(config.isNotifyDevice() || checkNotifyDeviceMdValue(msg.getMetaData().getValue(NOTIFY_DEVICE_METADATA_KEY))) + .strategy(strategy) .previousCalculatedFieldIds(msg.getPreviousCalculatedFieldIds()) .tbMsgId(msg.getId()) .tbMsgType(msg.getInternalType()) @@ -131,7 +218,7 @@ public class TbMsgAttributesNode implements TbNode { .build()); } - List filterChangedAttr(List currentAttributes, List newAttributes) { + private List filterChangedAttr(List currentAttributes, List newAttributes) { if (currentAttributes == null || currentAttributes.isEmpty()) { return newAttributes; } @@ -181,6 +268,9 @@ public class TbMsgAttributesNode implements TbNode { hasChanges = fixEscapedBooleanConfigParameter(oldConfiguration, SEND_ATTRIBUTES_UPDATED_NOTIFICATION_KEY, hasChanges, false); // update updateAttributesOnlyOnValueChange. hasChanges = fixEscapedBooleanConfigParameter(oldConfiguration, UPDATE_ATTRIBUTES_ONLY_ON_VALUE_CHANGE_KEY, hasChanges, true); + case 2: + hasChanges = true; + ((ObjectNode) oldConfiguration).set("processingSettings", JacksonUtil.valueToTree(new OnEveryMessage())); break; default: break; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java index 161aa64d5f..2687125a0c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeConfiguration.java @@ -15,13 +15,20 @@ */ package org.thingsboard.rule.engine.telemetry; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings; import org.thingsboard.server.common.data.DataConstants; +import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.OnEveryMessage; + @Data public class TbMsgAttributesNodeConfiguration implements NodeConfiguration { + @NotNull + private AttributesProcessingSettings processingSettings; + private String scope; private boolean notifyDevice; @@ -31,6 +38,7 @@ public class TbMsgAttributesNodeConfiguration implements NodeConfigurationactions: + Node performs four actions: