38 changed files with 1474 additions and 175 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,25 @@ |
|||
{ |
|||
"widgetsBundle": { |
|||
"alias": "control_widgets", |
|||
"title": "Control widgets", |
|||
"image": null |
|||
}, |
|||
"widgetTypes": [ |
|||
{ |
|||
"alias": "device_terminal", |
|||
"name": "Device terminal", |
|||
"descriptor": { |
|||
"type": "rpc", |
|||
"sizeX": 9.5, |
|||
"sizeY": 5.5, |
|||
"resources": [], |
|||
"templateHtml": "<div style=\"height: 100%;\" id=\"device-terminal\"></div>", |
|||
"templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", |
|||
"controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC commands terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = angular.copy(command).trim();\n if (localCommand == 'help') {\n printUsage(this);\n } else {\n var cmdObj = $.terminal.parse_command(localCommand);\n if (cmdObj.args.length > 1) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (cmdObj.args.length && cmdObj.args[0]) {\n try {\n params = JSON.parse(cmdObj.args[0]);\n } catch (e) {\n params = cmdObj.args[0];\n }\n }\n performRpc(this, cmdObj.name, params);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\nfunction performRpc(terminal, method, params) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout).then(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n \nself.onDestroy = function() {\n}\n", |
|||
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\"\n ]\n}", |
|||
"dataKeySettingsSchema": "{}\n", |
|||
"defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"Device terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,138 @@ |
|||
/* |
|||
* Copyright © 2016-2017 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
export function utf8Encode(str) { |
|||
var result; |
|||
|
|||
if (angular.isUndefined(Uint8Array)) { // eslint-disable-line no-undef
|
|||
result = utf8ToBytes(str); |
|||
} else { |
|||
result = new Uint8Array(utf8ToBytes(str)); // eslint-disable-line no-undef
|
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
export function utf8Decode(bytes) { |
|||
return utf8Slice(bytes, 0, bytes.length); |
|||
} |
|||
|
|||
function utf8Slice (buf, start, end) { |
|||
var res = '' |
|||
var tmp = '' |
|||
end = Math.min(buf.length, end || Infinity) |
|||
start = start || 0; |
|||
|
|||
for (var i = start; i < end; i++) { |
|||
if (buf[i] <= 0x7F) { |
|||
res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]) |
|||
tmp = '' |
|||
} else { |
|||
tmp += '%' + buf[i].toString(16) |
|||
} |
|||
} |
|||
|
|||
return res + decodeUtf8Char(tmp) |
|||
} |
|||
|
|||
function decodeUtf8Char (str) { |
|||
try { |
|||
return decodeURIComponent(str) |
|||
} catch (err) { |
|||
return String.fromCharCode(0xFFFD) // UTF 8 invalid char
|
|||
} |
|||
} |
|||
|
|||
function utf8ToBytes (string, units) { |
|||
units = units || Infinity |
|||
var codePoint |
|||
var length = string.length |
|||
var leadSurrogate = null |
|||
var bytes = [] |
|||
var i = 0 |
|||
|
|||
for (; i < length; i++) { |
|||
codePoint = string.charCodeAt(i) |
|||
|
|||
// is surrogate component
|
|||
if (codePoint > 0xD7FF && codePoint < 0xE000) { |
|||
// last char was a lead
|
|||
if (leadSurrogate) { |
|||
// 2 leads in a row
|
|||
if (codePoint < 0xDC00) { |
|||
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) |
|||
leadSurrogate = codePoint |
|||
continue |
|||
} else { |
|||
// valid surrogate pair
|
|||
codePoint = leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00 | 0x10000 |
|||
leadSurrogate = null |
|||
} |
|||
} else { |
|||
// no lead yet
|
|||
|
|||
if (codePoint > 0xDBFF) { |
|||
// unexpected trail
|
|||
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) |
|||
continue |
|||
} else if (i + 1 === length) { |
|||
// unpaired lead
|
|||
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) |
|||
continue |
|||
} else { |
|||
// valid lead
|
|||
leadSurrogate = codePoint |
|||
continue |
|||
} |
|||
} |
|||
} else if (leadSurrogate) { |
|||
// valid bmp char, but last char was a lead
|
|||
if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) |
|||
leadSurrogate = null |
|||
} |
|||
|
|||
// encode utf8
|
|||
if (codePoint < 0x80) { |
|||
if ((units -= 1) < 0) break |
|||
bytes.push(codePoint) |
|||
} else if (codePoint < 0x800) { |
|||
if ((units -= 2) < 0) break |
|||
bytes.push( |
|||
codePoint >> 0x6 | 0xC0, |
|||
codePoint & 0x3F | 0x80 |
|||
) |
|||
} else if (codePoint < 0x10000) { |
|||
if ((units -= 3) < 0) break |
|||
bytes.push( |
|||
codePoint >> 0xC | 0xE0, |
|||
codePoint >> 0x6 & 0x3F | 0x80, |
|||
codePoint & 0x3F | 0x80 |
|||
) |
|||
} else if (codePoint < 0x200000) { |
|||
if ((units -= 4) < 0) break |
|||
bytes.push( |
|||
codePoint >> 0x12 | 0xF0, |
|||
codePoint >> 0xC & 0x3F | 0x80, |
|||
codePoint >> 0x6 & 0x3F | 0x80, |
|||
codePoint & 0x3F | 0x80 |
|||
) |
|||
} else { |
|||
throw new Error('Invalid code point') |
|||
} |
|||
} |
|||
|
|||
return bytes |
|||
} |
|||
@ -0,0 +1,680 @@ |
|||
/* |
|||
* Copyright © 2016-2017 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
import tinycolor from 'tinycolor2'; |
|||
|
|||
import TbGoogleMap from './google-map'; |
|||
import TbOpenStreetMap from './openstreet-map'; |
|||
|
|||
import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils'; |
|||
|
|||
export default class TbMapWidgetV2 { |
|||
constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) { |
|||
var tbMap = this; |
|||
this.ctx = ctx; |
|||
if (!$element) { |
|||
$element = ctx.$container; |
|||
} |
|||
this.utils = ctx.$scope.$injector.get('utils'); |
|||
this.drawRoutes = drawRoutes; |
|||
this.markers = []; |
|||
if (this.drawRoutes) { |
|||
this.polylines = []; |
|||
} |
|||
|
|||
this.locationSettings = {}; |
|||
|
|||
var settings = ctx.settings; |
|||
|
|||
this.callbacks = {}; |
|||
this.callbacks.onLocationClick = function(){}; |
|||
|
|||
if (settings.defaultZoomLevel) { |
|||
if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) { |
|||
this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel); |
|||
} |
|||
} |
|||
|
|||
this.dontFitMapBounds = settings.fitMapBounds === false; |
|||
|
|||
if (!useDynamicLocations) { |
|||
this.subscription = this.ctx.defaultSubscription; |
|||
} |
|||
|
|||
this.configureLocationsSettings(); |
|||
|
|||
var minZoomLevel = this.drawRoutes ? 18 : 15; |
|||
|
|||
var initCallback = function() { |
|||
tbMap.update(); |
|||
tbMap.resize(); |
|||
}; |
|||
|
|||
this.ctx.$scope.onTooltipAction = function(event, actionName, dsIndex) { |
|||
tbMap.onTooltipAction(event, actionName, dsIndex); |
|||
}; |
|||
this.tooltipActionsMap = {}; |
|||
var descriptors = this.ctx.actionsApi.getActionDescriptors('tooltipAction'); |
|||
descriptors.forEach(function (descriptor) { |
|||
tbMap.tooltipActionsMap[descriptor.name] = descriptor; |
|||
}); |
|||
|
|||
if (mapProvider === 'google-map') { |
|||
this.map = new TbGoogleMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType); |
|||
} else if (mapProvider === 'openstreet-map') { |
|||
this.map = new TbOpenStreetMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel); |
|||
} |
|||
} |
|||
|
|||
setCallbacks(callbacks) { |
|||
Object.assign(this.callbacks, callbacks); |
|||
} |
|||
|
|||
clearLocations() { |
|||
if (this.locations) { |
|||
var tbMap = this; |
|||
this.locations.forEach(function(location) { |
|||
if (location.marker) { |
|||
tbMap.map.removeMarker(location.marker); |
|||
} |
|||
if (location.polyline) { |
|||
tbMap.map.removePolyline(location.polyline); |
|||
} |
|||
}); |
|||
this.locations = null; |
|||
this.markers = []; |
|||
if (this.drawRoutes) { |
|||
this.polylines = []; |
|||
} |
|||
} |
|||
} |
|||
|
|||
setSubscription(subscription) { |
|||
this.subscription = subscription; |
|||
this.clearLocations(); |
|||
} |
|||
|
|||
configureLocationsSettings() { |
|||
|
|||
this.locationSettings.latKeyName = this.ctx.settings.latKeyName || 'latitude'; |
|||
this.locationSettings.lngKeyName = this.ctx.settings.lngKeyName || 'longitude'; |
|||
|
|||
this.locationSettings.tooltipPattern = this.ctx.settings.tooltipPattern || "<b>${entityName}</b><br/><br/><b>Latitude:</b> ${"+this.locationSettings.latKeyName+":7}<br/><b>Longitude:</b> ${"+this.locationSettings.lngKeyName+":7}"; |
|||
|
|||
this.locationSettings.showLabel = this.ctx.settings.showLabel !== false; |
|||
this.locationSettings.displayTooltip = true; |
|||
this.locationSettings.labelColor = this.ctx.widgetConfig.color || '#000000', |
|||
this.locationSettings.label = this.ctx.settings.label || "${entityName}"; |
|||
this.locationSettings.color = this.ctx.settings.color ? tinycolor(this.ctx.settings.color).toHexString() : "#FE7569"; |
|||
|
|||
this.locationSettings.useColorFunction = this.ctx.settings.useColorFunction === true; |
|||
if (angular.isDefined(this.ctx.settings.colorFunction) && this.ctx.settings.colorFunction.length > 0) { |
|||
try { |
|||
this.locationSettings.colorFunction = new Function('data, dsData, dsIndex', this.ctx.settings.colorFunction); |
|||
} catch (e) { |
|||
this.locationSettings.colorFunction = null; |
|||
} |
|||
} |
|||
|
|||
this.locationSettings.useMarkerImageFunction = this.ctx.settings.useMarkerImageFunction === true; |
|||
if (angular.isDefined(this.ctx.settings.markerImageFunction) && this.ctx.settings.markerImageFunction.length > 0) { |
|||
try { |
|||
this.locationSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', this.ctx.settings.markerImageFunction); |
|||
} catch (e) { |
|||
this.locationSettings.markerImageFunction = null; |
|||
} |
|||
} |
|||
|
|||
this.locationSettings.markerImages = this.ctx.settings.markerImages || []; |
|||
|
|||
if (!this.locationSettings.useMarkerImageFunction && |
|||
angular.isDefined(this.ctx.settings.markerImage) && |
|||
this.ctx.settings.markerImage.length > 0) { |
|||
this.locationSettings.markerImage = this.ctx.settings.markerImage; |
|||
this.locationSettings.useMarkerImage = true; |
|||
this.locationSettings.markerImageSize = this.ctx.settings.markerImageSize || 34; |
|||
} |
|||
|
|||
if (this.drawRoutes) { |
|||
this.locationSettings.strokeWeight = this.ctx.settings.strokeWeight || 2; |
|||
this.locationSettings.strokeOpacity = this.ctx.settings.strokeOpacity || 1.0; |
|||
} |
|||
} |
|||
|
|||
onTooltipAction(event, actionName, dsIndex) { |
|||
var descriptor = this.tooltipActionsMap[actionName]; |
|||
if (descriptor) { |
|||
var datasource = this.subscription.datasources[dsIndex]; |
|||
var entityId = {}; |
|||
entityId.id = datasource.entityId; |
|||
entityId.entityType = datasource.entityType; |
|||
var entityName = datasource.entityName; |
|||
this.ctx.actionsApi.handleWidgetAction(event, descriptor, entityId, entityName); |
|||
} |
|||
} |
|||
|
|||
update() { |
|||
|
|||
var tbMap = this; |
|||
|
|||
function updateLocationLabel(location) { |
|||
if (location.settings.showLabel && location.settings.labelReplaceInfo.variables.length) { |
|||
location.settings.labelText = fillPattern(location.settings.label, |
|||
location.settings.labelReplaceInfo, tbMap.subscription.data); |
|||
tbMap.map.updateMarkerLabel(location.marker, location.settings); |
|||
} |
|||
} |
|||
|
|||
function calculateLocationColor(location, dataMap) { |
|||
if (location.settings.useColorFunction && location.settings.colorFunction) { |
|||
var color; |
|||
try { |
|||
color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex); |
|||
} catch (e) {/**/} |
|||
if (!color) { |
|||
color = '#FE7569'; |
|||
} |
|||
return tinycolor(color).toHexString(); |
|||
} else { |
|||
return location.settings.color; |
|||
} |
|||
} |
|||
|
|||
function updateLocationColor(location, color, image) { |
|||
if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) { |
|||
if (!location.settings.useMarkerImage && !image) { |
|||
tbMap.map.updateMarkerColor(location.marker, color); |
|||
} |
|||
if (location.polyline) { |
|||
tbMap.map.updatePolylineColor(location.polyline, location.settings, color); |
|||
} |
|||
location.settings.calculatedColor = color; |
|||
} |
|||
} |
|||
|
|||
function calculateLocationMarkerImage(location, dataMap) { |
|||
if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) { |
|||
var image = null; |
|||
try { |
|||
image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex); |
|||
} catch (e) { |
|||
image = null; |
|||
} |
|||
return image; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
function updateLocationMarkerImage(location, image) { |
|||
if (image && (!location.settings.calculatedImage || !angular.equals(location.settings.calculatedImage, image))) { |
|||
tbMap.map.updateMarkerImage(location.marker, location.settings, image.url, image.size); |
|||
location.settings.calculatedImage = image; |
|||
} |
|||
} |
|||
|
|||
function updateLocationStyle(location, dataMap) { |
|||
updateLocationLabel(location); |
|||
var color = calculateLocationColor(location, dataMap); |
|||
var image = calculateLocationMarkerImage(location, dataMap); |
|||
updateLocationColor(location, color, image); |
|||
updateLocationMarkerImage(location, image); |
|||
} |
|||
|
|||
function locationRowClick($event, location) { |
|||
var descriptors = tbMap.ctx.actionsApi.getActionDescriptors('markerClick'); |
|||
if (descriptors.length) { |
|||
var datasource = tbMap.subscription.datasources[location.dsIndex]; |
|||
var entityId = {}; |
|||
entityId.id = datasource.entityId; |
|||
entityId.entityType = datasource.entityType; |
|||
var entityName = datasource.entityName; |
|||
tbMap.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName); |
|||
} |
|||
} |
|||
|
|||
function updateLocation(location, data, dataMap) { |
|||
var locationChanged = false; |
|||
if (location.latIndex > -1 && location.lngIndex > -1) { |
|||
var latData = data[location.latIndex].data; |
|||
var lngData = data[location.lngIndex].data; |
|||
var lat, lng, latLng; |
|||
if (latData.length > 0 && lngData.length > 0) { |
|||
if (tbMap.drawRoutes) { |
|||
// Create or update route
|
|||
var latLngs = []; |
|||
for (var i = 0; i < latData.length; i++) { |
|||
lat = latData[i][1]; |
|||
lng = lngData[i][1]; |
|||
latLng = tbMap.map.createLatLng(lat, lng); |
|||
if (i == 0 || !latLngs[latLngs.length - 1].equals(latLng)) { |
|||
latLngs.push(latLng); |
|||
} |
|||
} |
|||
if (latLngs.length > 0) { |
|||
var markerLocation = latLngs[latLngs.length - 1]; |
|||
if (!location.marker) { |
|||
location.marker = tbMap.map.createMarker(markerLocation, location.settings, |
|||
function (event) { |
|||
tbMap.callbacks.onLocationClick(location); |
|||
locationRowClick(event, location); |
|||
}, [location.dsIndex] |
|||
); |
|||
} else { |
|||
tbMap.map.setMarkerPosition(location.marker, markerLocation); |
|||
} |
|||
} |
|||
if (!location.polyline) { |
|||
location.polyline = tbMap.map.createPolyline(latLngs, location.settings); |
|||
tbMap.polylines.push(location.polyline); |
|||
locationChanged = true; |
|||
} else { |
|||
var prevPath = tbMap.map.getPolylineLatLngs(location.polyline); |
|||
if (!prevPath || !arraysEqual(prevPath, latLngs)) { |
|||
tbMap.map.setPolylineLatLngs(location.polyline, latLngs); |
|||
locationChanged = true; |
|||
} |
|||
} |
|||
} else { |
|||
// Create or update marker
|
|||
lat = latData[latData.length - 1][1]; |
|||
lng = lngData[lngData.length - 1][1]; |
|||
latLng = tbMap.map.createLatLng(lat, lng); |
|||
if (!location.marker) { |
|||
location.marker = tbMap.map.createMarker(latLng, location.settings, |
|||
function (event) { |
|||
tbMap.callbacks.onLocationClick(location); |
|||
locationRowClick(event, location); |
|||
}, [location.dsIndex]); |
|||
tbMap.markers.push(location.marker); |
|||
locationChanged = true; |
|||
} else { |
|||
var prevPosition = tbMap.map.getMarkerPosition(location.marker); |
|||
if (!prevPosition.equals(latLng)) { |
|||
tbMap.map.setMarkerPosition(location.marker, latLng); |
|||
locationChanged = true; |
|||
} |
|||
} |
|||
} |
|||
updateLocationStyle(location, dataMap); |
|||
} |
|||
} |
|||
return locationChanged; |
|||
} |
|||
|
|||
function loadLocations(data, datasources) { |
|||
var bounds = tbMap.map.createBounds(); |
|||
tbMap.locations = []; |
|||
var dataMap = toLabelValueMap(data, datasources); |
|||
var currentDatasource = null; |
|||
var currentDatasourceIndex = -1; |
|||
var latIndex = -1; |
|||
var lngIndex = -1; |
|||
|
|||
for (var i=0;i<data.length;i++) { |
|||
var dataKeyData = data[i]; |
|||
var dataKey = dataKeyData.dataKey; |
|||
if (dataKeyData.datasource != currentDatasource) { |
|||
currentDatasource = dataKeyData.datasource; |
|||
currentDatasourceIndex++; |
|||
latIndex = -1; |
|||
lngIndex = -1; |
|||
} |
|||
var nameToCheck; |
|||
if (dataKey.locationAttrName) { |
|||
nameToCheck = dataKey.locationAttrName; |
|||
} else { |
|||
nameToCheck = dataKey.label; |
|||
} |
|||
if (nameToCheck === tbMap.locationSettings.latKeyName) { |
|||
latIndex = i; |
|||
} else if (nameToCheck === tbMap.locationSettings.lngKeyName) { |
|||
lngIndex = i; |
|||
} |
|||
if (latIndex > -1 && lngIndex > -1) { |
|||
var location = { |
|||
latIndex: latIndex, |
|||
lngIndex: lngIndex, |
|||
dsIndex: currentDatasourceIndex, |
|||
settings: angular.copy(tbMap.locationSettings) |
|||
}; |
|||
if (location.settings.showLabel) { |
|||
location.settings.label = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.label); |
|||
location.settings.labelReplaceInfo = processPattern(location.settings.label, datasources, currentDatasourceIndex); |
|||
location.settings.labelText = location.settings.label; |
|||
} |
|||
if (location.settings.displayTooltip) { |
|||
location.settings.tooltipPattern = tbMap.utils.createLabelFromDatasource(currentDatasource, location.settings.tooltipPattern); |
|||
location.settings.tooltipReplaceInfo = processPattern(location.settings.tooltipPattern, datasources, currentDatasourceIndex); |
|||
} |
|||
|
|||
tbMap.locations.push(location); |
|||
updateLocation(location, data, dataMap); |
|||
if (location.polyline) { |
|||
tbMap.map.extendBounds(bounds, location.polyline); |
|||
} else if (location.marker) { |
|||
tbMap.map.extendBoundsWithMarker(bounds, location.marker); |
|||
} |
|||
latIndex = -1; |
|||
lngIndex = -1; |
|||
} |
|||
|
|||
} |
|||
tbMap.map.fitBounds(bounds); |
|||
} |
|||
|
|||
function updateLocations(data, datasources) { |
|||
var locationsChanged = false; |
|||
var bounds = tbMap.map.createBounds(); |
|||
var dataMap = toLabelValueMap(data, datasources); |
|||
for (var p = 0; p < tbMap.locations.length; p++) { |
|||
var location = tbMap.locations[p]; |
|||
locationsChanged |= updateLocation(location, data, dataMap); |
|||
if (location.polyline) { |
|||
tbMap.map.extendBounds(bounds, location.polyline); |
|||
} else if (location.marker) { |
|||
tbMap.map.extendBoundsWithMarker(bounds, location.marker); |
|||
} |
|||
} |
|||
if (locationsChanged) { |
|||
tbMap.map.fitBounds(bounds); |
|||
} |
|||
} |
|||
|
|||
if (this.map && this.map.inited() && this.subscription) { |
|||
if (this.subscription.data) { |
|||
if (!this.locations) { |
|||
loadLocations(this.subscription.data, this.subscription.datasources); |
|||
} else { |
|||
updateLocations(this.subscription.data, this.subscription.datasources); |
|||
} |
|||
var tooltips = this.map.getTooltips(); |
|||
for (var t=0; t < tooltips.length; t++) { |
|||
var tooltip = tooltips[t]; |
|||
var text = fillPattern(tooltip.pattern, tooltip.replaceInfo, this.subscription.data); |
|||
text = fillPatternWithActions(text, 'onTooltipAction', tooltip.markerArgs); |
|||
tooltip.popup.setContent(text); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
resize() { |
|||
if (this.map && this.map.inited()) { |
|||
this.map.invalidateSize(); |
|||
if (this.locations && this.locations.length > 0) { |
|||
var bounds = this.map.createBounds(); |
|||
for (var m = 0; m < this.markers.length; m++) { |
|||
this.map.extendBoundsWithMarker(bounds, this.markers[m]); |
|||
} |
|||
if (this.polylines) { |
|||
for (var p = 0; p < this.polylines.length; p++) { |
|||
this.map.extendBounds(bounds, this.polylines[p]); |
|||
} |
|||
} |
|||
this.map.fitBounds(bounds); |
|||
} |
|||
} |
|||
} |
|||
|
|||
static settingsSchema(mapProvider, drawRoutes) { |
|||
var schema; |
|||
if (mapProvider === 'google-map') { |
|||
schema = angular.copy(googleMapSettingsSchema); |
|||
} else if (mapProvider === 'openstreet-map') { |
|||
schema = angular.copy(openstreetMapSettingsSchema); |
|||
} |
|||
angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties); |
|||
schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required); |
|||
schema.form = schema.form.concat(commonMapSettingsSchema.form); |
|||
if (drawRoutes) { |
|||
angular.merge(schema.schema.properties, routeMapSettingsSchema.schema.properties); |
|||
schema.schema.required = schema.schema.required.concat(routeMapSettingsSchema.schema.required); |
|||
schema.form = schema.form.concat(routeMapSettingsSchema.form); |
|||
} |
|||
return schema; |
|||
} |
|||
|
|||
static dataKeySettingsSchema(/*mapProvider*/) { |
|||
return {}; |
|||
} |
|||
|
|||
static actionSources() { |
|||
return { |
|||
'markerClick': { |
|||
name: 'widget-action.marker-click', |
|||
multiple: false |
|||
}, |
|||
'tooltipAction': { |
|||
name: 'widget-action.tooltip-tag-action', |
|||
multiple: true |
|||
} |
|||
}; |
|||
} |
|||
|
|||
} |
|||
|
|||
const googleMapSettingsSchema = |
|||
{ |
|||
"schema":{ |
|||
"title":"Google Map Configuration", |
|||
"type":"object", |
|||
"properties":{ |
|||
"gmApiKey":{ |
|||
"title":"Google Maps API Key", |
|||
"type":"string" |
|||
}, |
|||
"gmDefaultMapType":{ |
|||
"title":"Default map type", |
|||
"type":"string", |
|||
"default":"roadmap" |
|||
} |
|||
}, |
|||
"required":[ |
|||
"gmApiKey" |
|||
] |
|||
}, |
|||
"form":[ |
|||
"gmApiKey", |
|||
{ |
|||
"key":"gmDefaultMapType", |
|||
"type":"rc-select", |
|||
"multiple":false, |
|||
"items":[ |
|||
{ |
|||
"value":"roadmap", |
|||
"label":"Roadmap" |
|||
}, |
|||
{ |
|||
"value":"satellite", |
|||
"label":"Satellite" |
|||
}, |
|||
{ |
|||
"value":"hybrid", |
|||
"label":"Hybrid" |
|||
}, |
|||
{ |
|||
"value":"terrain", |
|||
"label":"Terrain" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}; |
|||
|
|||
const openstreetMapSettingsSchema = |
|||
{ |
|||
"schema":{ |
|||
"title":"Openstreet Map Configuration", |
|||
"type":"object", |
|||
"properties":{ |
|||
}, |
|||
"required":[ |
|||
] |
|||
}, |
|||
"form":[ |
|||
] |
|||
}; |
|||
|
|||
const commonMapSettingsSchema = |
|||
{ |
|||
"schema":{ |
|||
"title":"Map Configuration", |
|||
"type":"object", |
|||
"properties":{ |
|||
"defaultZoomLevel":{ |
|||
"title":"Default map zoom level (1 - 20)", |
|||
"type":"number" |
|||
}, |
|||
"fitMapBounds":{ |
|||
"title":"Fit map bounds to cover all markers", |
|||
"type":"boolean", |
|||
"default":true |
|||
}, |
|||
"latKeyName":{ |
|||
"title":"Latitude key name", |
|||
"type":"string", |
|||
"default":"latitude" |
|||
}, |
|||
"lngKeyName":{ |
|||
"title":"Longitude key name", |
|||
"type":"string", |
|||
"default":"longitude" |
|||
}, |
|||
"showLabel":{ |
|||
"title":"Show label", |
|||
"type":"boolean", |
|||
"default":true |
|||
}, |
|||
"label":{ |
|||
"title":"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", |
|||
"type":"string", |
|||
"default":"${entityName}" |
|||
}, |
|||
"tooltipPattern":{ |
|||
"title":"Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')", |
|||
"type":"string", |
|||
"default":"<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}" |
|||
}, |
|||
"color":{ |
|||
"title":"Color", |
|||
"type":"string" |
|||
}, |
|||
"useColorFunction":{ |
|||
"title":"Use color function", |
|||
"type":"boolean", |
|||
"default":false |
|||
}, |
|||
"colorFunction":{ |
|||
"title":"Color function: f(data, dsData, dsIndex)", |
|||
"type":"string" |
|||
}, |
|||
"markerImage":{ |
|||
"title":"Custom marker image", |
|||
"type":"string" |
|||
}, |
|||
"markerImageSize":{ |
|||
"title":"Custom marker image size (px)", |
|||
"type":"number", |
|||
"default":34 |
|||
}, |
|||
"useMarkerImageFunction":{ |
|||
"title":"Use marker image function", |
|||
"type":"boolean", |
|||
"default":false |
|||
}, |
|||
"markerImageFunction":{ |
|||
"title":"Marker image function: f(data, images, dsData, dsIndex)", |
|||
"type":"string" |
|||
}, |
|||
"markerImages":{ |
|||
"title":"Marker images", |
|||
"type":"array", |
|||
"items":{ |
|||
"title":"Marker image", |
|||
"type":"string" |
|||
} |
|||
} |
|||
}, |
|||
"required":[] |
|||
}, |
|||
"form":[ |
|||
"defaultZoomLevel", |
|||
"fitMapBounds", |
|||
"latKeyName", |
|||
"lngKeyName", |
|||
"showLabel", |
|||
"label", |
|||
{ |
|||
"key": "tooltipPattern", |
|||
"type": "textarea" |
|||
}, |
|||
{ |
|||
"key":"color", |
|||
"type":"color" |
|||
}, |
|||
"useColorFunction", |
|||
{ |
|||
"key":"colorFunction", |
|||
"type":"javascript" |
|||
}, |
|||
{ |
|||
"key":"markerImage", |
|||
"type":"image" |
|||
}, |
|||
"markerImageSize", |
|||
"useMarkerImageFunction", |
|||
{ |
|||
"key":"markerImageFunction", |
|||
"type":"javascript" |
|||
}, |
|||
{ |
|||
"key":"markerImages", |
|||
"items":[ |
|||
{ |
|||
"key":"markerImages[]", |
|||
"type":"image" |
|||
} |
|||
] |
|||
} |
|||
] |
|||
}; |
|||
|
|||
const routeMapSettingsSchema = |
|||
{ |
|||
"schema":{ |
|||
"title":"Route Map Configuration", |
|||
"type":"object", |
|||
"properties":{ |
|||
"strokeWeight": { |
|||
"title": "Stroke weight", |
|||
"type": "number", |
|||
"default": 2 |
|||
}, |
|||
"strokeOpacity": { |
|||
"title": "Stroke opacity", |
|||
"type": "number", |
|||
"default": 1.0 |
|||
} |
|||
}, |
|||
"required":[ |
|||
] |
|||
}, |
|||
"form":[ |
|||
"strokeWeight", |
|||
"strokeOpacity" |
|||
] |
|||
}; |
|||
@ -0,0 +1,209 @@ |
|||
/* |
|||
* Copyright © 2016-2017 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
const varsRegex = /\$\{([^\}]*)\}/g; |
|||
|
|||
const linkActionRegex = /\<link-act name=['"]([^['"]*)['"]\>([^\<]*)\<\/link-act\>/g; |
|||
const buttonActionRegex = /\<button-act name=['"]([^['"]*)['"]\>([^\<]*)\<\/button-act\>/g; |
|||
|
|||
export function processPattern(pattern, datasources, dsIndex) { |
|||
var match = varsRegex.exec(pattern); |
|||
var replaceInfo = {}; |
|||
replaceInfo.variables = []; |
|||
while (match !== null) { |
|||
var variableInfo = {}; |
|||
variableInfo.dataKeyIndex = -1; |
|||
var variable = match[0]; |
|||
var label = match[1]; |
|||
var valDec = 2; |
|||
var splitVals = label.split(':'); |
|||
if (splitVals.length > 1) { |
|||
label = splitVals[0]; |
|||
valDec = parseFloat(splitVals[1]); |
|||
} |
|||
variableInfo.variable = variable; |
|||
variableInfo.valDec = valDec; |
|||
|
|||
var keyIndex = -1; |
|||
|
|||
if (label.startsWith('#')) { |
|||
var keyIndexStr = label.substring(1); |
|||
var n = Math.floor(Number(keyIndexStr)); |
|||
if (String(n) === keyIndexStr && n >= 0) { |
|||
keyIndex = n; |
|||
} |
|||
} |
|||
var offset = 0; |
|||
for (var i=0;i<datasources.length;i++) { |
|||
var datasource = datasources[i]; |
|||
if (dsIndex == i) { |
|||
if (keyIndex > -1) { |
|||
if (keyIndex < datasource.dataKeys.length) { |
|||
variableInfo.dataKeyIndex = offset + keyIndex; |
|||
} |
|||
} else { |
|||
for (var k = 0; k < datasource.dataKeys.length; k++) { |
|||
var dataKey = datasource.dataKeys[k]; |
|||
if (dataKey.label === label) { |
|||
variableInfo.dataKeyIndex = offset + k; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
offset += datasource.dataKeys.length; |
|||
} |
|||
replaceInfo.variables.push(variableInfo); |
|||
match = varsRegex.exec(pattern); |
|||
} |
|||
return replaceInfo; |
|||
} |
|||
|
|||
export function fillPattern(pattern, replaceInfo, data) { |
|||
var text = angular.copy(pattern); |
|||
for (var v = 0; v < replaceInfo.variables.length; v++) { |
|||
var variableInfo = replaceInfo.variables[v]; |
|||
var txtVal = ''; |
|||
if (variableInfo.dataKeyIndex > -1 && data[variableInfo.dataKeyIndex]) { |
|||
var varData = data[variableInfo.dataKeyIndex].data; |
|||
if (varData.length > 0) { |
|||
var val = varData[varData.length - 1][1]; |
|||
if (isNumber(val)) { |
|||
txtVal = padValue(val, variableInfo.valDec, 0); |
|||
} else { |
|||
txtVal = val; |
|||
} |
|||
} |
|||
} |
|||
text = text.split(variableInfo.variable).join(txtVal); |
|||
} |
|||
return text; |
|||
} |
|||
|
|||
function createLink(actionName, actionText, actionCallbackName, additionalArgs) { |
|||
var args = 'event,\''+actionName+'\''; |
|||
if (additionalArgs && additionalArgs.length) { |
|||
args += ','+additionalArgs.join(); |
|||
} |
|||
return '<a href="#" onclick="angular.element(this).scope().'+actionCallbackName+'('+args+'); return false;">'+actionText+'</a>'; |
|||
} |
|||
|
|||
function createButton(actionName, actionText, actionCallbackName, additionalArgs) { |
|||
var args = 'event,\''+actionName+'\''; |
|||
if (additionalArgs && additionalArgs.length) { |
|||
args += ','+additionalArgs.join(); |
|||
} |
|||
return '<button onclick="angular.element(this).scope().'+actionCallbackName+'('+args+'); return false;">'+actionText+'</button>'; |
|||
} |
|||
|
|||
export function fillPatternWithActions(pattern, actionCallbackName, additionalArgs) { |
|||
var text = angular.copy(pattern); |
|||
var match = linkActionRegex.exec(pattern); |
|||
var actionTags; |
|||
var actionName; |
|||
var actionText; |
|||
var actionHtml; |
|||
while (match !== null) { |
|||
actionTags = match[0]; |
|||
actionName = match[1]; |
|||
actionText = match[2]; |
|||
actionHtml = createLink(actionName, actionText, actionCallbackName, additionalArgs); |
|||
text = text.split(actionTags).join(actionHtml); |
|||
match = linkActionRegex.exec(pattern); |
|||
} |
|||
match = buttonActionRegex.exec(pattern); |
|||
while (match !== null) { |
|||
actionTags = match[0]; |
|||
actionName = match[1]; |
|||
actionText = match[2]; |
|||
actionHtml = createButton(actionName, actionText, actionCallbackName, additionalArgs); |
|||
text = text.split(actionTags).join(actionHtml); |
|||
match = buttonActionRegex.exec(pattern); |
|||
} |
|||
return text; |
|||
} |
|||
|
|||
export function toLabelValueMap(data, datasources) { |
|||
var dataMap = {}; |
|||
var dsDataMap = []; |
|||
for (var d=0;d<datasources.length;d++) { |
|||
dsDataMap[d] = {}; |
|||
} |
|||
for (var i = 0; i < data.length; i++) { |
|||
var dataKey = data[i].dataKey; |
|||
var label = dataKey.label; |
|||
var keyData = data[i].data; |
|||
var val = null; |
|||
if (keyData.length > 0) { |
|||
val = keyData[keyData.length-1][1]; |
|||
} |
|||
dataMap[label] = val; |
|||
var dsIndex = datasources.indexOf(data[i].datasource); |
|||
dsDataMap[dsIndex][label] = val; |
|||
} |
|||
return { |
|||
dataMap: dataMap, |
|||
dsDataMap: dsDataMap |
|||
}; |
|||
} |
|||
|
|||
export function isNumber(n) { |
|||
return !isNaN(parseFloat(n)) && isFinite(n); |
|||
} |
|||
|
|||
export function padValue(val, dec, int) { |
|||
var i = 0; |
|||
var s, strVal, n; |
|||
|
|||
val = parseFloat(val); |
|||
n = (val < 0); |
|||
val = Math.abs(val); |
|||
|
|||
if (dec > 0) { |
|||
strVal = val.toFixed(dec).toString().split('.'); |
|||
s = int - strVal[0].length; |
|||
|
|||
for (; i < s; ++i) { |
|||
strVal[0] = '0' + strVal[0]; |
|||
} |
|||
|
|||
strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1]; |
|||
} |
|||
|
|||
else { |
|||
strVal = Math.round(val).toString(); |
|||
s = int - strVal.length; |
|||
|
|||
for (; i < s; ++i) { |
|||
strVal = '0' + strVal; |
|||
} |
|||
|
|||
strVal = (n ? '-' : '') + strVal; |
|||
} |
|||
|
|||
return strVal; |
|||
} |
|||
|
|||
export function arraysEqual(a, b) { |
|||
if (a === b) return true; |
|||
if (a === null || b === null) return false; |
|||
if (a.length != b.length) return false; |
|||
|
|||
for (var i = 0; i < a.length; ++i) { |
|||
if (!a[i].equals(b[i])) return false; |
|||
} |
|||
return true; |
|||
} |
|||
Loading…
Reference in new issue