TB-70: Image map widget.

This commit is contained in:
Igor Kulikov 2017-08-03 19:19:22 +03:00
parent c0a8849784
commit 2dcf8d6ca1
5 changed files with 562 additions and 3 deletions

File diff suppressed because one or more lines are too long

View File

@ -81,6 +81,7 @@
"schema-inspector": "^1.6.6",
"split.js": "^1.0.7",
"tinycolor2": "^1.4.1",
"tooltipster": "^4.2.4",
"typeface-roboto": "0.0.22",
"v-accordion": "^1.6.0"
},

View File

@ -0,0 +1,302 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'tooltipster/dist/css/tooltipster.bundle.min.css';
import 'tooltipster/dist/js/tooltipster.bundle.min.js';
import 'tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css';
import './image-map.scss';
const pinShape = '<path id="pin" d="m 12.033721,23.509909 c 0.165665,-3.220958 1.940547,-8.45243 4.512974,-11.745035 1.401507,-1.7940561 2.046337,-3.5425327 2.046337,-4.6032909 0,-3.6844827 -2.951858,-6.67149197 -6.592948,-6.67149197 l -1.68e-4,0 c -3.6412584,0 -6.5929483,2.98700927 -6.5929483,6.67149197 0,1.0607582 0.6448307,2.8092348 2.0463367,4.6032909 2.5724276,3.292605 4.3471416,8.524077 4.5129736,11.745035 l 0.06745,0 z" style="fill:#f2756a;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-opacity:1"/>';
const circleShape = '<circle id="circle" fill-rule="evenodd" cy="6.9234" cx="12" clip-rule="evenodd" r="1.5"/>';
const pinSvg = `<svg class="image-map-pin-image" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">${pinShape}${circleShape}</svg>`;
export default class TbImageMap {
constructor($containerElement, initCallback, imageUrl, posFunction) {
this.tooltips = [];
$containerElement.append('<div id="image-map-container"><div id="image-map"></div></div>');
this.imageMapContainer = angular.element('#image-map-container', $containerElement);
this.imageMap = angular.element('#image-map', $containerElement);
this.aspect = 0;
this.width = 0;
this.height = 0;
this.markers = [];
if (!imageUrl) {
imageUrl = 'data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==';
}
this.imageMap.css({backgroundImage: 'url('+imageUrl+')'});
if (angular.isDefined(posFunction) && posFunction.length > 0) {
try {
this.posFunction = new Function('origXPos, origYPos', posFunction);
} catch (e) {
this.posFunction = null;
}
}
if (!this.posFunction) {
this.posFunction = (origXPos, origYPos) => {return {x: origXPos, y: origYPos}};
}
var imageMap = this;
var testImage = new Image(); // eslint-disable-line no-undef
testImage.onload = function() {
imageMap.aspect = testImage.width / testImage.height;
imageMap.onresize();
if (initCallback) {
setTimeout(initCallback, 0); //eslint-disable-line
}
}
testImage.src = imageUrl;
}
onresize() {
if (this.aspect > 0) {
var width = this.imageMapContainer.width();
if (width > 0) {
var height = width / this.aspect;
var imageMapHeight = this.imageMapContainer.height();
if (imageMapHeight > 0 && height > imageMapHeight) {
height = imageMapHeight;
width = height * this.aspect;
}
if (this.width !== width) {
this.width = width;
this.height = width / this.aspect;
this.imageMap.css({width: this.width, height: this.height});
this.markers.forEach((marker) => {
this.updateMarkerDimensions(marker);
});
}
}
}
}
inited() {
return this.aspect > 0 ? true : false;
}
updateMarkerLabel(marker, settings) {
if (settings.showLabel) {
marker.labelElement.css({color: settings.labelColor});
marker.labelElement.html(`<b>${settings.labelText}</b>`);
}
}
updateMarkerColor(marker, color) {
marker.pinSvgElement.css({fill: color});
}
updateMarkerImage(marker, settings, image, maxSize) {
var testImage = new Image(); // eslint-disable-line no-undef
var imageMap = this;
testImage.onload = function() {
var width;
var height;
var aspect = testImage.width / testImage.height;
if (aspect > 1) {
width = maxSize;
height = maxSize / aspect;
} else {
width = maxSize * aspect;
height = maxSize;
}
var size = Math.max(width, height);
marker.size = size;
if (marker.imgElement) {
marker.imgElement.remove();
}
marker.imgElement = angular.element(`<img src="${image}" aria-label="pin" class="image-map-pin-image"/>`);
var left = (size - width)/2;
var top = (size - height)/2;
marker.imgElement.css({width: width, height: height, left: left, top: top});
marker.pinElement.append(marker.imgElement);
imageMap.updateMarkerDimensions(marker);
}
testImage.src = image;
}
updateMarkerDimensions(marker) {
var pinElement = marker.pinElement;
pinElement.css({width: marker.size, height: marker.size});
var left = marker.x * this.width - marker.size/2;
var top = marker.y * this.height - marker.size;
pinElement.css({left: left, top: top});
}
createMarker(position, settings, onClickListener, markerArgs) {
var marker = {
size: 34,
position: position
};
var pos = this.posFunction(position.x, position.y);
marker.x = pos.x;
marker.y = pos.y;
marker.pinElement = angular.element('<div class="image-map-pin"></div>');
if (settings.showLabel) {
marker.labelElement = angular.element(`<div class="image-map-pin-title"><b>${settings.labelText}</b></div>`);
marker.labelElement.css({color: settings.labelColor});
marker.pinElement.append(marker.labelElement);
}
marker.imgElement = angular.element(pinSvg);
marker.pinSvgElement = marker.imgElement.find('#pin');
marker.pinElement.append(marker.imgElement);
marker.pinSvgElement.css({fill: settings.color});
this.updateMarkerDimensions(marker);
this.imageMap.append(marker.pinElement);
if (settings.useMarkerImage) {
this.updateMarkerImage(marker, settings, settings.markerImage, settings.markerImageSize || 34);
}
if (settings.displayTooltip) {
this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo, markerArgs);
}
if (onClickListener) {
marker.pinElement.on('click', onClickListener);
}
this.markers.push(marker);
return marker;
}
removeMarker(marker) {
var index = this.markers.indexOf(marker);
if (index > -1) {
marker.pinElement.remove();
this.markers.splice(index, 1);
}
}
createTooltip(marker, pattern, replaceInfo, markerArgs) {
var popup = new Popup(marker.pinElement);
popup.setContent('');
this.tooltips.push( {
markerArgs: markerArgs,
popup: popup,
pattern: pattern,
replaceInfo: replaceInfo
});
}
updatePolylineColor(/*polyline, settings, color*/) {
}
createPolyline(/*locations, settings*/) {
}
removePolyline(/*polyline*/) {
}
fitBounds() {
}
createLatLng(x, y) {
return new Position(x, y);
}
extendBoundsWithMarker() {
}
getMarkerPosition(marker) {
return marker.position;
}
setMarkerPosition(marker, position) {
marker.position = position;
var pos = this.posFunction(position.x, position.y);
marker.x = pos.x;
marker.y = pos.y;
this.updateMarkerDimensions(marker);
}
getPolylineLatLngs(/*polyline*/) {
}
setPolylineLatLngs(/*polyline, latLngs*/) {
}
createBounds() {
return {};
}
extendBounds() {
}
invalidateSize() {
this.onresize();
}
getTooltips() {
return this.tooltips;
}
}
class Position {
constructor(x, y) {
this.x = x;
this.y = y;
}
equals(loc) {
return loc && loc.x == this.x && loc.y == this.y;
}
}
class Popup {
constructor(anchor) {
anchor.tooltipster(
{
theme: 'tooltipster-shadow',
delay: 100,
trigger: 'custom',
triggerOpen: {
click: true,
tap: true
},
trackOrigin: true
}
);
this.tooltip = anchor.tooltipster('instance');
var contentElement = angular.element('<div class="image-map-pin-tooltip">' +
'<a class="image-map-pin-tooltip-close-button" id="close" style="outline: none;">×</a>' +
'<div flex id="tooltip-content" layout="column">' +
'</div>' +
'</div>');
var popup = this;
contentElement.find('#close').on('click', function() {
popup.tooltip.close();
});
this.content = contentElement.find('#tooltip-content');
this.tooltip.content(contentElement);
}
setContent(content) {
this.content.html(content);
}
}

