You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1204 lines
29 KiB
1204 lines
29 KiB
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
|
|
/**
|
|
* SVGMap class.
|
|
*
|
|
* Implements vector map rendering functionality.
|
|
*/
|
|
function SVGMap(options) {
|
|
var container,
|
|
layers;
|
|
|
|
this.layers = {};
|
|
this.options = options;
|
|
this.elements = {};
|
|
this.shapes = {};
|
|
this.links = {};
|
|
this.background = null;
|
|
this.container = null;
|
|
this.imageUrl = 'imgstore.php?iconid=';
|
|
this.imageCache = new ImageCache();
|
|
this.canvas = new SVGCanvas(options.canvas, true);
|
|
if (typeof this.options.show_timestamp === 'undefined') {
|
|
this.options.show_timestamp = true;
|
|
}
|
|
|
|
// Extra group for font styles.
|
|
container = this.canvas.add('g', {
|
|
class: 'map-container',
|
|
'font-family': SVGMap.FONTS[9],
|
|
'font-size': '10px'
|
|
});
|
|
|
|
var layers_to_add = [
|
|
// Background.
|
|
{
|
|
type: 'g',
|
|
attributes: {
|
|
class: 'map-background',
|
|
fill: '#' + options.theme.backgroundcolor
|
|
}
|
|
},
|
|
// Grid.
|
|
{
|
|
type: 'g',
|
|
attributes: {
|
|
class: 'map-grid',
|
|
stroke: '#' + options.theme.gridcolor,
|
|
fill: '#' + options.theme.gridcolor,
|
|
'stroke-width': '1',
|
|
'stroke-dasharray': '4,4',
|
|
'shape-rendering': 'crispEdges'
|
|
}
|
|
},
|
|
// Custom shapes.
|
|
{
|
|
type: 'g',
|
|
attributes: {
|
|
class: 'map-shapes'
|
|
}
|
|
},
|
|
// Highlights of elements.
|
|
{
|
|
type: 'g',
|
|
attributes: {
|
|
class: 'map-highlights'
|
|
}
|
|
},
|
|
// Links.
|
|
{
|
|
type: 'g',
|
|
attributes: {
|
|
class: 'map-links'
|
|
}
|
|
},
|
|
// Elements.
|
|
{
|
|
type: 'g',
|
|
attributes: {
|
|
class: 'map-elements'
|
|
}
|
|
}
|
|
];
|
|
|
|
// Marks (timestamp and homepage).
|
|
if (options.show_timestamp) {
|
|
layers_to_add.push({
|
|
type: 'g',
|
|
attributes: {
|
|
class: 'map-marks',
|
|
fill: 'rgba(150, 150, 150, 0.75)',
|
|
'font-size': '8px',
|
|
'shape-rendering': 'crispEdges'
|
|
},
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
attributes: {
|
|
class: 'map-timestamp',
|
|
'text-anchor': 'end',
|
|
x: options.canvas.width - 6,
|
|
y: options.canvas.height - 6
|
|
}
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
layers = container.add(layers_to_add);
|
|
|
|
['background', 'grid', 'shapes', 'highlights', 'links', 'elements', 'marks'].forEach(function (attribute, index) {
|
|
this.layers[attribute] = layers[index];
|
|
}, this);
|
|
|
|
this.layers.background.add('rect', {
|
|
x: 0,
|
|
y: 0,
|
|
width: this.options.canvas.width,
|
|
height: this.options.canvas.height
|
|
});
|
|
|
|
// Render goes first as it is needed for getBBox to work.
|
|
if (this.options.container) {
|
|
this.render(this.options.container);
|
|
}
|
|
|
|
if (options.show_timestamp) {
|
|
var elements = this.canvas.getElementsByAttributes({class: 'map-timestamp'});
|
|
if (elements.length == 0) {
|
|
throw 'timestamp element is missing';
|
|
}
|
|
else {
|
|
this['timestamp'] = elements[0];
|
|
}
|
|
}
|
|
this.update(this.options);
|
|
}
|
|
|
|
// Predefined list of fonts for maps.
|
|
SVGMap.FONTS = [
|
|
'Georgia, serif',
|
|
'"Palatino Linotype", "Book Antiqua", Palatino, serif',
|
|
'"Times New Roman", Times, serif',
|
|
'Arial, Helvetica, sans-serif',
|
|
'"Arial Black", Gadget, sans-serif',
|
|
'"Comic Sans MS", cursive, sans-serif',
|
|
'Impact, Charcoal, sans-serif',
|
|
'"Lucida Sans Unicode", "Lucida Grande", sans-serif',
|
|
'Tahoma, Geneva, sans-serif',
|
|
'"Trebuchet MS", Helvetica, sans-serif',
|
|
'Verdana, Geneva, sans-serif',
|
|
'"Courier New", Courier, monospace',
|
|
'"Lucida Console", Monaco, monospace'
|
|
];
|
|
|
|
// Predefined border types (@see dash-array of SVG) for maps.
|
|
SVGMap.BORDER_TYPES = {
|
|
'0': '',
|
|
'1': 'none',
|
|
'2': '1,2',
|
|
'3': '4,4'
|
|
};
|
|
|
|
/**
|
|
* Convert array of objects to hashmap (object).
|
|
*
|
|
* @param {array} array Array of objects.
|
|
* @param {string} key Object field used to identify object.
|
|
*
|
|
* @return {object} Hashmap.
|
|
*/
|
|
SVGMap.toHashmap = function (array, key) {
|
|
var hashmap = {};
|
|
|
|
array.forEach(function (item) {
|
|
if (typeof item !== 'object' || typeof item[key] === 'undefined') {
|
|
// Skip elements that are not objects.
|
|
return;
|
|
}
|
|
|
|
hashmap[item[key]] = item;
|
|
});
|
|
|
|
return hashmap;
|
|
};
|
|
|
|
/**
|
|
* Get image url.
|
|
*
|
|
* @param {number|string} id Image id.
|
|
*
|
|
* @return {string} Image url.
|
|
*/
|
|
SVGMap.prototype.getImageUrl = function (id) {
|
|
return this.imageUrl + id;
|
|
};
|
|
|
|
/**
|
|
* Get image from image cache.
|
|
*
|
|
* @param {number|string} id Image id.
|
|
*
|
|
* @return {object} Image object or null if image object is not present in cache.
|
|
*/
|
|
SVGMap.prototype.getImage = function (id) {
|
|
if (typeof id !== 'undefined' && typeof this.imageCache.images[id] !== 'undefined') {
|
|
return this.imageCache.images[id];
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Update background image.
|
|
*
|
|
* @param {string} background Background image id.
|
|
*/
|
|
SVGMap.prototype.updateBackground = function (background) {
|
|
var element = null;
|
|
|
|
if (background && background !== '0') {
|
|
if (this.background !== null && background === this.options.background) {
|
|
// Background was not changed.
|
|
return;
|
|
}
|
|
|
|
var image = this.getImage(background);
|
|
|
|
element = this.layers.background.add('image', {
|
|
x: 0,
|
|
y: 0,
|
|
width: image.naturalWidth,
|
|
height: image.naturalHeight,
|
|
'xlink:href': this.getImageUrl(background)
|
|
});
|
|
}
|
|
|
|
if (this.background !== null) {
|
|
this.background.remove();
|
|
}
|
|
|
|
this.background = element;
|
|
};
|
|
|
|
/**
|
|
* Set grid size.
|
|
*
|
|
* @param {number} size Grid size. Setting grid size to 0 turns of the grid.
|
|
*/
|
|
SVGMap.prototype.setGrid = function (size) {
|
|
this.layers.grid.clear();
|
|
|
|
if (size === 0) {
|
|
return;
|
|
}
|
|
|
|
for (var x = size; x < this.options.canvas.width; x += size) {
|
|
this.layers.grid.add('line', {
|
|
'x1': x,
|
|
'y1': 0,
|
|
'x2': x,
|
|
'y2': this.options.canvas.height
|
|
});
|
|
|
|
this.layers.grid.add('text', {
|
|
'x': x + 3,
|
|
'y': 9 + 3,
|
|
'stroke-width': 0
|
|
}, x);
|
|
}
|
|
|
|
for (var y = size; y < this.options.canvas.height; y += size) {
|
|
this.layers.grid.add('line', {
|
|
'x1': 0,
|
|
'y1': y,
|
|
'x2': this.options.canvas.width,
|
|
'y2': y
|
|
});
|
|
|
|
this.layers.grid.add('text', {
|
|
'x': 3,
|
|
'y': y + 12,
|
|
'stroke-width': 0
|
|
}, y);
|
|
}
|
|
|
|
this.layers.grid.add('text', {
|
|
'x': 2,
|
|
'y': 12,
|
|
'stroke-width': 0
|
|
}, 'Y X:');
|
|
};
|
|
|
|
/**
|
|
* Compare objects. * Used to compare map object attributes to determine if attributes were changed.
|
|
*
|
|
* @param {object} source Object to be compared.
|
|
* @param {object} target Object to be compared with.
|
|
*
|
|
* @return {boolean} True if objects attributes are different, false if object attributes are the same.
|
|
*/
|
|
SVGMap.isChanged = function (source, target) {
|
|
if (typeof source !== 'object' || source === null) {
|
|
return true;
|
|
}
|
|
|
|
var keys = Object.keys(target);
|
|
|
|
for (var i = 0; i < keys.length; i++) {
|
|
if (typeof target[keys[i]] === 'object') {
|
|
if (SVGMap.isChanged(source[keys[i]], target[keys[i]])) {
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
if (target[keys[i]] !== source[keys[i]]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Update map objects. Iterate through map objects of specified type and update object attributes.
|
|
*
|
|
* @param {string} type Object type (name of SVGMap class attribute).
|
|
* @param {string} className Class name used to create instance of a new object.
|
|
* @param {object} items Hashmap of map objects.
|
|
* @param {boolean} incremental Update method. If set to true, items are added to the existing set of map objects.
|
|
*/
|
|
SVGMap.prototype.updateItems = function (type, className, items, incremental) {
|
|
var keys = Object.keys(items);
|
|
|
|
if (incremental !== true) {
|
|
Object.keys(this[type]).forEach(function (key) {
|
|
if (keys.indexOf(key) === -1) {
|
|
this[type][key].remove();
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
keys.forEach(function (key) {
|
|
if (typeof this[type][key] !== 'object') {
|
|
this[type][key] = new window[className](this, {});
|
|
}
|
|
|
|
this[type][key].update(items[key]);
|
|
}, this);
|
|
};
|
|
|
|
/**
|
|
* Update ordered map objects.
|
|
*
|
|
* @param {string} type Object type (name of SVGMap class attribute).
|
|
* @param {string} idField Field used to identify objects.
|
|
* @param {string} className Class name used to create instance of a new object.
|
|
* @param {object} items Array of map objects.
|
|
* @param {boolean} incremental Update method. If set to true, items are added to the existing set of map objects.
|
|
*/
|
|
SVGMap.prototype.updateOrderedItems = function (type, idField, className, items, incremental) {
|
|
if (incremental !== true) {
|
|
Object.keys(this[type]).forEach(function (key) {
|
|
if (items.filter(function (item) {
|
|
return item[idField] == key;
|
|
}).length === 0) {
|
|
this[type][key].remove();
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
items.forEach(function (item) {
|
|
if (typeof this[type][item[idField]] !== 'object') {
|
|
this[type][item[idField]] = new window[className](this, {});
|
|
}
|
|
|
|
this[type][item[idField]].update(item);
|
|
}, this);
|
|
};
|
|
|
|
/**
|
|
* Update map objects based on specified options.
|
|
*
|
|
* @param {object} options Map options.
|
|
* @param {boolean} incremental Update method. If set to true, items are added to the existing set of map objects.
|
|
*/
|
|
SVGMap.prototype.update = function (options, incremental) {
|
|
var images = {},
|
|
rules = [
|
|
{
|
|
name: 'elements',
|
|
field: 'selementid'
|
|
},
|
|
{
|
|
name: 'links',
|
|
field: 'linkid'
|
|
}
|
|
];
|
|
|
|
// elements and links are converted into hashmap as order is not important.
|
|
rules.forEach(function (rule) {
|
|
if (typeof options[rule.name] !== 'undefined') {
|
|
options[rule.name] = SVGMap.toHashmap(options[rule.name], rule.field);
|
|
}
|
|
else {
|
|
options[rule.name] = {};
|
|
}
|
|
});
|
|
|
|
// Performs ordering of shapes based on zindex value.
|
|
if (typeof options.shapes === 'undefined') {
|
|
options.shapes = [];
|
|
}
|
|
else {
|
|
options.shapes = options.shapes.sort(function (a,b) {
|
|
return a.zindex - b.zindex;
|
|
});
|
|
}
|
|
|
|
this.options.label_location = options.label_location;
|
|
|
|
// Collect the list of images.
|
|
Object.keys(options.elements).forEach(function (key) {
|
|
var element = options.elements[key];
|
|
if (typeof element.icon !== 'undefined') {
|
|
images[element.icon] = this.getImageUrl(element.icon);
|
|
}
|
|
}, this);
|
|
|
|
if (options.background && options.background !== '0') {
|
|
images[options.background] = this.getImageUrl(options.background);
|
|
}
|
|
|
|
// Resize the canvas and move marks
|
|
if (typeof options.canvas !== 'undefined' && typeof options.canvas.width !== 'undefined'
|
|
&& typeof options.canvas.height !== 'undefined'
|
|
&& this.canvas.resize(options.canvas.width, options.canvas.height)) {
|
|
|
|
this.options.canvas = options.canvas;
|
|
|
|
if (this.container !== null) {
|
|
this.container.style.width = options.canvas.width + 'px';
|
|
this.container.style.height = options.canvas.height + 'px';
|
|
}
|
|
|
|
if (options.show_timestamp) {
|
|
this.timestamp.update({
|
|
x: options.canvas.width,
|
|
y: options.canvas.height - 6
|
|
});
|
|
}
|
|
}
|
|
|
|
// Images are preloaded before update.
|
|
this.imageCache.preload(images, function () {
|
|
// Update is performed after preloading all of the images.
|
|
this.updateItems('elements', 'SVGMapElement', options.elements, incremental);
|
|
this.updateOrderedItems('shapes', 'sysmap_shapeid', 'SVGMapShape', options.shapes, incremental);
|
|
this.updateItems('links', 'SVGMapLink', options.links, incremental);
|
|
this.updateBackground(options.background, incremental);
|
|
|
|
this.options = SVGElement.mergeAttributes(this.options, options);
|
|
}, this);
|
|
|
|
// Timestamp (date on map) is updated.
|
|
if (options.show_timestamp && typeof options.timestamp !== 'undefined') {
|
|
this.timestamp.element.textContent = options.timestamp;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Invalidate items based on type.
|
|
*
|
|
* @param {string} type Object type (name of SVGMap class attribute).
|
|
*/
|
|
SVGMap.prototype.invalidate = function (type) {
|
|
Object.keys(this[type]).forEach(function (key) {
|
|
this[type][key].options = {};
|
|
this[type][key].element.invalidate();
|
|
}, this);
|
|
};
|
|
|
|
/**
|
|
* Render map within container.
|
|
*
|
|
* @param {mixed} container DOM element or jQuery selector.
|
|
*/
|
|
SVGMap.prototype.render = function (container) {
|
|
if (typeof container === 'string') {
|
|
container = jQuery(container)[0];
|
|
}
|
|
this.canvas.render(container);
|
|
this.container = container;
|
|
};
|
|
|
|
/*
|
|
* SVGMapElement class. Implements rendering of map elements (selements).
|
|
*
|
|
* @param {object} map Parent map.
|
|
* @param {object} options Element attributes (match field names in data source).
|
|
*/
|
|
function SVGMapElement(map, options) {
|
|
this.map = map;
|
|
this.options = options;
|
|
this.highlight = null;
|
|
this.image = null;
|
|
this.label = null;
|
|
this.markers = null;
|
|
}
|
|
|
|
// Predefined label positions.
|
|
SVGMapElement.LABEL_POSITION_NONE = null;
|
|
SVGMapElement.LABEL_POSITION_DEFAULT = -1;
|
|
SVGMapElement.LABEL_POSITION_BOTTOM = 0;
|
|
SVGMapElement.LABEL_POSITION_LEFT = 1;
|
|
SVGMapElement.LABEL_POSITION_RIGHT = 2;
|
|
SVGMapElement.LABEL_POSITION_TOP = 3;
|
|
|
|
/**
|
|
* Remove part (item) of an element.
|
|
*
|
|
* @param {string} item Item to be removed.
|
|
*/
|
|
SVGMapElement.prototype.removeItem = function (item) {
|
|
if (this[item] !== null) {
|
|
this[item].remove();
|
|
this[item] = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove element.
|
|
*/
|
|
SVGMapElement.prototype.remove = function () {
|
|
['highlight', 'image', 'label', 'markers'].forEach(function (name) {
|
|
this.removeItem(name);
|
|
}, this);
|
|
|
|
delete this.map.elements[this.options.selementid];
|
|
};
|
|
|
|
/**
|
|
* Update element highlight (shape and markers placed on the background of element).
|
|
*/
|
|
SVGMapElement.prototype.updateHighlight = function() {
|
|
var type = null,
|
|
options = null;
|
|
|
|
if (this.options.latelyChanged) {
|
|
var radius = Math.floor(this.width / 2) + 12,
|
|
markers = [];
|
|
|
|
if (this.options.label_location !== SVGMapElement.LABEL_POSITION_BOTTOM) {
|
|
markers.push({
|
|
type: 'path',
|
|
attributes: {
|
|
d: 'M11, 2.91 L5.87, 8 L11, 13.09 L8.07, 16 L0, 8 L8.07, 0, L11, 2.91',
|
|
transform: 'rotate(90 ' + (this.center.x+8) + ',' + (this.center.y+radius) + ') translate(' +
|
|
(this.center.x+8) + ',' + (this.center.y+radius) + ')'
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.options.label_location !== SVGMapElement.LABEL_POSITION_LEFT) {
|
|
markers.push({
|
|
type: 'path',
|
|
attributes: {
|
|
d: 'M11, 2.91 L5.87, 8 L11, 13.09 L8.07, 16 L0, 8 L8.07, 0, L11, 2.91',
|
|
transform: 'rotate(180 ' + (this.center.x-radius) + ',' + (this.center.y+8) + ') translate(' +
|
|
(this.center.x-radius) + ',' + (this.center.y+8) + ')'
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.options.label_location !== SVGMapElement.LABEL_POSITION_RIGHT) {
|
|
markers.push({
|
|
type: 'path',
|
|
attributes: {
|
|
d: 'M11, 2.91 L5.87, 8 L11, 13.09 L8.07, 16 L0, 8 L8.07, 0, L11, 2.91',
|
|
transform: 'translate(' + (this.center.x+radius) + ',' + (this.center.y-8) + ')'
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.options.label_location !== SVGMapElement.LABEL_POSITION_TOP) {
|
|
markers.push({
|
|
type: 'path',
|
|
attributes: {
|
|
d: 'M11, 2.91 L5.87, 8 L11, 13.09 L8.07, 16 L0, 8 L8.07, 0, L11, 2.91',
|
|
transform: 'rotate(270 ' + (this.center.x-8) + ',' + (this.center.y-radius) + ') translate(' +
|
|
(this.center.x-8) + ',' + (this.center.y-radius) + ')'
|
|
}
|
|
});
|
|
}
|
|
|
|
var element = this.map.layers.highlights.add('g', {
|
|
fill: '#F44336',
|
|
stroke: '#B71C1C'
|
|
}, markers);
|
|
|
|
this.removeItem('markers');
|
|
this.markers = element;
|
|
}
|
|
else {
|
|
this.removeItem('markers');
|
|
}
|
|
|
|
if (typeof this.options.highlight === 'object' && this.options.highlight !== null) {
|
|
if (this.options.highlight.st !== null) {
|
|
type = 'rect';
|
|
options = {
|
|
x: this.x - 2,
|
|
y: this.y - 2,
|
|
width: this.width + 4,
|
|
height: this.height + 4,
|
|
fill: '#' + this.options.highlight.st,
|
|
'fill-opacity': 0.5
|
|
};
|
|
}
|
|
|
|
if (this.options.highlight.hl !== null) {
|
|
type = 'ellipse';
|
|
options = {
|
|
cx: this.center.x,
|
|
cy: this.center.y,
|
|
rx: Math.floor(this.width / 2) + 10,
|
|
ry: Math.floor(this.width / 2) + 10,
|
|
fill: '#' + this.options.highlight.hl
|
|
};
|
|
|
|
if (this.options.highlight.ack === true) {
|
|
options.stroke = '#329632';
|
|
options['stroke-width'] = '4px';
|
|
}
|
|
else {
|
|
options['stroke-width'] = '0';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type !== null) {
|
|
if (this.highlight === null || type !== this.highlight.type) {
|
|
var element = this.map.layers.highlights.add(type, options);
|
|
this.removeItem('highlight');
|
|
this.highlight = element;
|
|
}
|
|
else {
|
|
this.highlight.update(options);
|
|
}
|
|
}
|
|
else {
|
|
this.removeItem('highlight');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update element image. Image should be pre-loaded and placed in cache before calling this method.
|
|
*/
|
|
SVGMapElement.prototype.updateImage = function() {
|
|
var image,
|
|
options = {
|
|
x: this.x,
|
|
y: this.y,
|
|
width: this.width,
|
|
height: this.height
|
|
};
|
|
|
|
if (this.options.actions !== null && this.options.actions !== 'null'
|
|
&& typeof this.options.actions !== 'undefined') {
|
|
var actions = JSON.parse(this.options.actions);
|
|
|
|
// 4 - SYSMAP_ELEMENT_TYPE_IMAGE. Don't draw context menu and hand cursor for image elements with no links.
|
|
if (actions.data.elementtype != 4 || actions.data.urls.length != 0) {
|
|
options['data-menu-popup'] = this.options.actions;
|
|
options['style'] = 'cursor: pointer';
|
|
}
|
|
}
|
|
|
|
if (typeof this.options.icon !== 'undefined') {
|
|
var href = this.map.getImageUrl(this.options.icon);
|
|
// 2 - PERM_READ
|
|
if (2 > this.options.permission) {
|
|
href += '&unavailable=1';
|
|
}
|
|
|
|
if (this.image === null || this.image.attributes['xlink:href'] !== href) {
|
|
options['xlink:href'] = href;
|
|
|
|
var image = this.map.layers.elements.add('image', options);
|
|
this.removeItem('image');
|
|
this.image = image;
|
|
}
|
|
else {
|
|
this.image.update(options);
|
|
}
|
|
}
|
|
else {
|
|
this.removeItem('image');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update element label.
|
|
*/
|
|
SVGMapElement.prototype.updateLabel = function() {
|
|
var x = this.center.x,
|
|
y = this.center.y,
|
|
anchor = {
|
|
horizontal: 'left',
|
|
vertical: 'top'
|
|
};
|
|
|
|
switch (this.options.label_location) {
|
|
case SVGMapElement.LABEL_POSITION_BOTTOM:
|
|
y = this.y + this.height + this.map.canvas.textPadding;
|
|
anchor.horizontal = 'center';
|
|
break;
|
|
|
|
case SVGMapElement.LABEL_POSITION_LEFT:
|
|
x = this.x - this.map.canvas.textPadding;
|
|
anchor.horizontal = 'right';
|
|
anchor.vertical = 'middle';
|
|
break;
|
|
|
|
case SVGMapElement.LABEL_POSITION_RIGHT:
|
|
x = this.x + this.width + this.map.canvas.textPadding;
|
|
anchor.vertical = 'middle';
|
|
break;
|
|
|
|
case SVGMapElement.LABEL_POSITION_TOP:
|
|
y = this.y - this.map.canvas.textPadding;
|
|
anchor.horizontal = 'center';
|
|
anchor.vertical = 'bottom';
|
|
break;
|
|
}
|
|
|
|
if (this.options.label !== null) {
|
|
var element = this.map.layers.elements.add('textarea', {
|
|
'x': x,
|
|
'y': y,
|
|
fill: '#' + this.map.options.theme.textcolor,
|
|
'anchor': anchor,
|
|
background: {
|
|
fill: '#' + this.map.options.theme.backgroundcolor,
|
|
opacity: 0.7
|
|
}
|
|
}, this.options.label);
|
|
|
|
this.removeItem('label');
|
|
this.label = element;
|
|
}
|
|
else {
|
|
this.removeItem('label');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update element (highlight, image and label).
|
|
*
|
|
* @param {object} options Element attributes.
|
|
*/
|
|
SVGMapElement.prototype.update = function(options) {
|
|
var image = this.map.getImage(options.icon);
|
|
|
|
if (image === null) {
|
|
throw "Invalid element configuration!";
|
|
}
|
|
|
|
// Data type normalization.
|
|
['x', 'y', 'width', 'height', 'label_location'].forEach(function(name) {
|
|
if (typeof options[name] !== 'undefined') {
|
|
options[name] = parseInt(options[name]);
|
|
}
|
|
});
|
|
|
|
// Inherit label location from map options.
|
|
if (options.label_location === SVGMapElement.LABEL_POSITION_DEFAULT) {
|
|
options.label_location = parseInt(this.map.options.label_location);
|
|
}
|
|
|
|
if (typeof options.width !== 'undefined' && typeof options.height !== 'undefined') {
|
|
options.x += Math.floor(options.width / 2) - Math.floor(image.naturalWidth / 2);
|
|
options.y += Math.floor(options.height / 2) - Math.floor(image.naturalHeight / 2);
|
|
}
|
|
|
|
options.width = image.naturalWidth;
|
|
options.height = image.naturalHeight;
|
|
|
|
if (options.label === null) {
|
|
options.label_location = SVGMapElement.LABEL_POSITION_NONE;
|
|
}
|
|
|
|
if (SVGMap.isChanged(this.options, options) === false) {
|
|
// No need to update.
|
|
return;
|
|
}
|
|
|
|
this.options = options;
|
|
|
|
if (this.x !== options.x || this.y !== options.y || this.width !== options.width
|
|
|| this.height !== options.height) {
|
|
['x', 'y', 'width', 'height'].forEach(function(name) {
|
|
this[name] = options[name];
|
|
}, this);
|
|
|
|
this.center = {
|
|
x: this.x + Math.floor(this.width / 2),
|
|
y: this.y + Math.floor(this.height / 2)
|
|
};
|
|
}
|
|
|
|
this.updateHighlight();
|
|
this.updateImage();
|
|
this.updateLabel();
|
|
};
|
|
|
|
/**
|
|
* SVGMapLink class. Implements rendering of map links.
|
|
*
|
|
* @param {object} map Parent map.
|
|
* @param {object} options Link attributes.
|
|
*/
|
|
function SVGMapLink(map, options) {
|
|
this.map = map;
|
|
this.options = options;
|
|
this.element = null;
|
|
}
|
|
|
|
// Predefined set of line styles
|
|
SVGMapLink.LINE_STYLE_DEFAULT = 0;
|
|
SVGMapLink.LINE_STYLE_BOLD = 2;
|
|
SVGMapLink.LINE_STYLE_DOTTED = 3;
|
|
SVGMapLink.LINE_STYLE_DASHED = 4;
|
|
|
|
/**
|
|
* Update link.
|
|
*
|
|
* @param {object} options Link attributes (match field names in data source).
|
|
*/
|
|
SVGMapLink.prototype.update = function(options) {
|
|
// Data type normalization.
|
|
options.drawtype = parseInt(options.drawtype);
|
|
options.elements = [this.map.elements[options.selementid1], this.map.elements[options.selementid2]];
|
|
|
|
if (typeof options.elements[0] === 'undefined' || typeof options.elements[1] === 'undefined') {
|
|
var remove = true;
|
|
|
|
if (options.elements[0] === options.elements[1]) {
|
|
// Check if link is from hostgroup to hostgroup.
|
|
options.elements = [
|
|
this.map.shapes['e-' + options.selementid1],
|
|
this.map.shapes['e-' + options.selementid2]
|
|
];
|
|
|
|
remove = (typeof options.elements[0] === 'undefined' || typeof options.elements[1] === 'undefined');
|
|
}
|
|
|
|
if (remove) {
|
|
// Invalid link configuration.
|
|
this.remove();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
options.elements[0] = options.elements[0].center;
|
|
options.elements[1] = options.elements[1].center;
|
|
options.center = {
|
|
x: options.elements[0].x + Math.floor((options.elements[1].x - options.elements[0].x)/2),
|
|
y: options.elements[0].y + Math.floor((options.elements[1].y - options.elements[0].y)/2)
|
|
};
|
|
|
|
if (SVGMap.isChanged(this.options, options) === false) {
|
|
// No need to update.
|
|
return;
|
|
}
|
|
|
|
this.options = options;
|
|
this.remove();
|
|
|
|
var attributes = {
|
|
stroke: '#' + options.color,
|
|
'stroke-width': 1,
|
|
fill: '#' + this.map.options.theme.backgroundcolor
|
|
};
|
|
|
|
switch (options.drawtype) {
|
|
case SVGMapLink.LINE_STYLE_BOLD:
|
|
attributes['stroke-width'] = 2;
|
|
break;
|
|
|
|
case SVGMapLink.LINE_STYLE_DOTTED:
|
|
attributes['stroke-dasharray'] = '1,2';
|
|
break;
|
|
|
|
case SVGMapLink.LINE_STYLE_DASHED:
|
|
attributes['stroke-dasharray'] = '4,4';
|
|
break;
|
|
}
|
|
|
|
this.element = this.map.layers.links.add('g', attributes, [
|
|
{
|
|
type: 'line',
|
|
attributes: {
|
|
x1: options.elements[0].x,
|
|
y1: options.elements[0].y,
|
|
x2: options.elements[1].x,
|
|
y2: options.elements[1].y
|
|
}
|
|
}
|
|
]);
|
|
|
|
this.element.add('textarea', {
|
|
x: options.center.x,
|
|
y: options.center.y,
|
|
fill: '#' + this.map.options.theme.textcolor,
|
|
'font-size': '10px',
|
|
'stroke-width': 0,
|
|
anchor: {
|
|
horizontal: 'center',
|
|
vertical: 'middle'
|
|
},
|
|
background: {
|
|
}
|
|
}, options.label
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Remove link.
|
|
*/
|
|
SVGMapLink.prototype.remove = function () {
|
|
if (this.element !== null) {
|
|
this.element.remove();
|
|
this.element = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* SVGMapShape class. Implements rendering of map shapes.
|
|
*
|
|
* @param {object} map Parent map.
|
|
* @param {object} options Shape attributes.
|
|
*/
|
|
function SVGMapShape(map, options) {
|
|
this.map = map;
|
|
this.options = options;
|
|
this.element = null;
|
|
}
|
|
|
|
// Predefined set of map shape types.
|
|
SVGMapShape.TYPE_RECTANGLE = 0;
|
|
SVGMapShape.TYPE_ELLIPSE = 1;
|
|
SVGMapShape.TYPE_LINE = 2;
|
|
|
|
// Predefined label horizontal alignments.
|
|
SVGMapShape.LABEL_HALIGN_CENTER = 0;
|
|
SVGMapShape.LABEL_HALIGN_LEFT = 1;
|
|
SVGMapShape.LABEL_HALIGN_RIGHT = 2;
|
|
|
|
// Predefined label vertical alignments.
|
|
SVGMapShape.LABEL_VALIGN_MIDDLE = 0;
|
|
SVGMapShape.LABEL_VALIGN_TOP = 1;
|
|
SVGMapShape.LABEL_VALIGN_BOTTOM = 2;
|
|
|
|
/**
|
|
* Update shape.
|
|
*
|
|
* @param {object} options Shape attributes (match field names in data source).
|
|
*/
|
|
SVGMapShape.prototype.update = function(options) {
|
|
if (SVGMap.isChanged(this.options, options) === false) {
|
|
// No need to update.
|
|
return;
|
|
}
|
|
|
|
this.options = options;
|
|
|
|
['x', 'y', 'width', 'height'].forEach(function(name) {
|
|
this[name] = parseInt(options[name]);
|
|
}, this);
|
|
|
|
this.rx = Math.floor(this.width / 2);
|
|
this.ry = Math.floor(this.height / 2);
|
|
|
|
this.center = {
|
|
x: this.x + this.rx,
|
|
y: this.y + this.ry
|
|
};
|
|
|
|
var type,
|
|
element,
|
|
clip = {},
|
|
attributes = {},
|
|
mapping = [
|
|
{
|
|
key: 'background_color',
|
|
value: 'fill'
|
|
},
|
|
{
|
|
key: 'border_color',
|
|
value: 'stroke'
|
|
}
|
|
];
|
|
|
|
mapping.forEach(function(map) {
|
|
if (typeof options[map.key] !== 'undefined' && /[0-9A-F]{6}/g.test(options[map.key].trim())) {
|
|
attributes[map.value] = '#' + options[map.key];
|
|
}
|
|
else {
|
|
attributes[map.value] = 'none';
|
|
}
|
|
}, this);
|
|
|
|
if (typeof options['border_width'] !== 'undefined') {
|
|
attributes['stroke-width'] = parseInt(options['border_width']);
|
|
}
|
|
|
|
if (typeof options['border_type'] !== 'undefined') {
|
|
var border_type = SVGMap.BORDER_TYPES[parseInt(options['border_type'])];
|
|
|
|
if (border_type !== '' && border_type !== 'none' && attributes['stroke-width'] > 1) {
|
|
var parts = border_type.split(',').map(function (value) {
|
|
return parseInt(value);
|
|
});
|
|
|
|
// Make dots round.
|
|
if (parts[0] === 1 && attributes['stroke-width'] > 2) {
|
|
attributes['stroke-linecap'] = 'round';
|
|
}
|
|
|
|
border_type = parts.map(function (part) {
|
|
if (part === 1 && attributes['stroke-width'] > 2) {
|
|
return 1;
|
|
}
|
|
|
|
return part * attributes['stroke-width'];
|
|
}).join(',');
|
|
}
|
|
|
|
if (border_type !== '') {
|
|
attributes['stroke-dasharray'] = border_type;
|
|
}
|
|
else {
|
|
attributes['stroke-width'] = 0;
|
|
}
|
|
}
|
|
|
|
switch (parseInt(options.type)) {
|
|
case SVGMapShape.TYPE_RECTANGLE:
|
|
type = 'rect';
|
|
attributes = SVGElement.mergeAttributes(attributes, {
|
|
x: this.x,
|
|
y: this.y,
|
|
width: this.width,
|
|
height: this.height
|
|
});
|
|
|
|
clip = {
|
|
x: this.x,
|
|
y: this.y,
|
|
width: this.width,
|
|
height: this.height
|
|
};
|
|
break;
|
|
|
|
case SVGMapShape.TYPE_ELLIPSE:
|
|
type = 'ellipse';
|
|
attributes = SVGElement.mergeAttributes(attributes, {
|
|
cx: this.center.x,
|
|
cy: this.center.y,
|
|
rx: this.rx,
|
|
ry: this.ry
|
|
});
|
|
|
|
clip = {
|
|
cx: this.center.x,
|
|
cy: this.center.y,
|
|
rx: this.rx,
|
|
ry: this.ry
|
|
};
|
|
break;
|
|
|
|
case SVGMapShape.TYPE_LINE:
|
|
type = 'line';
|
|
|
|
delete attributes['fill'];
|
|
delete options['text'];
|
|
attributes = SVGElement.mergeAttributes(attributes, {
|
|
x1: this.x,
|
|
y1: this.y,
|
|
x2: this.width,
|
|
y2: this.height
|
|
});
|
|
break;
|
|
|
|
default:
|
|
throw "Invalid shape configuration!";
|
|
}
|
|
|
|
if (typeof options.text === 'undefined' || options.text.trim() === '') {
|
|
element = this.map.layers.shapes.add(type, attributes);
|
|
}
|
|
else {
|
|
element = this.map.layers.shapes.add('g', null, [{
|
|
'type': type,
|
|
'attributes': attributes
|
|
}]);
|
|
|
|
var x = this.center.x,
|
|
y = this.center.y,
|
|
anchor = {
|
|
horizontal: 'center',
|
|
vertical: 'middle'
|
|
};
|
|
|
|
switch (parseInt(options['text_halign'])) {
|
|
case SVGMapShape.LABEL_HALIGN_LEFT:
|
|
x = this.x + this.map.canvas.textPadding;
|
|
anchor.horizontal = 'left';
|
|
break;
|
|
|
|
case SVGMapShape.LABEL_HALIGN_RIGHT:
|
|
x = this.x + this.width - this.map.canvas.textPadding;
|
|
anchor.horizontal = 'right';
|
|
break;
|
|
}
|
|
|
|
switch (parseInt(options['text_valign'])) {
|
|
case SVGMapShape.LABEL_VALIGN_TOP:
|
|
y = this.y + this.map.canvas.textPadding;
|
|
anchor.vertical = 'top';
|
|
break;
|
|
|
|
case SVGMapShape.LABEL_VALIGN_BOTTOM:
|
|
y = this.y + this.height - this.map.canvas.textPadding;
|
|
anchor.vertical = 'bottom';
|
|
break;
|
|
}
|
|
|
|
element.add('textarea', {
|
|
'x': x,
|
|
'y': y,
|
|
fill: '#' + (/[0-9A-F]{6}/g.test(options['font_color'].trim()) ? options['font_color'] : '000000'),
|
|
'font-family': SVGMap.FONTS[parseInt(options.font)],
|
|
'font-size': parseInt(options['font_size']) + 'px',
|
|
'anchor': anchor,
|
|
clip: {
|
|
'type': type,
|
|
'attributes': clip
|
|
},
|
|
'parse-links': true
|
|
}, options.text);
|
|
}
|
|
|
|
this.replace(element);
|
|
};
|
|
|
|
/**
|
|
* Replace shape.
|
|
*
|
|
* @see SVGElement.prototype.replace
|
|
*
|
|
* @param {object} element New shape element.
|
|
*/
|
|
SVGMapShape.prototype.replace = function (element) {
|
|
if (this.element !== null) {
|
|
this.element.replace(element);
|
|
}
|
|
else {
|
|
this.element = element;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove shape.
|
|
*/
|
|
SVGMapShape.prototype.remove = function () {
|
|
if (this.element !== null) {
|
|
delete this.map.shapes[this.options.sysmap_shapeid];
|
|
|
|
this.element.remove();
|
|
this.element = null;
|
|
}
|
|
};
|