"use strict";
/**
* used to handle everything related to the detailed view
* handles Sigma and Fabric instances and their interplay
*/
class DetailView {
/**
* creates a new detail view with given settings
*/
constructor() {
this.selectionCanvas = undefined;
this.sigInst = undefined;
this.selectionBoxArr = [];
this.min_val = 0;
this.max_val = 500;
this.x_axis = undefined;
this.y_axis = undefined;
this.scalingParamMap = new Map();
this.showAllEdges = false;
}
/**
* create new sigma instance
* at startup do not show edges do prevent cluttering
*
* @param container_id id of div where detail graph should be displayed
* @param panel_id id of the bootstrap panel wrapping the container
* @param use_web_gl if true, webgl is used for rendering (keep in mind that less functionality is supported)
*/
initSigma(container_id, panel_id, use_web_gl) {
let _self = this;
if (!use_web_gl) {
sigma.canvas.edges.def = sigma.canvas.edges.curvedArrow;
}
this.sigInst = new sigma({
container:container_id,
renderer: {
container:document.getElementById(container_id),
type: (use_web_gl ? 'webgl' : 'canvas')
},
settings: {
minNodeSize: 1,
maxNodeSize: 1,
minArrowSize: 4,
zoomMin: 0.1,
autoRescale: false,
hideEdgesOnMove: true
}
});
// hide/show detail view on collapse click (fix for sigma.js)
$("#" + panel_id + " .collapse-link").click(function(){ $("#" + container_id).toggle();});
// hide/show unmapped edges on eye click
$("#" + panel_id + " .edge_toggle").click(function() {
$("#" + panel_id + " .edge_toggle > i").toggleClass('fa-eye fa-eye-slash');
_self.showAllEdges = ! _self.showAllEdges;
controller.recalcBoxes();
});
// add new selection box on click
$("#" + panel_id + " .add_selection").click(function (){_self.addSelection(_self);});
// change incoming/outgoing edges on click
$("#" + panel_id + " .change_edge_dir").click(function (){
controller.useOutgoingEdges = !controller.useOutgoingEdges;
controller.buildEdgeDict(_self.sigInst.camera.graph.edges());
controller.recalcBoxes();
_self.sigInst.refresh({skipIndexation: true});
});
// key binder
$(document).keydown(function(e) {
switch(e.which) {
case 46: // remove
_self.removeActiveSelection();
break;
default: return;
}
e.preventDefault(); // prevent the default action (scroll / move caret)
});
}
/**
* positions sigma camera to the center of our scale
* and removes scaling
*/
setCameraToCenter() {
let center_pos = (this.max_val - this.min_val) / 2;
this.sigInst.camera.goTo({x:center_pos, y:center_pos, ratio:1});
}
/**
* read in all nodes from the specified JSON-file
* and sets default axis
*
* @param file path to the JSON-file to be loaded
* @param node_id_col column name from file used as ID
* @param x_axis column name from file used as horizontal axis
* @param y_axis column name from file used as vertical axis
* @param callback_function function to be executed as soon as loading finishes
*/
readNodes(file, node_id_col, x_axis, y_axis, callback_function) {
this.x_axis = x_axis;
this.y_axis = y_axis;
let _self = this;
// read nodes using oboe streaming
oboe(file)
.done(function(json){
let rawNodes = json; //load data
_self.initDimensionSwitcher(rawNodes[0]);
// get scaling parameter from raw data (we don't have sigma nodes at this point)
_self.fetchScalingParams(x_axis, rawNodes);
_self.fetchScalingParams(y_axis, rawNodes);
rawNodes.forEach(function(rawNode){
rawNode.x = _self.getScaled(rawNode[x_axis], x_axis);
rawNode.y = _self.getScaled(rawNode[y_axis], y_axis);
rawNode.id = rawNode[node_id_col].toString();
rawNode.size = _self.sigInst.settings("minNodeSize");
_self.sigInst.graph.addNode(rawNode);
});
_self.sigInst.refresh();
_self.initDetailSelectionCanvas();
_self.setCameraToCenter();
callback_function();
});
}
/**
* reads in all edges from the specified JSON-File
* and reports progress to a supplied div
*
* @param file path to the JSON-file to be loaded
* @param progress_div a jquery-element that displays the current progress
* @param edge_id_col column name from file used as ID
* @param source_node_col column name from file used as source element
* @param target_node_col column name from file used as target element
*/
readEdges(file, progress_div, edge_id_col, source_node_col, target_node_col) {
let _self = this;
let rawEdges = [];
let edgeCnt = 0;
let graph = this.sigInst.graph;
progress_div.css("visibility", "visible");
oboe(file)
.node("!.*", function(edge) {
edge.id = edge[edge_id_col];
edge.source = edge[source_node_col];
edge.target = edge[target_node_col];
edge.hidden = !_self.showAllEdges;
rawEdges.push(edge);
graph.addEdge(edge);
if (edgeCnt++ % 987 == 0){
progress_div.text("Loaded " + edgeCnt + " edges...");
}
return oboe.drop;
})
.done(function(json){
progress_div.remove();
controller.buildEdgeDict(_self.sigInst.camera.graph.edges());
_self.sigInst.refresh();
});
}
/**
* inits the fabric canvas which is used to create selection rectangles and
* also inits the transmission of mouse events to the sigma canvas
*/
initDetailSelectionCanvas() {
let _self = this;
let $SIGMA_SCENE = $("#detail_graph_container .sigma-scene");
// create new selection canvas
let copyCanvas = document.createElement("canvas");
copyCanvas.id = "selection_canvas";
copyCanvas.width = $SIGMA_SCENE.width();
copyCanvas.height = $SIGMA_SCENE.height();
$SIGMA_SCENE.parent()[0].appendChild(copyCanvas);
_self.selectionCanvas = new fabric.Canvas("selection_canvas");
//set background to transparent to allow rendering of other layers
_self.selectionCanvas.setBackgroundColor(null);
$("#detail_graph_container .canvas-container").on('mousewheel DOMMouseScroll', function (event) {
wheelHandler(event, _self.sigInst, _self.selectionCanvas);
});
_self.selectionCanvas.on('mouse:down', function(opt) {
var evt = opt.e;
// if no object is selected we try to pan both canvas
if (_self.selectionCanvas.getActiveObject() == null) {
this.isDragging = true;
this.selection = false;
this.lastPosX = evt.clientX;
this.lastPosY = evt.clientY;
downHandler(evt, _self.sigInst);
}
});
_self.selectionCanvas.on('mouse:move', function(opt) {
if (this.isDragging) {
// panning of whole canvas
var e = opt.e;
this.viewportTransform[4] += e.clientX - this.lastPosX;
this.viewportTransform[5] += e.clientY - this.lastPosY;
this.requestRenderAll();
this.lastPosX = e.clientX;
this.lastPosY = e.clientY;
moveHandler(e, _self.sigInst);
} else if (_self.selectionCanvas.getActiveObject() != null) {
// move or transform a selection rectangle
let rect = _self.selectionCanvas.getActiveObject();
let filterIdx = _self.selectionBoxArr.indexOf(rect);
let sigmaCoord = _self.getSigmaCoordinatesFromFabric(rect);
let featureCoord = _self.getFeatureCoordinatesFromSigma(sigmaCoord);
// notify controller
controller.updateFilter(filterIdx, _self.x_axis, {min: featureCoord.x1, max:featureCoord.x2}, false, true);
controller.updateFilter(filterIdx, _self.y_axis, {min: featureCoord.y1, max:featureCoord.y2}, false, true);
}
});
_self.selectionCanvas.on('mouse:up', function(opt) {
this.isDragging = false;
this.selection = true;
upHandler(opt.e, _self.sigInst);
});
$("#detail_graph_container .canvas-container").on('click', function (event) {
clickHandler(event, _self.sigInst);
});
$("#detail_graph_container .canvas-container").on('mouseout', function (event) {
_self.selectionCanvas.isDragging = false;
upHandler(event, _self.sigInst);
});
// update canvas size on screen resize
$(window).resize(function() {
$(".canvas-container").width($SIGMA_SCENE.width());
$(".canvas-container").height($SIGMA_SCENE.height());
$(".upper-canvas").width($SIGMA_SCENE.width());
$(".upper-canvas").height($SIGMA_SCENE.height());
$("#selection_canvas").width($SIGMA_SCENE.width());
$("#selection_canvas").height($SIGMA_SCENE.height());
});
}
/**
* inits detail-view box toggle
* enables users to switch dimensions
* @param node node used to check available dimensions
*/
initDimensionSwitcher(node){
// get all keys
let keys = Object.keys(node);
let _self = this;
for (let i = 0; i < keys.length; i++) {
let val = node[keys[i]];
// only numeric values for detail graph
if (isNaN(val)) {
continue;
}
$("#y_axis_selector ul").append('<li><a id="y_axis_sel_' + keys[i] +'">' + keys[i] + '</a></li>');
$("#y_axis_sel_" + keys[i]).on('click', function (event) {
$("#y_axis_sel_" + keys[i]).addClass("active");
$("#y_axis_sel_" + _self.y_axis).removeClass("active");
_self.setYDimension(keys[i]);
});
$("#x_axis_selector ul").append('<li><a id="x_axis_sel_' + keys[i] +'">' + keys[i] + '</a></li>');
$("#x_axis_sel_" + keys[i]).on('click', function (event) {
$("#x_axis_sel_" + keys[i]).addClass("active");
$("#x_axis_sel_" + _self.x_axis).removeClass("active");
_self.setXDimension(keys[i]);
});
}
// set currently active selection
$("#x_axis_sel_" + _self.x_axis).addClass("active");
$("#y_axis_sel_" + _self.y_axis).addClass("active");
}
/**
* sets the active x-dimension for the detail view
* changes the positions of all nodes and selection boxes
*
* @param feature_name feature to be used as new x-axis
*/
setXDimension(feature_name){
let _self = this;
_self.x_axis = feature_name;
// update all node positions
_self.sigInst.camera.graph.nodes().forEach(function(node){
node.x = _self.getScaled(node[feature_name], feature_name);
});
_self.sigInst.refresh();
// update selection boxes
controller.recalcSelectionBoxes();
}
/**
* sets the active y-dimension for the detail view
* changes the positions of all nodes and selection boxes
*
* @param feature_name feature to be used as new y-axis
*/
setYDimension(feature_name){
let _self = this;
_self.y_axis = feature_name;
// update all node positions
_self.sigInst.camera.graph.nodes().forEach(function(node){
node.y = _self.getScaled(node[feature_name], feature_name);
});
_self.sigInst.refresh();
// update selection boxes
controller.recalcSelectionBoxes();
}
/**
* adds a new selection box
*/
addSelection(view) {
let selectionCnt = view.selectionBoxArr.length;
let rgbVals = colorPool[selectionCnt % colorPool.length];
let colorStr = "rgb("+ rgbVals[0] + "," + rgbVals[1] + "," + rgbVals[2] + ")";
// set initial size relative to current zoom
let sizeFactor = view.sigInst.camera.ratio;
// position new elements within current view
let positionOffset= view.selectionCanvas.calcViewportBoundaries().tl;
// create a rectangle object
let rect = new fabric.Rect({
left: positionOffset.x + 150 * sizeFactor,
top: positionOffset.y + 150 * sizeFactor,
fill: 'transparent',
stroke: colorStr,
opacity: 0.75,
hasRotatingPoint: false,
width: 200 * sizeFactor,
height: 100 * sizeFactor,
cornerSize: 5,
transparentCorners: true
});
let sigmaCoord = view.getSigmaCoordinatesFromFabric(rect);
let featureCoord = view.getFeatureCoordinatesFromSigma(sigmaCoord);
let filter = new Filter([
{feature:view.x_axis, boundary:{min: featureCoord.x1, max:featureCoord.x2}},
{feature:view.y_axis, boundary:{min: featureCoord.y1, max:featureCoord.y2}}
],
colorStr);
view.selectionCanvas.add(rect);
view.selectionBoxArr.push(rect);
controller.addFilter(filter);
}
/**
* removes the currently selected box in the detail view
* does nothing if no box is selected
*/
removeActiveSelection() {
let activeRectangle = this.selectionCanvas.getActiveObject();
if (activeRectangle != null) {
let idx = this.selectionBoxArr.indexOf(activeRectangle);
if (idx > -1) {
this.selectionBoxArr.splice(idx, 1);
controller.removeFilter(idx);
}
this.selectionCanvas.remove(activeRectangle);
}
}
/**
* retrieves the index of the selection box which is currently selected
* this is the same index as the one of the related filter
*
* @return {*} index of the currently selected filter, or -1 if none is selected
*/
getActiveSelectionIndex() {
let activeRectangle = this.selectionCanvas.getActiveObject();
if (activeRectangle != null) {
return this.selectionBoxArr.indexOf(activeRectangle);
}
return -1;
}
/**
* changes the color for a selection rectangle
*
* @param idx index of the respective selection rectangle
* @param color new color for the selection rectangle
*/
setSelectionColor(idx, color) {
this.selectionBoxArr[idx].stroke = color;
this.selectionBoxArr[idx].dirty = true;
this.selectionCanvas.renderAll();
}
/**
* colors all mapped nodes and edges to the respective color of the filter
*
* @param nodeBoxResult result of filter search [containing 'mapped' & 'unmapped' nodes]
* @param edgeBoxResult result of filter search [containing 'mapped' & 'unmapped' edges]
*/
recalcColoring(nodeBoxResult, edgeBoxResult) {
this.recalcNodeColoring(nodeBoxResult);
// change coloring & refresh graph
this.recalcEdgeColoring(edgeBoxResult, true);
}
/**
* colors all mapped nodes to the respective color of the filter
*
* @param boxResult result of filter search [containing 'mapped' & 'unmapped' nodes]
* @param refresh if true the sigma graph is updated
*/
recalcNodeColoring(boxResult, refresh=false) {
let colorArr = controller.getFilterColors();
let _self = this;
// color all matched nodes
for (let i = 0; i< boxResult.mapped.length; i++) {
let curColor = controller.filterArr[i].markingColor;
boxResult.mapped[i].forEach(function(node) {
node.color = curColor;
});
}
// if we've made a selection color other nodes grey
let nodeColor = (boxResult.mapped.length > 0 ? 'rgb(224,224,224)' : _self.sigInst.settings('defaultNodeColor'));
// color all unmatched nodes
boxResult.unmapped.forEach(function(node) {
node.color = nodeColor;
});
if (refresh) {
_self.sigInst.refresh({skipIndexation: true});
}
}
/**
* colors all mapped edges to the respective color of the filter
*
* @param boxResult result of filter search [containing 'mapped' & 'unmapped' edges]
* @param refresh if true the sigma graph is updated
*/
recalcEdgeColoring(boxResult, refresh=false) {
let colorArr = controller.getFilterColors();
let _self = this;
// color all matched nodes
for (let i = 0; i< boxResult.mapped.length; i++) {
let curColor = controller.filterArr[i].markingColor;
boxResult.mapped[i].forEach(function(edge) {
edge.hidden = false;
edge.color = curColor;
});
}
// if we've made a selection color other edges grey
let edgeColor = (boxResult.mapped.length > 0 ? 'rgb(224,224,224)' : _self.sigInst.settings('defaultNodeColor'));
// hide or mark all unmatched edges
boxResult.unmapped.forEach(function(edge) {
edge.hidden = !_self.showAllEdges;
edge.color = edgeColor;
});
if (refresh) {
_self.sigInst.refresh({skipIndexation: true});
}
}
/**
* Change selection boxes in detail view according to supplied filters
* This can be used to keep selection boxes up to date with filter changes
* from outside
*
* @param idx index of the rectangle to be changed
* @param nodeFilterMap all node filters of the related filter
*/
recalcSelectionBoxes(idx, nodeFilterMap) {
let x_min = -99999,
x_max = 999999,
y_min = -99999,
y_max = 999999;
if (nodeFilterMap.has(this.x_axis)) {
let boundary = nodeFilterMap.get(this.x_axis);
x_min = boundary.min;
x_max = boundary.max;
}
if (nodeFilterMap.has(this.y_axis )) {
let boundary = nodeFilterMap.get(this.y_axis);
y_min = boundary.min;
y_max = boundary.max;
}
let sigmaRectangle = this.getSigmaCoordinatesFromFeature({x1:x_min, x2:x_max, y1:y_min, y2:y_max});
let fabricRectangle = this.getFabricRectangleFromSigmaCoordinates(sigmaRectangle);
// update rectangle size
let recToChange = this.selectionBoxArr[idx];
recToChange.left = fabricRectangle.left;
recToChange.top = fabricRectangle.top;
recToChange.width = fabricRectangle.width;
recToChange.height = fabricRectangle.height;
// tell fabric to redraw border
recToChange.dirty = true;
recToChange.setCoords();
this.selectionCanvas.renderAll();
}
/**
* switches filter order for the supplied two indices
* order of supplied filters is irrelevant
* @param filterIdx1 index of a filter
* @param filterIdx2 index of a filter
*/
switchFilterRectangles(filterIdx1, filterIdx2){
let tempRec = this.selectionBoxArr[filterIdx1];
this.selectionBoxArr[filterIdx1] = this.selectionBoxArr[filterIdx2];
this.selectionBoxArr[filterIdx2] = tempRec;
}
/**
* translate from fabric canvas coordinates to sigma coordinates
*
* @param fbSelectionRectangle
* @returns {{x1: number, x2: number, y1: number, y2: number}}
*/
getSigmaCoordinatesFromFabric(fbSelectionRectangle) {
let sigRectangle = this.sigInst.camera.getRectangle(this.sigInst.renderers[0].width, this.sigInst.renderers[0].height);
let fbRectangle = this.selectionCanvas.calcViewportBoundaries();
let fbWidth = fbRectangle.tr.x - fbRectangle.tl.x,
fbHeight = fbRectangle.bl.y - fbRectangle.tl.y,
sigWidth = sigRectangle.x2 - sigRectangle.x1,
sigHeight = sigRectangle.height;
// calculate proportional distance within fabric space
let x_min_prop = (fbSelectionRectangle.left - fbRectangle.tl.x) / fbWidth,
x_max_prop = (fbSelectionRectangle.left + fbSelectionRectangle.width * fbSelectionRectangle.scaleX - fbRectangle.tl.x) / fbWidth,
y_min_prop = (fbSelectionRectangle.top - fbRectangle.tl.y) / fbHeight,
y_max_prop = (fbSelectionRectangle.top + fbSelectionRectangle.height * fbSelectionRectangle.scaleY - fbRectangle.tl.y) / fbHeight;
// return rectangle fitted to sigma space
return {
x1: sigRectangle.x1 + x_min_prop * sigWidth,
x2: sigRectangle.x1 + x_max_prop * sigWidth,
y1: sigRectangle.y1 + y_min_prop * sigHeight,
y2: sigRectangle.y1 + y_max_prop * sigHeight
};
}
/**
* translate from sigma coordinates to fabric rectangle
*
* @param sigmaCoordinates
* @returns {{x1: number, x2: number, y1: number, y2: number}}
*/
getFabricRectangleFromSigmaCoordinates(sigmaCoordinates) {
let sigRectangle = this.sigInst.camera.getRectangle(this.sigInst.renderers[0].width, this.sigInst.renderers[0].height);
let fbRectangle = this.selectionCanvas.calcViewportBoundaries();
let fbWidth = fbRectangle.tr.x - fbRectangle.tl.x,
fbHeight = fbRectangle.bl.y - fbRectangle.tl.y,
sigWidth = sigRectangle.x2 - sigRectangle.x1,
sigHeight = sigRectangle.height;
// calculate proportional distance within sigma space
let x_min_prop = (sigmaCoordinates.x1 - sigRectangle.x1) / sigWidth,
x_max_prop = (sigmaCoordinates.x2 - sigRectangle.x1) / sigWidth,
y_min_prop = (sigmaCoordinates.y1 - sigRectangle.y1) / sigHeight,
y_max_prop = (sigmaCoordinates.y2 - sigRectangle.y1) / sigHeight;
// return rectangle fitted to zoomed and panned fabric space
return {
left: fbRectangle.tl.x + x_min_prop * fbWidth,
top: fbRectangle.tl.y + y_min_prop * fbHeight,
width: fbRectangle.tl.x + x_max_prop * fbWidth - (fbRectangle.tl.x + x_min_prop * fbWidth),
height: fbRectangle.tl.y + y_max_prop * fbHeight - (fbRectangle.tl.y + y_min_prop * fbHeight)
};
}
/**
* Translates sigma coordinates to feature coordinates by applying un/re-scaling
* @param sigmaRectangle rectangle of sigma coordinates
*
* @returns {{x1: number, x2: number, y1: number, y2: number}}
*/
getFeatureCoordinatesFromSigma(sigmaRectangle) {
let x1 = this.getUnscaled(sigmaRectangle.x1, this.x_axis),
x2 = this.getUnscaled(sigmaRectangle.x2, this.x_axis),
y1 = this.getUnscaled(sigmaRectangle.y1, this.y_axis),
y2 = this.getUnscaled(sigmaRectangle.y2, this.y_axis);
// return min/max as left-most variable does not need to be the biggest (lat/lng)
return {
x1: Math.min(x1, x2),
x2: Math.max(x1, x2),
y1: Math.min(y1, y2),
y2: Math.max(y1, y2)
};
}
/**
* Translates feature coordinates to sigma coordinates by applying scaling
* @param featureRectangle rectangle of feature coordinates
*
* @returns {{x1: number, x2: number, y1: number, y2: number}}
*/
getSigmaCoordinatesFromFeature(featureRectangle) {
let x1 = this.getScaled(featureRectangle.x1, this.x_axis),
x2 = this.getScaled(featureRectangle.x2, this.x_axis),
y1 = this.getScaled(featureRectangle.y1, this.y_axis),
y2 = this.getScaled(featureRectangle.y2, this.y_axis);
// return min/max as left-most variable does not need to be the biggest (lat/lng)
return {
x1: Math.min(x1, x2),
x2: Math.max(x1, x2),
y1: Math.min(y1, y2),
y2: Math.max(y1, y2)
};
}
/**
* fetches min/max values for feature
*
* @param feature feature to be queried
* @param nodes nodes to be checked (optional: if not present the nodes of the sigma instance are used)
* @returns {Object} [min: max:] minima/maxima
*/
fetchScalingParams(feature, nodes){
let _self = this;
if (_self.scalingParamMap.has(feature)) {
return _self.scalingParamMap.get(feature);
} else {
// no nodes supplied -> get from sigma
if (nodes == undefined) {
nodes = _self.sigInst.camera.graph.nodes();
}
// calculate max-min
let max = Math.max.apply(Math, nodes.map(function (o) {
return o[feature];
}));
let min = Math.min.apply(Math, nodes.map(function (o) {
return o[feature];
}));
// cache for consecutive queries
_self.scalingParamMap.set(feature, {min: min, max: max});
return {min: min, max: max};
}
}
/**
* scale value from old interval to interval [DETAIL_MIN_VAL, DETAIL_MAX_VAL]
* @param value original feature-value to be fitted into sigma-space
* @param feature feature for which the value should be scaled
* @returns number position in interval
*/
getScaled(value, feature) {
let scalingVals = this.fetchScalingParams(feature);
// apply standard interval scaling
let scaledVal = this.min_val + (this.max_val-this.min_val)/(scalingVals.max - scalingVals.min) *(value-scalingVals.min);
// latitude is higher at "top" (north pole)
if (feature.toLowerCase().includes("latitude")) {
scaledVal *= -1;
scaledVal += this.max_val;
}
return scaledVal;
}
/**
* revert scaling from [DETAIL_MIN_VAL, DETAIL_MAX_VAL] to old interval
* @param scaledValue value from sigma-space to be converted back to original feature space
* @param feature feature for which the value should be unscaled
* @returns {number}
*/
getUnscaled(scaledValue, feature) {
let scalingVals = this.fetchScalingParams(feature);
// inverse for latitude
if (feature.toLowerCase().includes("latitude")) {
scaledValue -= this.max_val;
scaledValue *= -1;
}
// apply standard interval scaling
return scalingVals.min + (scalingVals.max - scalingVals.min) / (this.max_val - this.min_val) * (scaledValue - this.min_val);;
}
};
/**
* resembles the panel containing all selection entries
* used to handle filter removal/additions/changes
*/
class FilterPanel{
/**
*
* @param selectionPanelId container for the panel
*/
constructor(selectionPanelId) {
this.selectionPanelId = selectionPanelId;
this.cnt = 0;
this.idMap = [];
}
/**
* adds a new filter to the filter panel
*
* @param filterIdx index of the new filter
* @param filterColor color of the new filter
*/
addFilter(filterIdx, filterColor){
let _self = this;
let idx = this.cnt++;
this.idMap[filterIdx] = idx;
let divId = 'selection_entry_' + idx;
let colorPickerId = 'color_picker_' + idx;
let newDiv = "<div class='selection_entry' id='" + divId + "'>";
newDiv += "<div class='selection_mover'><div><a class='selection_up'><i class='fa fa-chevron-up'></i></a></div>" +
"<div class='selection_mover'><a class='selection_down'><i class='fa fa-chevron-down'></i></a></div></div>";
newDiv += "<p>Filter " + (idx + 1) + "</p>";
newDiv += "<div id='" + colorPickerId +"' class='input-group colorpicker-component'>"
newDiv += "<span class='input-group-addon'><i></i></span>";
newDiv += "</div>";
newDiv += "</div>";
$("#"+this.selectionPanelId).append(newDiv);
$("#"+ colorPickerId).colorpicker({
color: filterColor,
useAlpha: false
}).on('changeColor', function (e) {
controller.updateFilterColor(filterIdx, e.color);
});
// add up/move-handler
$("#"+divId + " .selection_up").on('mouseup', function(e){
// fetch currently assigned filter index (this might change bc of remove/move)
let mappedFilterIdx = _self.getFilterIdxFromIdMap(idx);
controller.moveFilter(mappedFilterIdx, true);
});
$("#"+divId + " .selection_down").on('mouseup', function(e){
// fetch currently assigned filter index (this might change bc of remove/move)
let mappedFilterIdx = _self.getFilterIdxFromIdMap(idx);
controller.moveFilter(mappedFilterIdx, false);
});
}
/**
* switches filter panels that are next to each other
* order of supplied filters is irrelevant
* @param filterIdx1 index of a filter
* @param filterIdx2 index of a filter
*/
switchFilterPanels(filterIdx1, filterIdx2){
let panelId1 = this.idMap[filterIdx1];
let panelId2 = this.idMap[filterIdx2];
let newTopPanel = panelId1,
newBottomPanel = panelId2;
// switch in other direction
if (filterIdx1 < filterIdx2) {
newTopPanel = panelId2;
newBottomPanel = panelId1;
}
// switch divs
$("#selection_entry_"+newTopPanel).insertBefore($("#selection_entry_"+newBottomPanel));
// switch idMap entries
this.idMap[filterIdx2] = panelId1;
this.idMap[filterIdx1] = panelId2;
}
/**
* retrieves the set filterId for a given panel id (reverse-lookup)
* @param id id of the panel
* @return {number} index of the filter, or -1 if no such exists
*/
getFilterIdxFromIdMap(id) {
for (let i=0; i < this.idMap.length; i++) {
if (this.idMap[i] == id) {
return i;
}
}
return -1;
}
/**
* removes a filter panel
*
* @param filterIdx index of the panel to be removed
*/
removeFilter(filterIdx) {
let idx = this.idMap[filterIdx];
let divId = 'selection_entry_' + idx;
$("#"+divId).remove();
// all following filters have to be set one position below
this.idMap.splice(filterIdx,1);
}
}