View File

@ -0,0 +1,98 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.image-map-pin-tooltip {
pointer-events: all;
padding: 5px;
.image-map-pin-tooltip-close-button {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
padding: 6px 6px 0 0;
border: none;
text-align: center;
width: 20px;
height: 16px;
font: 18px/16px Tahoma, Verdana, sans-serif;
color: #b0b0b0;
text-decoration: none;
font-weight: bold;
background: transparent;
&:hover {
color: #919191;
}
}
#tooltip-content {
line-height: normal;
font-size: 13px;
font-weight: 300;
color: #333;
}
}
#image-map-container {
width: 100%;
height: 100%;
#image-map {
color: rgba(0, 0, 0, 0.870588);
position: relative;
margin: auto;
background: transparent no-repeat scroll 0 0;
background-size: cover;
&.is-pointer {
cursor: pointer !important;
}
.movable {
cursor: move;
}
.image-map-pin {
outline: none;
position: absolute;
background: none;
.image-map-pin-title {
position: relative;
white-space: nowrap;
text-align: center;
line-height: 1.5;
font-size: 12px;
font-weight: 400;
top: -20px;
&:before {
content: "";
margin-left: -100%;
}
&:after {
content: "";
margin-right: -100%;
}
}
.image-map-pin-image {
position: absolute;
pointer-events: none;
top: 0;
bottom: 0;
right: 0;
left: 0;
width: 100%;
height: 100%;
}
}
}
}

