blob: 3621780098ba29cb0040fddf94c34b8764aef3de [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/* global define, module, require, exports */
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery',
'd3',
'nf.Storage',
'nf.Common',
'nf.Client',
'nf.CanvasUtils'],
function ($, d3, nfStorage, nfCommon, nfClient, nfCanvasUtils) {
return (nf.Label = factory($, d3, nfStorage, nfCommon, nfClient, nfCanvasUtils));
});
} else if (typeof exports === 'object' && typeof module === 'object') {
module.exports = (nf.Label =
factory(require('jquery'),
require('d3'),
require('nf.Storage'),
require('nf.Common'),
require('nf.Client'),
require('nf.CanvasUtils')));
} else {
nf.Label = factory(root.$,
root.d3,
root.nf.Storage,
root.nf.Common,
root.nf.Client,
root.nf.CanvasUtils);
}
}(this, function ($, d3, nfStorage, nfCommon, nfClient, nfCanvasUtils) {
'use strict';
var nfConnectable;
var nfDraggable;
var nfSelectable;
var nfQuickSelect;
var nfContextMenu;
var dimensions = {
width: 148,
height: 148
};
var MIN_HEIGHT = 24;
var MIN_WIDTH = 64;
// -----------------------------
// labels currently on the graph
// -----------------------------
var labelMap;
// -----------------------------------------------------------
// cache for components that are added/removed from the canvas
// -----------------------------------------------------------
var removedCache;
var addedCache;
// --------------------
// component containers
// --------------------
var labelContainer;
// ---------------------------------
// drag handler for the label points
// ---------------------------------
var labelPointDrag;
// --------------------------
// Snap alignment for label resizing
// --------------------------
var snapAlignmentPixels = 8;
var snapEnabled = true;
// --------------------------
// privately scoped functions
// --------------------------
/**
* Selects the labels elements against the current label map.
*/
var select = function () {
return labelContainer.selectAll('g.label').data(labelMap.values(), function (d) {
return d.id;
});
};
/**
* Renders the labels in the specified selection.
*
* @param {selection} entered The selection of labels to be rendered
* @param {boolean} selected Whether the label should be selected
* @return the entered selection
*/
var renderLabels = function (entered, selected) {
if (entered.empty()) {
return entered;
}
var label = entered.append('g')
.attrs({
'id': function (d) {
return 'id-' + d.id;
},
'class': 'label component'
})
.classed('selected', selected)
.call(nfCanvasUtils.position);
// label border
label.append('rect')
.attrs({
'class': 'border',
'fill': 'transparent',
'stroke': 'transparent'
});
// label
label.append('rect')
.attrs({
'class': 'body',
'filter': 'url(#component-drop-shadow)',
'stroke-width': 0
});
// label value
label.append('text')
.attrs({
'xml:space': 'preserve',
'font-weight': 'bold',
'fill': 'black',
'class': 'label-value'
});
// always support selecting
label.call(nfSelectable.activate).call(nfContextMenu.activate).call(nfQuickSelect.activate);
return label;
};
/**
* Updates the labels in the specified selection.
*
* @param {selection} updated The labels to be updated
*/
var updateLabels = function (updated) {
if (updated.empty()) {
return;
}
// update the border using the configured color
updated.select('rect.border')
.attrs({
'width': function (d) {
return d.dimensions.width;
},
'height': function (d) {
return d.dimensions.height;
}
})
.classed('unauthorized', function (d) {
return d.permissions.canRead === false;
});
// update the body fill using the configured color
updated.select('rect.body')
.attrs({
'width': function (d) {
return d.dimensions.width;
},
'height': function (d) {
return d.dimensions.height;
}
})
.style('fill', function (d) {
if (!d.permissions.canRead) {
return null;
}
var color = nfLabel.defaultColor();
// use the specified color if appropriate
if (nfCommon.isDefinedAndNotNull(d.component.style['background-color'])) {
color = d.component.style['background-color'];
}
return color;
})
.classed('unauthorized', function (d) {
return d.permissions.canRead === false;
});
// go through each label being updated
updated.each(function (d) {
var label = d3.select(this);
// update the component behavior as appropriate
nfCanvasUtils.editable(label, nfConnectable, nfDraggable);
// update the label
var labelText = label.select('text.label-value');
var labelPoint = label.selectAll('rect.labelpoint');
if (d.permissions.canRead) {
// udpate the font size
labelText.attr('font-size', function () {
var fontSize = '12px';
// use the specified color if appropriate
if (nfCommon.isDefinedAndNotNull(d.component.style['font-size'])) {
fontSize = d.component.style['font-size'];
}
return fontSize;
});
// remove the previous label value
labelText.selectAll('tspan').remove();
// parse the lines in this label
var lines = [];
if (nfCommon.isDefinedAndNotNull(d.component.label)) {
lines = d.component.label.split('\n');
} else {
lines.push('');
}
var color = nfLabel.defaultColor();
// use the specified color if appropriate
if (nfCommon.isDefinedAndNotNull(d.component.style['background-color'])) {
color = d.component.style['background-color'];
}
// add label value
$.each(lines, function (i, line) {
labelText.append('tspan')
.attr('x', '0.4em')
.attr('dy', '1.2em')
.text(function () {
return line;
})
.style('fill', function (d) {
return nfCommon.determineContrastColor(
nfCommon.substringAfterLast(
color, '#'));
});
});
// -----------
// labelpoints
// -----------
if (d.permissions.canWrite) {
var pointData = [
{x: d.dimensions.width, y: d.dimensions.height}
];
var points = labelPoint.data(pointData);
// create a point for the end
var pointsEntered = points.enter().append('rect')
.attrs({
'class': 'labelpoint',
'width': 10,
'height': 10
})
.call(labelPointDrag);
// update the midpoints
points.merge(pointsEntered).attr('transform', function (p) {
return 'translate(' + (p.x - 10) + ', ' + (p.y - 10) + ')';
});
// remove old items
points.exit().remove();
}
} else {
// remove the previous label value
labelText.selectAll('tspan').remove();
// remove the label points
labelPoint.remove()
}
});
};
/**
* Removes the labels in the specified selection.
*
* @param {selection} removed The labels to be removed
*/
var removeLabels = function (removed) {
removed.remove();
};
var nfLabel = {
config: {
width: dimensions.width,
height: dimensions.height
},
/**
* Initializes of the Processor handler.
*
* @param nfConnectableRef The nfConnectable module.
* @param nfDraggableRef The nfDraggable module.
* @param nfSelectableRef The nfSelectable module.
* @param nfContextMenuRef The nfContextMenu module.
* @param nfQuickSelectRef The nfQuickSelect module.
*/
init: function (nfConnectableRef, nfDraggableRef, nfSelectableRef, nfContextMenuRef, nfQuickSelectRef) {
nfConnectable = nfConnectableRef;
nfDraggable = nfDraggableRef;
nfSelectable = nfSelectableRef;
nfContextMenu = nfContextMenuRef;
nfQuickSelect = nfQuickSelectRef;
labelMap = d3.map();
removedCache = d3.map();
addedCache = d3.map();
// create the label container
labelContainer = d3.select('#canvas').append('g')
.attrs({
'pointer-events': 'all',
'class': 'labels'
});
// handle bend point drag events
labelPointDrag = d3.drag()
.on('start', function () {
// stop further propagation
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function () {
var label = d3.select(this.parentNode);
var labelData = label.datum();
// update the dimensions and ensure they are still within bounds
// snap between aligned sizes unless the user is holding shift
snapEnabled = !d3.event.sourceEvent.shiftKey;
labelData.dimensions.width = Math.max(MIN_WIDTH, snapEnabled ? (Math.round(d3.event.x/snapAlignmentPixels) * snapAlignmentPixels) : d3.event.x);
labelData.dimensions.height = Math.max(MIN_HEIGHT, snapEnabled ? (Math.round(d3.event.y/snapAlignmentPixels) * snapAlignmentPixels) : d3.event.y);
// redraw this connection
updateLabels(label);
})
.on('end', function () {
var label = d3.select(this.parentNode);
var labelData = label.datum();
// determine if the width has changed
var different = false;
if (nfCommon.isDefinedAndNotNull(labelData.component.width) || labelData.dimensions.width !== labelData.component.width) {
different = true;
}
// determine if the height has changed
if (!different && nfCommon.isDefinedAndNotNull(labelData.component.height) || labelData.dimensions.height !== labelData.component.height) {
different = true;
}
// only save the updated bends if necessary
if (different) {
var labelEntity = {
'revision': nfClient.getRevision(labelData),
'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
'component': {
'id': labelData.id,
'width': labelData.dimensions.width,
'height': labelData.dimensions.height
}
}
$.ajax({
type: 'PUT',
url: labelData.uri,
data: JSON.stringify(labelEntity),
dataType: 'json',
contentType: 'application/json'
}).done(function (response) {
// request was successful, update the entry
nfLabel.set(response);
}).fail(function () {
// determine the previous width
var width = dimensions.width;
if (nfCommon.isDefinedAndNotNull(labelData.component.width)) {
width = labelData.component.width;
}
// determine the previous height
var height = dimensions.height;
if (nfCommon.isDefinedAndNotNull(labelData.component.height)) {
height = labelData.component.height;
}
// restore the previous dimensions
labelData.dimensions = {
width: width,
height: height
};
// refresh the label
label.call(updateLabels);
});
}
// stop further propagation
d3.event.sourceEvent.stopPropagation();
});
},
/**
* Adds the specified label entity.
*
* @param labelEntities The label
* @param options Configuration options
*/
add: function (labelEntities, options) {
var selectAll = false;
if (nfCommon.isDefinedAndNotNull(options)) {
selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll;
}
// get the current time
var now = new Date().getTime();
var add = function (labelEntity) {
addedCache.set(labelEntity.id, now);
// add the label
labelMap.set(labelEntity.id, $.extend({
type: 'Label'
}, labelEntity));
};
// determine how to handle the specified label status
if ($.isArray(labelEntities)) {
$.each(labelEntities, function (_, labelEntity) {
add(labelEntity);
});
} else if (nfCommon.isDefinedAndNotNull(labelEntities)) {
add(labelEntities);
}
// select
var selection = select();
// enter
var entered = renderLabels(selection.enter(), selectAll);
// update
updateLabels(selection.merge(entered));
},
/**
* Populates the graph with the specified labels.
*
* @argument {object | array} labelEntities The labels to add
* @argument {object} options Configuration options
*/
set: function (labelEntities, options) {
var selectAll = false;
var transition = false;
var overrideRevisionCheck = false;
if (nfCommon.isDefinedAndNotNull(options)) {
selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll;
transition = nfCommon.isDefinedAndNotNull(options.transition) ? options.transition : transition;
overrideRevisionCheck = nfCommon.isDefinedAndNotNull(options.overrideRevisionCheck) ? options.overrideRevisionCheck : overrideRevisionCheck;
}
var set = function (proposedLabelEntity) {
var currentLabelEntity = labelMap.get(proposedLabelEntity.id);
// set the processor if appropriate due to revision and wasn't previously removed
if ((nfClient.isNewerRevision(currentLabelEntity, proposedLabelEntity) && !removedCache.has(proposedLabelEntity.id)) || overrideRevisionCheck === true) {
labelMap.set(proposedLabelEntity.id, $.extend({
type: 'Label'
}, proposedLabelEntity));
}
};
if ($.isArray(labelEntities)) {
$.each(labelMap.keys(), function (_, key) {
var currentLabelEntity = labelMap.get(key);
var isPresent = $.grep(labelEntities, function (proposedLabelEntity) {
return proposedLabelEntity.id === currentLabelEntity.id;
});
// if the current label is not present and was not recently added, remove it
if (isPresent.length === 0 && !addedCache.has(key)) {
labelMap.remove(key);
}
});
$.each(labelEntities, function (_, labelEntity) {
set(labelEntity);
});
} else if (nfCommon.isDefinedAndNotNull(labelEntities)) {
set(labelEntities);
}
// select
var selection = select();
// enter
var entered = renderLabels(selection.enter(), selectAll);
// update
var updated = selection.merge(entered);
updated.call(updateLabels).call(nfCanvasUtils.position, transition);
// exit
selection.exit().call(removeLabels);
},
/**
* If the label id is specified it is returned. If no label id
* specified, all labels are returned.
*
* @param {string} id
*/
get: function (id) {
if (nfCommon.isUndefined(id)) {
return labelMap.values();
} else {
return labelMap.get(id);
}
},
/**
* If the label id is specified it is refresh according to the current
* state. If not label id is specified, all labels are refreshed.
*
* @param {string} id Optional
*/
refresh: function (id) {
if (nfCommon.isDefinedAndNotNull(id)) {
d3.select('#id-' + id).call(updateLabels);
} else {
d3.selectAll('g.label').call(updateLabels);
}
},
/**
* Reloads the label state from the server and refreshes the UI.
* If the label is currently unknown, this function just returns.
*
* @param {string} id The label id
*/
reload: function (id) {
if (labelMap.has(id)) {
var labelEntity = labelMap.get(id);
return $.ajax({
type: 'GET',
url: labelEntity.uri,
dataType: 'json'
}).done(function (response) {
nfLabel.set(response);
});
}
},
/**
* Positions the component.
*
* @param {string} id The id
*/
position: function (id) {
d3.select('#id-' + id).call(nfCanvasUtils.position);
},
/**
* Removes the specified label.
*
* @param {array|string} labelIds The label id(s)
*/
remove: function (labelIds) {
var now = new Date().getTime();
if ($.isArray(labelIds)) {
$.each(labelIds, function (_, labelId) {
removedCache.set(labelId, now);
labelMap.remove(labelId);
});
} else {
removedCache.set(labelIds, now);
labelMap.remove(labelIds);
}
// apply the selection and handle all removed labels
select().exit().call(removeLabels);
},
/**
* Removes all label.
*/
removeAll: function () {
nfLabel.remove(labelMap.keys());
},
/**
* Expires the caches up to the specified timestamp.
*
* @param timestamp
*/
expireCaches: function (timestamp) {
var expire = function (cache) {
cache.each(function (entryTimestamp, id) {
if (timestamp > entryTimestamp) {
cache.remove(id);
}
});
};
expire(addedCache);
expire(removedCache);
},
/**
* Returns the default color that should be used when drawing a label.
*/
defaultColor: function () {
return '#fff7d7';
}
};
return nfLabel;
}));