thingsboard-memoizeit

Improve mobile layout. Improve route map widget. Add route

12/29/2016 2:48:47 PM

Details

diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index f91243a..c538a1c 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -134,7 +134,7 @@ VALUES (  now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
 VALUES (  now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
-'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n    color: red;\n}\n.tb-labels {\n  color: #222;\n  font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n  text-align: center;\n  width: 100px;\n  white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n        var match = varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split('':'');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            \n            if (label.startsWith(''#'')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = datasourceOffset + n;\n                }\n            }\n            if (variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < datasource.dataKeys.length; i++) {\n                     var dataKey = datasource.dataKeys[i];\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = datasourceOffset + i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }\n\n    \n    var configuredRoutesSettings = settings.routesSettings;\n    if (!configuredRoutesSettings) {\n        configuredRoutesSettings = [];\n    }\n    \n    var datasourceOffset = 0;\n    for (var i=0;i<datasources.length;i++) {\n        routesSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,            \n            color: \"#FE7569\",\n            strokeWeight: 2,\n            strokeOpacity: 1.0,\n            tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n        };\n        if (configuredRoutesSettings[i]) {\n            routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n            routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n            routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n            \n            routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n            \n            routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n            routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n            routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n            routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n            routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity;            \n        }\n        datasourceOffset += datasources[i].dataKeys.length;\n    }\n\n    var mapId = '''' + Math.random().toString(36).substr(2, 9);\n    \n    function clearGlobalId() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n        }\n    }\n    \n    $window.gm_authFailure = function() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n            $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n            displayError($window.gmApiKeys[apiKey].error);\n        }\n    };\n    \n    function displayError(message) {\n        $(containerElement).html(\n            \"<div class=''error''>\"+ message + \"</div>\"\n        );\n    }\n\n    var initMapFunctionName = ''initGoogleMap_'' + mapId;\n    $window[initMapFunctionName] = function() {\n        lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n            function success() {\n                initMap();\n            },\n            function fail() {\n                clearGloabalId();\n                $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                displayError($window.gmApiKeys[apiKey].error);\n            }\n        );\n        \n    };   \n    \n    var apiKey = settings.gmApiKey || '''';\n\n    if (apiKey && apiKey.length > 0) {\n        if (!$window.gmApiKeys) {\n            $window.gmApiKeys = {};\n        }\n        if ($window.gmApiKeys[apiKey]) {\n            if ($window.gmApiKeys[apiKey].error) {\n                displayError($window.gmApiKeys[apiKey].error);\n            } else {\n                initMap();\n            }\n        } else {\n            $window.gmApiKeys[apiKey] = {};\n            var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n        \n            $window.loadingGmId = mapId;\n            lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n                function success() {\n                    setTimeout(clearGlobalId, 2000);\n                },\n                function fail(e) {\n                    clearGloabalId();\n                    $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                    displayError($window.gmApiKeys[apiKey].error);\n                }\n            );\n        }\n    } else {\n        displayError(''No Google Map Api Key provided!'');\n    }\n\n    function initMap() {\n        \n        map = new google.maps.Map(containerElement, {\n          scrollwheel: false,\n          zoom: defaultZoomLevel || 8\n        });\n\n    }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n        \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n    \n    function padValue(val, dec, int) {\n        var i = 0;\n        var s, strVal, n;\n    \n        val = parseFloat(val);\n        n = (val < 0);\n        val = Math.abs(val);\n    \n        if (dec > 0) {\n            strVal = val.toFixed(dec).toString().split(''.'');\n            s = int - strVal[0].length;\n    \n            for (; i < s; ++i) {\n                strVal[0] = ''0'' + strVal[0];\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n        }\n    \n        else {\n            strVal = Math.round(val).toString();\n            s = int - strVal.length;\n    \n            for (; i < s; ++i) {\n                strVal = ''0'' + strVal;\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal;\n        }\n    \n        return strVal;\n    }        \n        \n    function createMarker(location, settings) {\n        var pinColor = settings.color.substr(1);\n        var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n            new google.maps.Size(21, 34),\n            new google.maps.Point(0,0),\n            new google.maps.Point(10, 34));\n        var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n            new google.maps.Size(40, 37),\n            new google.maps.Point(0, 0),\n            new google.maps.Point(12, 35));        \n        var marker;\n        if (settings.showLabel) {    \n                marker = new MarkerWithLabel({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow,\n                    labelContent: ''<b>''+settings.label+''</b>'',\n                    labelClass: \"tb-labels\",\n                    labelAnchor: new google.maps.Point(50, 55)\n                });            \n        } else {\n                marker = new google.maps.Marker({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow\n                });            \n        }\n        \n        createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n            \n        return marker;    \n    }\n    \n    function createTooltip(marker, pattern, replaceInfo) {\n        var infowindow = new google.maps.InfoWindow({\n          content: ''''\n        });\n        marker.addListener(''click'', function() {\n          infowindow.open(map, marker);\n        });\n        tooltips.push( {\n            infowindow: infowindow,\n            pattern: pattern,\n            replaceInfo: replaceInfo\n        });\n    }\n\n    function createPolyline(locations, settings) {\n        var polyline = new google.maps.Polyline({\n          path: locations,\n          strokeColor: settings.color,\n          strokeOpacity: settings.strokeOpacity,\n          strokeWeight: settings.strokeWeight,\n          map: map\n        });\n            \n        return polyline;    \n    }    \n    \n    function arraysEqual(a, b) {\n        if (a === b) return true;\n        if (a === null || b === null) return false;\n        if (a.length != b.length) return false;\n\n        for (var i = 0; i < a.length; ++i) {\n            if (a[i] !== b[i]) return false;\n        }\n        return true;\n    }\n    \n    \n    function updateRoute(route, data) {\n        if (route.latIndex > -1 && route.lngIndex > -1) {\n            var latData = data[route.latIndex].data;\n            var lngData = data[route.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var locations = [];\n                for (var i = 0; i < latData.length; i++) {\n                    var lat = latData[i][1];\n                    var lng = lngData[i][1];\n                    var location = new google.maps.LatLng(lat, lng);\n                    locations.push(location);\n                }\n                var markerLocation;\n                if (locations.length > 0) {\n                    markerLocation = locations[locations.length-1];\n                }\n                if (!route.polyline) {\n                    route.polyline = createPolyline(locations, route.settings);\n                    if (markerLocation) {\n                        route.marker = createMarker(markerLocation, route.settings);\n                    }\n                    polylines.push(route.polyline);\n                    return true;\n                } else {\n                    var prevPath = route.polyline.getPath();\n                    if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n                        route.polyline.setPath(locations);\n                        if (markerLocation) {\n                            if (!route.marker) {\n                                route.marker = createMarker(markerLocation, route.settings);\n                            } else {\n                                route.marker.setPosition(markerLocation);\n                            }\n                        }\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n    \n    function extendBounds(bounds, polyline) {\n        if (polyline && polyline.getPath()) {\n            var locations = polyline.getPath();\n            for (var i = 0; i < locations.getLength(); i++) {\n                bounds.extend(locations.getAt(i));\n            }\n        }\n    }\n    \n    function loadRoutes(data) {\n        var bounds = new google.maps.LatLngBounds();\n        routes = [];\n        var datasourceIndex = -1;\n        var routeSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                routeSettings = routesSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === routeSettings.latKeyName ||\n                dataKey.label === routeSettings.lngKeyName) {\n                var route = routes[datasourceIndex];\n                if (!route) {\n                    route = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: routeSettings\n                    };\n                    routes[datasourceIndex] = route;\n                } else if (route.polyline) {\n                    continue;\n                }\n                if (dataKey.label === routeSettings.latKeyName) {\n                    route.latIndex = i;\n                } else {\n                    route.lngIndex = i;\n                }\n                if (route.latIndex > -1 && route.lngIndex > -1) {\n                    updateRoute(route, data);\n                    if (route.polyline) {\n                        extendBounds(bounds, route.polyline);\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n \n    \n    function updateRoutes(data) {\n        var routesChanged = false;\n        var bounds = new google.maps.LatLngBounds();\n        for (var r in routes) {\n            var route = routes[r];\n            routesChanged |= updateRoute(route, data);\n            if (route.polyline) {\n                extendBounds(bounds, route.polyline);\n            }\n        }\n        if (!dontFitMapBounds && routesChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n            var zoomLevel = defaultZoomLevel || map.getZoom();\n            this.setZoom(zoomLevel);\n            if (!defaultZoomLevel && this.getZoom() > 15) {\n                this.setZoom(15);\n            }\n        });\n        map.fitBounds(bounds);\n    }\n\n    if (map) {\n        if (data) {\n            if (!routes) {\n                loadRoutes(data);\n            } else {\n                updateRoutes(data);\n            }\n        }\n        if (sizeChanged) {\n            google.maps.event.trigger(map, \"resize\");\n            if (!dontFitMapBounds) {\n                var bounds = new google.maps.LatLngBounds();\n                for (var p in polylines) {\n                    extendBounds(bounds, polylines[p]);\n                }\n                fitMapBounds(bounds);\n            }\n        }\n        \n        for (var t in tooltips) {\n            var tooltip = tooltips[t];\n            var text = tooltip.pattern;\n            var replaceInfo = tooltip.replaceInfo;\n            for (var v in replaceInfo.variables) {\n                var variableInfo = replaceInfo.variables[v];\n                var txtVal = '''';\n                if (variableInfo.dataKeyIndex > -1) {\n                    var varData = data[variableInfo.dataKeyIndex].data;\n                    if (varData.length > 0) {\n                        var val = varData[varData.length-1][1];\n                        if (isNumber(val)) {\n                            txtVal = padValue(val, variableInfo.valDec, 0);\n                        } else {\n                            txtVal = val;\n                        }\n                    }\n                }\n                text = text.split(variableInfo.variable).join(txtVal);\n            }\n            tooltip.infowindow.setContent(text);\n        }\n        \n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Route Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"gmApiKey\": {\n        \"title\": \"Google Maps API Key\",\n        \"type\": \"string\"\n      },\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all routes\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"routesSettings\": {\n            \"title\": \"Routes settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Route settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },\n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"tooltipPattern\": {\n                    \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units''  )\",\n                    \"type\": \"string\",\n                    \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  },\n                  \"strokeWeight\": {\n                    \"title\": \"Stroke weight\",\n                    \"type\": \"number\",\n                    \"default\": 2\n                  },\n                  \"strokeOpacity\": {\n                    \"title\": \"Stroke opacity\",\n                    \"type\": \"number\",\n                    \"default\": 1.0\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n      \"gmApiKey\"\n    ]\n  },\n  \"form\": [\n    \"gmApiKey\",\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"routesSettings\",\n        \"items\": [\n            \"routesSettings[].latKeyName\",\n            \"routesSettings[].lngKeyName\",\n            \"routesSettings[].showLabel\",\n            \"routesSettings[].label\",\n            \"routesSettings[].tooltipPattern\",\n            {\n                \"key\": \"routesSettings[].color\",\n                \"type\": \"color\"\n            },\n            \"routesSettings[].strokeWeight\",\n            \"routesSettings[].strokeOpacity\"\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"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];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"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];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":false,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}],\"defaultZoomLevel\":16},\"title\":\"Route Map\"}"}',
+'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n    color: red;\n}\n.tb-labels {\n  color: #222;\n  font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n  text-align: center;\n  width: 100px;\n  white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n        var match = varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split('':'');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            \n            if (label.startsWith(''#'')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = datasourceOffset + n;\n                }\n            }\n            if (variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < datasource.dataKeys.length; i++) {\n                     var dataKey = datasource.dataKeys[i];\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = datasourceOffset + i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }\n\n    \n    var configuredRoutesSettings = settings.routesSettings;\n    if (!configuredRoutesSettings) {\n        configuredRoutesSettings = [];\n    }\n    \n    var datasourceOffset = 0;\n    for (var i=0;i<datasources.length;i++) {\n        routesSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,            \n            color: \"#FE7569\",\n            strokeWeight: 2,\n            strokeOpacity: 1.0,\n            tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n        };\n        if (configuredRoutesSettings[i]) {\n            routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n            routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n            routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n            \n            routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n            \n            routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n            routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n            routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n            routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n            routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity;            \n        }\n        datasourceOffset += datasources[i].dataKeys.length;\n    }\n\n    var mapId = '''' + Math.random().toString(36).substr(2, 9);\n    \n    function clearGlobalId() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n        }\n    }\n    \n    $window.gm_authFailure = function() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n            $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n            displayError($window.gmApiKeys[apiKey].error);\n        }\n    };\n    \n    function displayError(message) {\n        $(containerElement).html(\n            \"<div class=''error''>\"+ message + \"</div>\"\n        );\n    }\n\n    var initMapFunctionName = ''initGoogleMap_'' + mapId;\n    $window[initMapFunctionName] = function() {\n        lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n            function success() {\n                initMap();\n            },\n            function fail() {\n                clearGloabalId();\n                $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                displayError($window.gmApiKeys[apiKey].error);\n            }\n        );\n        \n    };   \n    \n    var apiKey = settings.gmApiKey || '''';\n\n    if (apiKey && apiKey.length > 0) {\n        if (!$window.gmApiKeys) {\n            $window.gmApiKeys = {};\n        }\n        if ($window.gmApiKeys[apiKey]) {\n            if ($window.gmApiKeys[apiKey].error) {\n                displayError($window.gmApiKeys[apiKey].error);\n            } else {\n                initMap();\n            }\n        } else {\n            $window.gmApiKeys[apiKey] = {};\n            var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n        \n            $window.loadingGmId = mapId;\n            lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n                function success() {\n                    setTimeout(clearGlobalId, 2000);\n                },\n                function fail(e) {\n                    clearGloabalId();\n                    $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                    displayError($window.gmApiKeys[apiKey].error);\n                }\n            );\n        }\n    } else {\n        displayError(''No Google Map Api Key provided!'');\n    }\n\n    function initMap() {\n        \n        map = new google.maps.Map(containerElement, {\n          scrollwheel: false,\n          zoom: defaultZoomLevel || 8\n        });\n\n    }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n        \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n    \n    function padValue(val, dec, int) {\n        var i = 0;\n        var s, strVal, n;\n    \n        val = parseFloat(val);\n        n = (val < 0);\n        val = Math.abs(val);\n    \n        if (dec > 0) {\n            strVal = val.toFixed(dec).toString().split(''.'');\n            s = int - strVal[0].length;\n    \n            for (; i < s; ++i) {\n                strVal[0] = ''0'' + strVal[0];\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n        }\n    \n        else {\n            strVal = Math.round(val).toString();\n            s = int - strVal.length;\n    \n            for (; i < s; ++i) {\n                strVal = ''0'' + strVal;\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal;\n        }\n    \n        return strVal;\n    }        \n        \n    function createMarker(location, settings) {\n        var pinColor = settings.color.substr(1);\n        var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n            new google.maps.Size(21, 34),\n            new google.maps.Point(0,0),\n            new google.maps.Point(10, 34));\n        var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n            new google.maps.Size(40, 37),\n            new google.maps.Point(0, 0),\n            new google.maps.Point(12, 35));        \n        var marker;\n        if (settings.showLabel) {    \n                marker = new MarkerWithLabel({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow,\n                    labelContent: ''<b>''+settings.label+''</b>'',\n                    labelClass: \"tb-labels\",\n                    labelAnchor: new google.maps.Point(50, 55)\n                });            \n        } else {\n                marker = new google.maps.Marker({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow\n                });            \n        }\n        \n        createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n            \n        return marker;    \n    }\n    \n    function createTooltip(marker, pattern, replaceInfo) {\n        var infowindow = new google.maps.InfoWindow({\n          content: ''''\n        });\n        marker.addListener(''click'', function() {\n          infowindow.open(map, marker);\n        });\n        tooltips.push( {\n            infowindow: infowindow,\n            pattern: pattern,\n            replaceInfo: replaceInfo\n        });\n    }\n\n    function createPolyline(locations, settings) {\n        var polyline = new google.maps.Polyline({\n          path: locations,\n          strokeColor: settings.color,\n          strokeOpacity: settings.strokeOpacity,\n          strokeWeight: settings.strokeWeight,\n          map: map\n        });\n            \n        return polyline;    \n    }    \n    \n    function arraysEqual(a, b) {\n        if (a === b) return true;\n        if (a === null || b === null) return false;\n        if (a.length != b.length) return false;\n\n        for (var i = 0; i < a.length; ++i) {\n            if (!a[i].equals(b[i])) return false;\n        }\n        return true;\n    }\n    \n    \n    function updateRoute(route, data) {\n        if (route.latIndex > -1 && route.lngIndex > -1) {\n            var latData = data[route.latIndex].data;\n            var lngData = data[route.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var locations = [];\n                for (var i = 0; i < latData.length; i++) {\n                    var lat = latData[i][1];\n                    var lng = lngData[i][1];\n                    var location = new google.maps.LatLng(lat, lng);\n                    if (i == 0 || !locations[locations.length-1].equals(location)) {\n                        locations.push(location);\n                    }\n                }\n                var markerLocation;\n                if (locations.length > 0) {\n                    markerLocation = locations[locations.length-1];\n                }\n                if (!route.polyline) {\n                    route.polyline = createPolyline(locations, route.settings);\n                    if (markerLocation) {\n                        route.marker = createMarker(markerLocation, route.settings);\n                    }\n                    polylines.push(route.polyline);\n                    return true;\n                } else {\n                    var prevPath = route.polyline.getPath();\n                    if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n                        route.polyline.setPath(locations);\n                        if (markerLocation) {\n                            if (!route.marker) {\n                                route.marker = createMarker(markerLocation, route.settings);\n                            } else {\n                                route.marker.setPosition(markerLocation);\n                            }\n                        }\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n    \n    function extendBounds(bounds, polyline) {\n        if (polyline && polyline.getPath()) {\n            var locations = polyline.getPath();\n            for (var i = 0; i < locations.getLength(); i++) {\n                bounds.extend(locations.getAt(i));\n            }\n        }\n    }\n    \n    function loadRoutes(data) {\n        var bounds = new google.maps.LatLngBounds();\n        routes = [];\n        var datasourceIndex = -1;\n        var routeSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                routeSettings = routesSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === routeSettings.latKeyName ||\n                dataKey.label === routeSettings.lngKeyName) {\n                var route = routes[datasourceIndex];\n                if (!route) {\n                    route = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: routeSettings\n                    };\n                    routes[datasourceIndex] = route;\n                } else if (route.polyline) {\n                    continue;\n                }\n                if (dataKey.label === routeSettings.latKeyName) {\n                    route.latIndex = i;\n                } else {\n                    route.lngIndex = i;\n                }\n                if (route.latIndex > -1 && route.lngIndex > -1) {\n                    updateRoute(route, data);\n                    if (route.polyline) {\n                        extendBounds(bounds, route.polyline);\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n \n    \n    function updateRoutes(data) {\n        var routesChanged = false;\n        var bounds = new google.maps.LatLngBounds();\n        for (var r in routes) {\n            var route = routes[r];\n            routesChanged |= updateRoute(route, data);\n            if (route.polyline) {\n                extendBounds(bounds, route.polyline);\n            }\n        }\n        if (!dontFitMapBounds && routesChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n            var newZoomLevel = map.getZoom();\n            if (dontFitMapBounds && defaultZoomLevel) {\n                newZoomLevel = defaultZoomLevel;\n            }\n            map.setZoom(newZoomLevel);\n            if (!defaultZoomLevel && map.getZoom() > 18) {\n                map.setZoom(18);\n            }\n        });\n        map.fitBounds(bounds);\n    }\n\n    if (map) {\n        if (data) {\n            if (!routes) {\n                loadRoutes(data);\n            } else {\n                updateRoutes(data);\n            }\n        }\n        if (sizeChanged) {\n            google.maps.event.trigger(map, \"resize\");\n            if (!dontFitMapBounds) {\n                var bounds = new google.maps.LatLngBounds();\n                for (var p in polylines) {\n                    extendBounds(bounds, polylines[p]);\n                }\n                fitMapBounds(bounds);\n            }\n        }\n        \n        for (var t in tooltips) {\n            var tooltip = tooltips[t];\n            var text = tooltip.pattern;\n            var replaceInfo = tooltip.replaceInfo;\n            for (var v in replaceInfo.variables) {\n                var variableInfo = replaceInfo.variables[v];\n                var txtVal = '''';\n                if (variableInfo.dataKeyIndex > -1) {\n                    var varData = data[variableInfo.dataKeyIndex].data;\n                    if (varData.length > 0) {\n                        var val = varData[varData.length-1][1];\n                        if (isNumber(val)) {\n                            txtVal = padValue(val, variableInfo.valDec, 0);\n                        } else {\n                            txtVal = val;\n                        }\n                    }\n                }\n                text = text.split(variableInfo.variable).join(txtVal);\n            }\n            tooltip.infowindow.setContent(text);\n        }\n        \n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Route Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"gmApiKey\": {\n        \"title\": \"Google Maps API Key\",\n        \"type\": \"string\"\n      },\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all routes\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"routesSettings\": {\n            \"title\": \"Routes settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Route settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },\n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"tooltipPattern\": {\n                    \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units''  )\",\n                    \"type\": \"string\",\n                    \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  },\n                  \"strokeWeight\": {\n                    \"title\": \"Stroke weight\",\n                    \"type\": \"number\",\n                    \"default\": 2\n                  },\n                  \"strokeOpacity\": {\n                    \"title\": \"Stroke opacity\",\n                    \"type\": \"number\",\n                    \"default\": 1.0\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n      \"gmApiKey\"\n    ]\n  },\n  \"form\": [\n    \"gmApiKey\",\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"routesSettings\",\n        \"items\": [\n            \"routesSettings[].latKeyName\",\n            \"routesSettings[].lngKeyName\",\n            \"routesSettings[].showLabel\",\n            \"routesSettings[].label\",\n            \"routesSettings[].tooltipPattern\",\n            {\n                \"key\": \"routesSettings[].color\",\n                \"type\": \"color\"\n            },\n            \"routesSettings[].strokeWeight\",\n            \"routesSettings[].strokeOpacity\"\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"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];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"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];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}]},\"title\":\"Route Map\"}"}',
 'Route Map' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
@@ -208,6 +208,11 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
 'OpenStreetMap' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map_openstreetmap',
+'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"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":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n        var match = varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split('':'');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            \n            if (label.startsWith(''#'')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = datasourceOffset + n;\n                }\n            }\n            if (variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < datasource.dataKeys.length; i++) {\n                     var dataKey = datasource.dataKeys[i];\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = datasourceOffset + i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }    \n    \n    var configuredRoutesSettings = settings.routesSettings;\n    if (!configuredRoutesSettings) {\n        configuredRoutesSettings = [];\n    }\n    \n    var datasourceOffset = 0;\n    for (var i=0;i<datasources.length;i++) {\n        routesSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,            \n            color: \"#FE7569\",\n            strokeWeight: 2,\n            strokeOpacity: 1.0,\n            tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n        };\n        if (configuredRoutesSettings[i]) {\n            \n            routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n            routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n            routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n            \n            routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n            \n            routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n            routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n            routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHex() : routesSettings[i].color;\n            routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n            routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n        }\n        datasourceOffset += datasources[i].dataKeys.length;\n    }\n    \n    map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n    L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n        attribution: ''&copy; <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n    }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n    \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n    \n    function padValue(val, dec, int) {\n        var i = 0;\n        var s, strVal, n;\n    \n        val = parseFloat(val);\n        n = (val < 0);\n        val = Math.abs(val);\n    \n        if (dec > 0) {\n            strVal = val.toFixed(dec).toString().split(''.'');\n            s = int - strVal[0].length;\n    \n            for (; i < s; ++i) {\n                strVal[0] = ''0'' + strVal[0];\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n        }\n    \n        else {\n            strVal = Math.round(val).toString();\n            s = int - strVal.length;\n    \n            for (; i < s; ++i) {\n                strVal = ''0'' + strVal;\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal;\n        }\n    \n        return strVal;\n    }                \n    \n    function createMarker(location, settings) {\n        var pinColor = settings.color;\n\n        var icon = L.icon({\n            iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n            iconSize: [21, 34],\n            iconAnchor: [10, 34],\n            popupAnchor: [0, -34],\n            shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n            shadowSize: [40, 37],\n            shadowAnchor: [12, 35]\n        });\n        \n        var marker = L.marker(location, {icon: icon}).addTo(map);\n        if (settings.showLabel) {\n            marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n        }\n        \n        createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n        \n        return marker;\n    }\n    \n        \n    function createTooltip(marker, pattern, replaceInfo) {\n        var popup = L.popup();\n        popup.setContent('''');\n        marker.bindPopup(popup, {autoClose: false, closeOnClick: false});\n        tooltips.push( {\n            popup: popup,\n            pattern: pattern,\n            replaceInfo: replaceInfo\n        });\n    }\n    \n    function createPolyline(locations, settings) {\n        var polyline = L.polyline(locations, \n                {\n                    color: \"#\" + settings.color,\n                    opacity: settings.strokeOpacity,\n                    weight: settings.strokeWeight\n                }\n            ).addTo(map);\n        return polyline;    \n    }    \n    \n    function arraysEqual(a, b) {\n        if (a === b) return true;\n        if (a === null || b === null) return false;\n        if (a.length != b.length) return false;\n\n        for (var i = 0; i < a.length; ++i) {\n            if (!a[i].equals(b[i])) return false;\n        }\n        return true;\n    }\n    \n    function updateRoute(route, data) {\n        if (route.latIndex > -1 && route.lngIndex > -1) {\n            var latData = data[route.latIndex].data;\n            var lngData = data[route.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var locations = [];\n                for (var i = 0; i < latData.length; i++) {\n                    var lat = latData[i][1];\n                    var lng = lngData[i][1];\n                    var location = L.latLng(lat, lng);\n                    if (i == 0 || !locations[locations.length-1].equals(location)) {\n                        locations.push(location);\n                    }\n                }\n                var markerLocation;\n                if (locations.length > 0) {\n                    markerLocation = locations[locations.length-1];\n                }\n                if (!route.polyline) {\n                    route.polyline = createPolyline(locations, route.settings);\n                    if (markerLocation) {\n                        route.marker = createMarker(markerLocation, route.settings);\n                    }\n                    polylines.push(route.polyline);\n                    return true;\n                } else {\n                    var prevPath = route.polyline.getLatLngs();\n                    if (!prevPath || !arraysEqual(prevPath, locations)) {\n                        route.polyline.setLatLngs(locations);\n                        if (markerLocation) {\n                            if (!route.marker) {\n                                route.marker = createMarker(markerLocation, route.settings);\n                            } else {\n                                route.marker.setLatLng(markerLocation);\n                            }\n                        }\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }    \n    \n    function extendBounds(bounds, polyline) {\n        if (polyline && polyline.getLatLngs()) {\n            bounds.extend(polyline.getBounds());\n        }\n    }\n    \n    function loadRoutes(data) {\n        var bounds = L.latLngBounds();\n        routes = [];\n        var datasourceIndex = -1;\n        var routeSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                routeSettings = routesSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === routeSettings.latKeyName ||\n                dataKey.label === routeSettings.lngKeyName) {\n                var route = routes[datasourceIndex];\n                if (!route) {\n                    route = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: routeSettings\n                    };\n                    routes[datasourceIndex] = route;\n                } else if (route.polyline) {\n                    continue;\n                }\n                if (dataKey.label === routeSettings.latKeyName) {\n                    route.latIndex = i;\n                } else {\n                    route.lngIndex = i;\n                }\n                if (route.latIndex > -1 && route.lngIndex > -1) {\n                    updateRoute(route, data);\n                    if (route.polyline) {\n                        extendBounds(bounds, route.polyline);\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n    \n    function updateRoutes(data) {\n        var routesChanged = false;\n        var bounds = L.latLngBounds();\n        for (var r in routes) {\n            var route = routes[r];\n            routesChanged |= updateRoute(route, data);\n            if (route.polyline) {\n                extendBounds(bounds, route.polyline);\n            }\n        }\n        if (!dontFitMapBounds && routesChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        map.once(''zoomend'', function(event) {\n            var newZoomLevel = map.getZoom();\n            if (dontFitMapBounds && defaultZoomLevel) {\n                newZoomLevel = defaultZoomLevel;\n            }\n            map.setZoom(newZoomLevel, {animate: false});\n            if (!defaultZoomLevel && this.getZoom() > 18) {\n                map.setZoom(18, {animate: false});\n            }\n        });\n        map.fitBounds(bounds, {padding: [50, 50], animate: false});\n    }\n\n    if (map) {\n        if (data) {\n            if (!routes) {\n                loadRoutes(data);\n            } else {\n                updateRoutes(data);\n            }\n        }\n        if (sizeChanged) {\n            map.invalidateSize(true);\n            if (!dontFitMapBounds) {\n                var bounds = L.latLngBounds();\n                for (var p in polylines) {\n                    extendBounds(bounds, polylines[p]);\n                }\n                fitMapBounds(bounds);\n            }            \n        }\n        \n        for (var t in tooltips) {\n            var tooltip = tooltips[t];\n            var text = tooltip.pattern;\n            var replaceInfo = tooltip.replaceInfo;\n            if (replaceInfo && replaceInfo.variables) {\n                for (var v in replaceInfo.variables) {\n                    var variableInfo = replaceInfo.variables[v];\n                    var txtVal = '''';\n                    if (variableInfo.dataKeyIndex > -1) {\n                        var varData = data[variableInfo.dataKeyIndex].data;\n                        if (varData.length > 0) {\n                            var val = varData[varData.length-1][1];\n                            if (isNumber(val)) {\n                                txtVal = padValue(val, variableInfo.valDec, 0);\n                            } else {\n                                txtVal = val;\n                            }\n                        }\n                    }\n                    text = text.split(variableInfo.variable).join(txtVal);\n                }\n            }\n            tooltip.popup.setContent(text);\n        }    \n        \n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Route Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all markers\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"routesSettings\": {\n            \"title\": \"Routes settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Route settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },\n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"tooltipPattern\": {\n                    \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units''  )\",\n                    \"type\": \"string\",\n                    \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  },\n                  \"strokeWeight\": {\n                    \"title\": \"Stroke weight\",\n                    \"type\": \"number\",\n                    \"default\": 2\n                  },\n                  \"strokeOpacity\": {\n                    \"title\": \"Stroke opacity\",\n                    \"type\": \"number\",\n                    \"default\": 1.0\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n    ]\n  },\n  \"form\": [\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"routesSettings\",\n        \"items\": [\n            \"routesSettings[].latKeyName\",\n            \"routesSettings[].lngKeyName\",\n            \"routesSettings[].showLabel\",\n            \"routesSettings[].label\",\n            \"routesSettings[].tooltipPattern\",\n            {\n                \"key\": \"routesSettings[].color\",\n                \"type\": \"color\"\n            },\n            \"routesSettings[].strokeWeight\",\n            \"routesSettings[].strokeOpacity\"\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8950926999078694,\"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];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.2757675428823283,\"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];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\",\"strokeWeight\":4,\"label\":\"First route\",\"color\":\"#3d5afe\",\"strokeOpacity\":1}]},\"title\":\"Route Map - OpenStreetMap\"}"}',
+'Route Map - OpenStreetMap' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
 VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'temperature_radial_gauge_canvas_gauges',
 '{"type":"latest","sizeX":6,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge'');    \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n    gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"Settings\",\n        \"properties\": {\n            \"minValue\": {\n                \"title\": \"Minimum value\",\n                \"type\": \"number\",\n                \"default\": 0\n            },\n            \"maxValue\": {\n                \"title\": \"Maximum value\",\n                \"type\": \"number\",\n                \"default\": 100\n            },\n            \"unitTitle\": {\n                \"title\": \"Unit title\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"showUnitTitle\": {\n                \"title\": \"Show unit title\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"units\": {\n                \"title\": \"Units\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"majorTicksCount\": {\n                \"title\": \"Major ticks count\",\n                \"type\": \"number\",\n                \"default\": null\n            },\n            \"minorTicks\": {\n                \"title\": \"Minor ticks count\",\n                \"type\": \"number\",\n                \"default\": 2\n            },\n            \"valueBox\": {\n                \"title\": \"Show value box\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"valueInt\": {\n                \"title\": \"Digits count for integer part of value\",\n                \"type\": \"number\",\n                \"default\": 3\n            },\n            \"valueDec\": {\n                \"title\": \"Digits count for decimal part of value\",\n                \"type\": \"number\",\n                \"default\": 2\n            },\n            \"defaultColor\": {\n                \"title\": \"Default color\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"colorPlate\": {\n                \"title\": \"Plate color\",\n                \"type\": \"string\",\n                \"default\": \"#fff\"\n            },\n            \"colorMajorTicks\": {\n                \"title\": \"Major ticks color\",\n                \"type\": \"string\",\n                \"default\": \"#444\"\n            },\n            \"colorMinorTicks\": {\n                \"title\": \"Minor ticks color\",\n                \"type\": \"string\",\n                \"default\": \"#666\"\n            },\n            \"colorNeedle\": {\n                \"title\": \"Needle color\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"colorNeedleEnd\": {\n                \"title\": \"Needle color - end gradient\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"colorNeedleShadowUp\": {\n                \"title\": \"Upper half of the needle shadow color\",\n                \"type\": \"string\",\n                \"default\": \"rgba(2,255,255,0.2)\"\n            },\n            \"colorNeedleShadowDown\": {\n                \"title\": \"Drop shadow needle color.\",\n                \"type\": \"string\",\n                \"default\": \"rgba(188,143,143,0.45)\"\n            },\n            \"colorValueBoxRect\": {\n                \"title\": \"Value box rectangle stroke color\",\n                \"type\": \"string\",\n                \"default\": \"#888\"\n            },\n            \"colorValueBoxRectEnd\": {\n                \"title\": \"Value box rectangle stroke color - end gradient\",\n                \"type\": \"string\",\n                \"default\": \"#666\"\n            },\n            \"colorValueBoxBackground\": {\n                \"title\": \"Value box background color\",\n                \"type\": \"string\",\n                \"default\": \"#babab2\"\n            },\n            \"colorValueBoxShadow\": {\n                \"title\": \"Value box shadow color\",\n                \"type\": \"string\",\n                \"default\": \"rgba(0,0,0,1)\"\n            },\n            \"highlights\": {\n                \"title\": \"Highlights\",\n                \"type\": \"array\",\n                \"items\": {\n                  \"title\": \"Highlight\",\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"from\": {\n                      \"title\": \"From\",\n                      \"type\": \"number\"\n                    },\n                    \"to\": {\n                      \"title\": \"To\",\n                      \"type\": \"number\"\n                    },\n                    \"color\": {\n                      \"title\": \"Color\",\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n            },\n            \"highlightsWidth\": {\n                \"title\": \"Highlights width\",\n                \"type\": \"number\",\n                \"default\": 15\n            },\n            \"showBorder\": {\n                \"title\": \"Show border\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"numbersFont\": {\n                \"title\": \"Tick numbers font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 18\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": null\n                    }\n                }\n            },\n            \"titleFont\": {\n                \"title\": \"Title text font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 24\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": \"#888\"\n                    }\n                }\n            },\n            \"unitsFont\": {\n                \"title\": \"Units text font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 22\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": \"#888\"\n                    }\n                }\n            },\n            \"valueFont\": {\n                \"title\": \"Value text font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 40\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": \"#444\"\n                    },\n                    \"shadowColor\": {\n                        \"title\": \"Shadow color\",\n                        \"type\": \"string\",\n                        \"default\": \"rgba(0,0,0,0.3)\"\n                    }\n                }\n            },\n            \"animation\": {\n                \"title\": \"Enable animation\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"animationDuration\": {\n                \"title\": \"Animation duration\",\n                \"type\": \"number\",\n                \"default\": 500\n            },\n            \"animationRule\": {\n                \"title\": \"Animation rule\",\n                \"type\": \"string\",\n                \"default\": \"cycle\"\n            },\n            \"startAngle\": {\n                \"title\": \"Start ticks angle\",\n                \"type\": \"number\",\n                \"default\": 45\n            },\n            \"ticksAngle\": {\n                \"title\": \"Ticks angle\",\n                \"type\": \"number\",\n                \"default\": 270\n            },\n            \"needleCircleSize\": {\n                \"title\": \"Needle circle size\",\n                \"type\": \"number\",\n                \"default\": 10\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"startAngle\",\n        \"ticksAngle\",\n        \"needleCircleSize\",\n        \"minValue\",\n        \"maxValue\",\n        \"unitTitle\",\n        \"showUnitTitle\",\n        \"units\",\n        \"majorTicksCount\",\n        \"minorTicks\",\n        \"valueBox\",\n        \"valueInt\",\n        \"valueDec\",\n        {\n            \"key\": \"defaultColor\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorPlate\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorMajorTicks\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorMinorTicks\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedle\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedleEnd\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedleShadowUp\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedleShadowDown\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxRect\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxRectEnd\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxBackground\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxShadow\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"highlights\",\n            \"items\": [\n                \"highlights[].from\",\n                \"highlights[].to\",\n                {\n                    \"key\": \"highlights[].color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        \"highlightsWidth\",\n        \"showBorder\",\n        {\n            \"key\": \"numbersFont\",\n            \"items\": [\n                \"numbersFont.family\",\n                \"numbersFont.size\",\n                {\n                   \"key\": \"numbersFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"numbersFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"numbersFont.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        {\n            \"key\": \"titleFont\",\n            \"items\": [\n                \"titleFont.family\",\n                \"titleFont.size\",\n                {\n                   \"key\": \"titleFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"titleFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"titleFont.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        {\n            \"key\": \"unitsFont\",\n            \"items\": [\n                \"unitsFont.family\",\n                \"unitsFont.size\",\n                {\n                   \"key\": \"unitsFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"unitsFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"unitsFont.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        {\n            \"key\": \"valueFont\",\n            \"items\": [\n                \"valueFont.family\",\n                \"valueFont.size\",\n                {\n                   \"key\": \"valueFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"valueFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"valueFont.color\",\n                    \"type\": \"color\"\n                },\n                {\n                    \"key\": \"valueFont.shadowColor\",\n                    \"type\": \"color\"\n                }\n            ]\n        },        \n        \"animation\",\n        \"animationDuration\",\n        {\n            \"key\": \"animationRule\",\n            \"type\": \"rc-select\",\n            \"multiple\": false,\n            \"items\": [\n                {\n                    \"value\": \"linear\",\n                    \"label\": \"Linear\"\n                },\n                {\n                    \"value\": \"quad\",\n                    \"label\": \"Quad\"\n                },\n                {\n                    \"value\": \"quint\",\n                    \"label\": \"Quint\"\n                },\n                {\n                    \"value\": \"cycle\",\n                    \"label\": \"Cycle\"\n                },\n                {\n                    \"value\": \"bounce\",\n                    \"label\": \"Bounce\"\n                },\n                {\n                    \"value\": \"elastic\",\n                    \"label\": \"Elastic\"\n                },\n                {\n                    \"value\": \"dequad\",\n                    \"label\": \"Dequad\"\n                },\n                {\n                    \"value\": \"dequint\",\n                    \"label\": \"Dequint\"\n                },\n                {\n                    \"value\": \"decycle\",\n                    \"label\": \"Decycle\"\n                },\n                {\n                    \"value\": \"debounce\",\n                    \"label\": \"Debounce\"\n                },\n                {\n                    \"value\": \"delastic\",\n                    \"label\": \"Delastic\"\n                }\n            ]\n        }\n    ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"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;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge - Canvas Gauges\"}"}',
 'Temperature radial gauge - Canvas Gauges' );
diff --git a/ui/src/app/components/dashboard.scss b/ui/src/app/components/dashboard.scss
index 1b08cdd..d5e4aee 100644
--- a/ui/src/app/components/dashboard.scss
+++ b/ui/src/app/components/dashboard.scss
@@ -20,6 +20,7 @@ div.tb-widget {
   height: 100%;
   margin: 0;
   overflow: hidden;
+  outline: none;
   @include transition(all .2s ease-in-out);
 
   .tb-widget-title {
@@ -91,6 +92,7 @@ md-content.tb-dashboard-content {
   left: 0;
   right: 0;
   bottom: 0;
+  outline: none;
 }
 
 .tb-widget-error-container {
diff --git a/ui/src/app/components/datasource.scss b/ui/src/app/components/datasource.scss
index 0b22134..b6196ac 100644
--- a/ui/src/app/components/datasource.scss
+++ b/ui/src/app/components/datasource.scss
@@ -38,6 +38,7 @@
 
 .tb-color-preview {
   content: '';
+  min-width: 24px;
   width: 24px;
   height: 24px;
   border: 2px solid #fff;
@@ -52,3 +53,14 @@
     height: 100%;
   }
 }
+
+.tb-attribute-chip {
+  .tb-chip-label {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .tb-chip-separator {
+    white-space: pre;
+  }
+}
diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html
index e698cc3..7ffd19f 100644
--- a/ui/src/app/components/datasource.tpl.html
+++ b/ui/src/app/components/datasource.tpl.html
@@ -15,7 +15,7 @@
     limitations under the License.
 
 -->
-<section flex layout='row' layout-align="start center" class="tb-datasource">
+<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center" class="tb-datasource">
     <md-input-container style="min-width: 110px;">
         <md-select placeholder="{{ 'datasource.type' | translate }}" required id="datasourceType" ng-model="model.type">
             <md-option ng-repeat="datasourceType in datasourceTypes" value="{{datasourceType}}">
@@ -23,15 +23,15 @@
             </md-option>
         </md-select>
     </md-input-container>
-    <section flex layout='row' layout-align="start center" class="datasource" ng-switch on="model.type">
-        <tb-datasource-func flex style="padding-left: 8px;"
+    <section flex class="datasource" ng-switch on="model.type">
+        <tb-datasource-func flex
                             ng-switch-default
                             ng-model="model"
                             datakey-settings-schema="datakeySettingsSchema"
                             ng-required="model.type === types.datasourceType.function"
                             generate-data-key="generateDataKey({chip: chip, type: type})">
         </tb-datasource-func>
-        <tb-datasource-device flex style="padding-left: 4px; padding-right: 4px;"
+        <tb-datasource-device flex
                               ng-model="model"
                               datakey-settings-schema="datakeySettingsSchema"
                               ng-switch-when="device"
diff --git a/ui/src/app/components/datasource-device.scss b/ui/src/app/components/datasource-device.scss
index 5e01d1f..a584b13 100644
--- a/ui/src/app/components/datasource-device.scss
+++ b/ui/src/app/components/datasource-device.scss
@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+@import '../../scss/constants';
+
 .tb-device-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete {
   .tb-not-found {
     display: block;
@@ -27,3 +30,16 @@
     white-space: normal !important;
   }
 }
+
+tb-datasource-device {
+  @media (min-width: $layout-breakpoint-gt-sm) {
+    padding-left: 4px;
+    padding-right: 4px;
+  }
+  tb-device-alias-select {
+    @media (min-width: $layout-breakpoint-gt-sm) {
+      width: 200px;
+      max-width: 200px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/components/datasource-device.tpl.html b/ui/src/app/components/datasource-device.tpl.html
index c9f4b10..1368cd1 100644
--- a/ui/src/app/components/datasource-device.tpl.html
+++ b/ui/src/app/components/datasource-device.tpl.html
@@ -15,16 +15,16 @@
     limitations under the License.
 
 -->
-<section flex layout='row' layout-align="start center">
-	   <tb-device-alias-select flex="40"
+<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
+	   <tb-device-alias-select
 							  tb-required="true"
 							  device-aliases="deviceAliases"
 							  ng-model="deviceAlias"
 							  on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
 	   </tb-device-alias-select>
-	   <section flex="120" layout='column'>
-		   <section flex layout='row' layout-align="start center">
-			   <md-chips flex style="padding-left: 4px;"
+	   <section flex layout='column'>
+		   <section flex layout='column' layout-align="center" style="padding-left: 4px;">
+			   <md-chips flex
 						 id="timeseries_datakey_chips"
 						 ng-required="true"
 						 ng-model="timeseriesDataKeys" md-autocomplete-snap
@@ -56,14 +56,19 @@
 							</md-not-found>
 					  </md-autocomplete>
 					  <md-chip-template>
-						<div layout="row" layout-align="start center">
+						<div layout="row" layout-align="start center" class="tb-attribute-chip">
 							<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
 								<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
 							</div>
-							<div>
-							  {{$chip.label}}:
-							  <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
-							  <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+							<div layout="row" flex>
+							  <div class="tb-chip-label">
+							  	{{$chip.label}}
+							  </div>
+							  <div class="tb-chip-separator">: </div>
+							  <div class="tb-chip-label">
+								  <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+								  <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+							  </div>
 							</div>
 							<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
 								<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
@@ -71,7 +76,7 @@
 						</div>
 					  </md-chip-template>
 			   </md-chips>
-			   <md-chips flex ng-if="widgetType === types.widgetType.latest.value" style="padding-left: 4px;"
+			   <md-chips flex ng-if="widgetType === types.widgetType.latest.value"
                          id="attribute_datakey_chips"
                          ng-required="true"
                          ng-model="attributeDataKeys" md-autocomplete-snap
@@ -103,19 +108,24 @@
 						    </md-not-found>
 					  </md-autocomplete>
 					  <md-chip-template>
-						<div layout="row" layout-align="start center">
-							<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
-								<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
-							</div>
-							<div>
-							  {{$chip.label}}:
-							  <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
-							  <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
-							</div>
-							<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
-								<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
-							</md-button>
-						</div>
+						  <div layout="row" layout-align="start center" class="tb-attribute-chip">
+							  <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+								  <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+							  </div>
+							  <div layout="row" flex>
+								  <div class="tb-chip-label">
+									  {{$chip.label}}
+								  </div>
+								  <div class="tb-chip-separator">: </div>
+								  <div class="tb-chip-label">
+									  <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+									  <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+								  </div>
+							  </div>
+							  <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+								  <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+							  </md-button>
+						  </div>
 					  </md-chip-template>
 			   </md-chips>
 		   </section>
diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss
index 08dbd4e..84e4150 100644
--- a/ui/src/app/components/datasource-func.scss
+++ b/ui/src/app/components/datasource-func.scss
@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+@import '../../scss/constants';
+
 .tb-func-datakey-autocomplete {
   .tb-not-found {
     display: block;
@@ -27,3 +30,9 @@
     white-space: normal !important;
   }
 }
+
+tb-datasource-func {
+  @media (min-width: $layout-breakpoint-gt-sm) {
+    padding-left: 8px;
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html
index 2b509f5..9dce208 100644
--- a/ui/src/app/components/datasource-func.tpl.html
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -15,8 +15,8 @@
     limitations under the License.
 
 -->
-<section flex layout='column'>
-   <md-chips flex style="padding-left: 4px;"
+<section flex layout='column' style="padding-left: 4px;">
+   <md-chips flex
 			     id="function_datakey_chips"
 	   			 ng-required="true"
 	             ng-model="funcDataKeys" md-autocomplete-snap
@@ -48,18 +48,23 @@
 				    </md-not-found>
 			  </md-autocomplete>
 		      <md-chip-template>
-		      	<div layout="row" layout-align="start center">
-					<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
-						<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
-					</div>
-			        <div>
-			          {{$chip.label}}: 
-			          <strong>{{$chip.name}}</strong>
-			        </div>
-				    <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
-		        		<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
-		      		</md-button>
-		      	</div>
+				  <div layout="row" layout-align="start center" class="tb-attribute-chip">
+					  <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+						  <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+					  </div>
+					  <div layout="row" flex>
+						  <div class="tb-chip-label">
+							  {{$chip.label}}
+						  </div>
+						  <div class="tb-chip-separator">: </div>
+						  <div class="tb-chip-label">
+							  <strong>{{$chip.name}}</strong>
+						  </div>
+					  </div>
+					  <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+						  <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+					  </md-button>
+				  </div>
 		      </md-chip-template>
 	</md-chips>
 	<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index 2dc2b51..4ad2988 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -20,15 +20,28 @@
   font-weight: 400;
   text-transform: uppercase;
   margin: 20px 8px 0 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: inherit;
 }
 
 .tb-details-subtitle {
   font-size: 1.000rem;
   margin: 10px 0;
   opacity: 0.8;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: inherit;
 }
 
 md-sidenav.tb-sidenav-details {
+  .md-toolbar-tools {
+      min-height: 100px;
+      max-height: 120px;
+      height: 100%;
+  }
   width: 100% !important;
   max-width: 100% !important;
   z-index: 59 !important;
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index a5e84e0..eddcc42 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -22,13 +22,12 @@
       layout="column">
       <header>
 	      <md-toolbar class="md-theme-light" ng-style="{'height':headerHeightPx+'px'}">
-	      	<div class="md-toolbar-tools">
-	      		<div class="md-toolbar-tools" layout="column" layout-align="start start">
+	      	<div class="md-toolbar-tools" layout="row">
+	      		<div flex class="md-toolbar-tools" layout="column" layout-align="start start">
 		        	<span class="tb-details-title">{{headerTitle}}</span>
 		        	<span class="tb-details-subtitle">{{headerSubtitle}}</span>
 					<span style="width: 100%;" ng-transclude="headerPane"></span>
 	        	</div>
-	        	<span flex></span>
 				<div ng-transclude="detailsButtons"></div>
 	        	<md-button class="md-icon-button" ng-click="closeDetails()">
 	        		<md-icon aria-label="close" class="material-icons">close</md-icon>
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index 896e1e8..b76bad6 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -25,7 +25,7 @@
                     <input name="title" ng-model="title">
                 </md-input-container>
                 <span translate>widget-config.general-settings</span>
-                <div layout="row" layout-align="start center">
+                <div layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
                     <div layout="row" layout-padding>
                         <md-checkbox flex aria-label="{{ 'widget-config.display-title' | translate }}"
                                      ng-model="showTitle">{{ 'widget-config.display-title' | translate }}
@@ -80,7 +80,7 @@
                                     <div flex layout="row" layout-align="start center"
                                          style="padding: 0 0 0 10px; margin: 5px;">
                                         <span translate style="min-width: 110px;">widget-config.datasource-type</span>
-                                        <span translate flex
+                                        <span hide show-gt-sm translate flex
                                               style="padding-left: 10px;">widget-config.datasource-parameters</span>
                                         <span style="min-width: 40px;"></span>
                                     </div>
diff --git a/ui/src/app/components/widgets-bundle-select.scss b/ui/src/app/components/widgets-bundle-select.scss
index 7b573f2..e5492c6 100644
--- a/ui/src/app/components/widgets-bundle-select.scss
+++ b/ui/src/app/components/widgets-bundle-select.scss
@@ -35,10 +35,12 @@ tb-widgets-bundle-select {
 
 tb-widgets-bundle-select, .tb-widgets-bundle-select {
   .md-text {
+    display: block;
     width: 100%;
   }
   .tb-bundle-item {
-    display: block;
+    display: inline-block;
+    width: 100%;
     span {
       display: inline-block;
       vertical-align: middle;
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 896e7a1..43225f5 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -312,15 +312,21 @@ export default function DashboardController(types, widgetService, userService,
         }
     }
 
+    function isHotKeyAllowed(event) {
+        var target = event.target || event.srcElement;
+        var scope = angular.element(target).scope();
+        return scope && scope.$parent !== $rootScope;
+    }
+
     function initHotKeys() {
         $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
             hotkeys.bindTo($scope)
                 .add({
                     combo: 'ctrl+c',
                     description: translations['action.copy'],
-                    allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
                     callback: function (event) {
-                        if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
                             var widget = vm.dashboardContainer.getSelectedWidget();
                             if (widget) {
                                 event.preventDefault();
@@ -332,9 +338,9 @@ export default function DashboardController(types, widgetService, userService,
                 .add({
                     combo: 'ctrl+v',
                     description: translations['action.paste'],
-                    allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
                     callback: function (event) {
-                        if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
                             if (itembuffer.hasWidget()) {
                                 event.preventDefault();
                                 pasteWidget(event);
@@ -345,9 +351,9 @@ export default function DashboardController(types, widgetService, userService,
                 .add({
                     combo: 'ctrl+x',
                     description: translations['action.delete'],
-                    allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
                     callback: function (event) {
-                        if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+                        if (isHotKeyAllowed(event) &&
+                            vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
                             var widget = vm.dashboardContainer.getSelectedWidget();
                             if (widget) {
                                 event.preventDefault();
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index ca46b97..4d9c21e 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-content flex tb-expand-fullscreen="vm.widgetEditMode" hide-expand-button="vm.widgetEditMode">
-    <section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap
+    <!--section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap
              class="tb-header-buttons tb-top-header-buttons md-fab" ng-style="{'right': '50px'}">
         <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit" ng-disabled="loading"
                    class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"
@@ -37,7 +37,7 @@
             <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
                         options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
         </md-button>
-    </section>
+    </section-->
     <section ng-show="!loading && vm.noData()" layout-align="center center"
              ng-class="{'tb-padded' : !vm.widgetEditMode}"
              style="text-transform: uppercase; display: flex; z-index: 1;"
@@ -180,8 +180,8 @@
         </div>
     </tb-details-sidenav>
     <!-- </section> -->
-    <section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
-        <md-button ng-disabled="loading" ng-if="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
+    <section layout="row" layout-wrap class="tb-footer-buttons md-fab">
+        <md-button ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
                    class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
                    aria-label="{{ 'dashboard.add-widget' | translate }}">
             <md-tooltip md-direction="top">
@@ -189,5 +189,25 @@
             </md-tooltip>
             <ng-md-icon icon="add"></ng-md-icon>
         </md-button>
+        <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading"
+                   class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   aria-label="{{ 'action.apply' | translate }}"
+                   ng-click="vm.saveDashboard()">
+            <md-tooltip md-direction="top">
+                {{ 'action.apply-changes' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="done"></ng-md-icon>
+        </md-button>
+        <md-button ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode"
+                   ng-if="vm.isTenantAdmin()" ng-disabled="loading"
+                   class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   aria-label="{{ 'action.edit-mode' | translate }}"
+                   ng-click="vm.toggleDashboardEditMode()">
+            <md-tooltip md-direction="top">
+                {{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
+                        options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+        </md-button>
     </section>
 </md-content>
diff --git a/ui/src/app/layout/breadcrumb.tpl.html b/ui/src/app/layout/breadcrumb.tpl.html
index b5ae863..3253974 100644
--- a/ui/src/app/layout/breadcrumb.tpl.html
+++ b/ui/src/app/layout/breadcrumb.tpl.html
@@ -15,7 +15,7 @@
     limitations under the License.
 
 -->
-<div class="tb-breadcrumb">
+<div flex class="tb-breadcrumb" layout="row">
 	<h1 flex hide-gt-sm>{{ steps[steps.length-1].ncyBreadcrumbLabel | breadcrumbLabel }}</h1>
 	<span hide-xs hide-sm ng-repeat="step in steps" ng-switch="$last || !!step.abstract">
 	    <a ng-switch-when="false" href="{{step.ncyBreadcrumbLink}}">
diff --git a/ui/src/app/layout/home.scss b/ui/src/app/layout/home.scss
index f2e4100..f3f9d25 100644
--- a/ui/src/app/layout/home.scss
+++ b/ui/src/app/layout/home.scss
@@ -29,6 +29,11 @@
 .tb-breadcrumb {
   font-size: 18px !important;
   font-weight: 400 !important;
+  h1, a, span {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
   a {
     border: none;
     opacity: 0.75;
diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html
index e5a4c15..f5e1571 100644
--- a/ui/src/app/layout/home.tpl.html
+++ b/ui/src/app/layout/home.tpl.html
@@ -39,7 +39,7 @@
 
   <div flex layout="column" tabIndex="-1" role="main">
     <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
-    	<div flex class="md-toolbar-tools">
+    	<div layout="row" flex class="md-toolbar-tools">
 		      <md-button id="main" hide-gt-sm
 		      		class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
 		      		<md-icon aria-label="{{ 'home.menu' | translate }}" class="material-icons">menu</md-icon>
@@ -47,7 +47,7 @@
 	          <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="searchConfig.showSearch = !searchConfig.showSearch" ng-class="{'tb-invisible': !vm.displaySearchMode()}" >
 		      	  <md-icon aria-label="{{ 'action.back' | translate }}" class="material-icons">arrow_back</md-icon>
 	          </md-button>		    
-			  <div flex ng-show="!vm.displaySearchMode()" tb-no-animate flex class="md-toolbar-tools">
+			  <div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools">
 				  <span ng-cloak ncy-breadcrumb></span>
 			  </div>
 			  <md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>