View File

@ -18,6 +18,7 @@ import tinycolor from 'tinycolor2';
import TbGoogleMap from './google-map';
import TbOpenStreetMap from './openstreet-map';
import TbImageMap from './image-map';
import {processPattern, arraysEqual, toLabelValueMap, fillPattern, fillPatternWithActions} from './widget-utils';
@ -25,6 +26,7 @@ export default class TbMapWidgetV2 {
constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
var tbMap = this;
this.ctx = ctx;
this.mapProvider = mapProvider;
if (!$element) {
$element = ctx.$container;
}
@ -76,6 +78,8 @@ export default class TbMapWidgetV2 {
this.map = new TbGoogleMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
} else if (mapProvider === 'openstreet-map') {
this.map = new TbOpenStreetMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel);
} else if (mapProvider === 'image-map') {
this.map = new TbImageMap($element, initCallback, settings.mapImageUrl, settings.posFunction);
}
}
@ -109,8 +113,13 @@ export default class TbMapWidgetV2 {
configureLocationsSettings() {
this.locationSettings.latKeyName = this.ctx.settings.latKeyName || 'latitude';
this.locationSettings.lngKeyName = this.ctx.settings.lngKeyName || 'longitude';
if (this.mapProvider == 'image-map') {
this.locationSettings.latKeyName = this.ctx.settings.xPosKeyName || 'xPos';
this.locationSettings.lngKeyName = this.ctx.settings.yPosKeyName || 'yPos';
} else {
this.locationSettings.latKeyName = this.ctx.settings.latKeyName || 'latitude';
this.locationSettings.lngKeyName = this.ctx.settings.lngKeyName || 'longitude';
}
this.locationSettings.tooltipPattern = this.ctx.settings.tooltipPattern || "<b>${entityName}</b><br/><br/><b>Latitude:</b> ${"+this.locationSettings.latKeyName+":7}<br/><b>Longitude:</b> ${"+this.locationSettings.lngKeyName+":7}";
@ -436,6 +445,8 @@ export default class TbMapWidgetV2 {
schema = angular.copy(googleMapSettingsSchema);
} else if (mapProvider === 'openstreet-map') {
schema = angular.copy(openstreetMapSettingsSchema);
} else if (mapProvider === 'image-map') {
return imageMapSettingsSchema;
}
angular.merge(schema.schema.properties, commonMapSettingsSchema.schema.properties);
schema.schema.required = schema.schema.required.concat(commonMapSettingsSchema.schema.required);
@ -678,3 +689,134 @@ const routeMapSettingsSchema =
"strokeOpacity"
]
};
const imageMapSettingsSchema =
{
"schema":{
"title":"Image Map Configuration",
"type":"object",
"properties":{
"mapImageUrl": {
"title": "Image map background",
"type": "string",
"default": "data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=="
},
"xPosKeyName":{
"title":"X position key name",
"type":"string",
"default":"xPos"
},
"yPosKeyName":{
"title":"Y position key name",
"type":"string",
"default":"yPos"
},
"showLabel":{
"title":"Show label",
"type":"boolean",
"default":true
},
"label":{
"title":"Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
"type":"string",
"default":"${entityName}"
},
"tooltipPattern":{
"title":"Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
"type":"string",
"default":"<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}"
},
"color":{
"title":"Color",
"type":"string"
},
"posFunction":{
"title":"Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each",
"type":"string",
"default": "return {x: origXPos, y: origYPos};"
},
"useColorFunction":{
"title":"Use color function",
"type":"boolean",
"default":false
},
"colorFunction":{
"title":"Color function: f(data, dsData, dsIndex)",
"type":"string"
},
"markerImage":{
"title":"Custom marker image",
"type":"string"
},
"markerImageSize":{
"title":"Custom marker image size (px)",
"type":"number",
"default":34
},
"useMarkerImageFunction":{
"title":"Use marker image function",
"type":"boolean",
"default":false
},
"markerImageFunction":{
"title":"Marker image function: f(data, images, dsData, dsIndex)",
"type":"string"
},
"markerImages":{
"title":"Marker images",
"type":"array",
"items":{
"title":"Marker image",
"type":"string"
}
}
},
"required":["mapImageUrl"]
},
"form":[
{
"key": "mapImageUrl",
"type": "image"
},
"xPosKeyName",
"yPosKeyName",
"showLabel",
"label",
{
"key": "tooltipPattern",
"type": "textarea"
},
{
"key":"color",
"type":"color"
},
{
"key":"posFunction",
"type":"javascript"
},
"useColorFunction",
{
"key":"colorFunction",
"type":"javascript"
},
{
"key":"markerImage",
"type":"image"
},
"markerImageSize",
"useMarkerImageFunction",
{
"key":"markerImageFunction",
"type":"javascript"
},
{
"key":"markerImages",
"items":[
{
"key":"markerImages[]",
"type":"image"
}
]
}
]
};