Files
pathfinder/js/lib/farahey.js
Mark Friedrich 8c8189d205 - new connection "size" flags for wormholes, closed #645, closed #568
- new persistent map zoom level between sessions
- improved wormhole type names. Color codes represent their target systems security level (green → HS, red → C5/6/0.0,…)
- improved JS performance for map updates, signature updates, map zoom, map overlays
- improved "manual" section for connections (size info added)
- upgraded [_farahey_](https://github.com/jsplumb/farahey) js lib `v0.5` → `v1.1.2`
2019-06-16 14:59:22 +02:00

519 lines
25 KiB
JavaScript

;(function() {
"use strict";
var root = this;
var Farahey = root.Farahey = {};
if (typeof exports !== 'undefined') {
exports.Farahey = Farahey;
}
var findInsertionPoint = function(sortedArr, val, comparator) {
var low = 0, high = sortedArr.length;
var mid = -1, c = 0;
while(low < high) {
mid = parseInt((low + high)/2);
c = comparator(sortedArr[mid], val);
if(c < 0) {
low = mid + 1;
}else if(c > 0) {
high = mid;
}else {
return mid;
}
}
return low;
},
geomSupport = root.Biltong,
insertSorted = function(array, value, comparator) {
var ip = findInsertionPoint(array, value, comparator);
array.splice(ip, 0, value);
},
EntryComparator = function(origin, getSize) {
var _origin = origin,
_cache = {},
_get = function(entry) {
if (!_cache[entry[1]]) {
var s = getSize(entry[2]);
_cache[entry[1]] = {
l:entry[0][0],
t:entry[0][1],
w:s[0],
h:s[1],
center:[entry[0][0] + (s[0] / 2), entry[0][1] + (s[1] / 2) ]
};
}
return _cache[entry[1]];
};
this.setOrigin = function(o) {
_origin = o;
_cache = {};
};
this.compare = function(e1, e2) {
var d1 = geomSupport.lineLength(_origin, _get(e1).center),
d2 = geomSupport.lineLength(_origin, _get(e2).center);
return d1 < d2 ? -1 : d1 == d2 ? 0 : 1;
};
};
var _isOnEdge = function(r, axis, dim, v) { return (r[axis] <= v && v <= r[axis] + r[dim]); },
_xAdj = [ function(r1, r2) { return r1.x + r1.w - r2.x; }, function(r1, r2) { return r1.x - (r2.x + r2.w); } ],
_yAdj = [ function(r1, r2) { return r1.y + r1.h - r2.y; }, function(r1, r2) { return r1.y - (r2.y + r2.h); } ],
_adj = [ null, [ _xAdj[0], _yAdj[1] ], [ _xAdj[0], _yAdj[0] ], [ _xAdj[1], _yAdj[0] ], [ _xAdj[1], _yAdj[1] ] ],
_genAdj = function(r1, r2, m, b, s) {
if (isNaN(m)) m = 0;
var y = r2.y + r2.h,
x = (m == Infinity || m == -Infinity) ? r2.x + (r2.w / 2) : (y - b) / m,
theta = Math.atan(m),
rise, hyp, run;
if (_isOnEdge(r2, "x", "w", x)) {
rise = _adj[s][1](r1, r2);
hyp = rise / Math.sin(theta);
run = hyp * Math.cos(theta);
return { left:run, top:rise };
}
else {
run = _adj[s][0](r1, r2);
hyp = run / Math.cos(theta);
rise = hyp * Math.sin(theta);
return { left:run, top:rise };
}
},
/*
* Calculates how far to move r2 from r1 so that it no longer overlaps.
* if origin is supplied, then it means we want r2 to move along a vector joining r2's center to that point.
* otherwise we want it to move along a vector joining the two rectangle centers.
*/
_calculateSpacingAdjustment = Farahey.calculateSpacingAdjustment = function(r1, r2) {
var c1 = r1.center || [ r1.x + (r1.w / 2), r1.y + (r1.h / 2) ],
c2 = r2.center || [ r2.x + (r2.w / 2), r2.y + (r2.h / 2) ],
m = geomSupport.gradient(c1, c2),
s = geomSupport.quadrant(c1, c2),
b = (m == Infinity || m == -Infinity || isNaN(m)) ? 0 : c1[1] - (m * c1[0]);
return _genAdj(r1, r2, m, b, s);
},
// calculate a padded rectangle for the given element with offset & size, and desired padding.
_paddedRectangle = Farahey.paddedRectangle = function(o, s, p) {
return { x:o[0] - p[0], y: o[1] - p[1], w:s[0] + (2 * p[0]), h:s[1] + (2 * p[1]) };
},
_magnetize = function(positionArray, positions, sizes, padding,
constrain, origin, filter,
updateOnStep, stepInterval, stepCallback, iterations,
exclude, excludeFocus)
{
origin = origin || [0,0];
stepCallback = stepCallback || function() { };
iterations = iterations || 2;
var focus = _paddedRectangle(origin, [1,1], padding),
iteration = 1, uncleanRun = true, adjustBy, constrainedAdjustment,
_movedElements = {},
_move = function(id, o, x, y) {
_movedElements[id] = true;
o[0] += x;
o[1] += y;
},
step = function() {
for (var i = 0; i < positionArray.length; i++) {
if (exclude(positionArray[i][1], positionArray[i][2])) {
continue;
}
var o1 = positions[positionArray[i][1]],
oid = positionArray[i][1],
a1 = positionArray[i][2], // angle to node from magnet origin
s1 = sizes[positionArray[i][1]],
// create a rectangle for first element: this encompasses the element and padding on each
//side
r1 = _paddedRectangle(o1, s1, padding);
if (!excludeFocus && filter(positionArray[i][1], positionArray[i][2]) && geomSupport.intersects(focus, r1)) {
adjustBy = _calculateSpacingAdjustment(focus, r1);
constrainedAdjustment = constrain(positionArray[i][1], o1, adjustBy);
_move(oid, o1, constrainedAdjustment.left, constrainedAdjustment.top);
}
// now move others to account for this one, if necessary.
// reset rectangle for node
r1 = _paddedRectangle(o1, s1, padding);
for (var j = 0; j < positionArray.length; j++) {
if (i != j) {
if (exclude(positionArray[j][1], positionArray[j][2])) {
continue;
}
if (filter(positionArray[j][1], positionArray[j][2])) {
var o2 = positions[positionArray[j][1]],
s2 = sizes[positionArray[j][1]],
// create a rectangle for the second element, again by putting padding of the desired
// amount around the bounds of the element.
r2 = _paddedRectangle(o2, s2, padding);
// if the two rectangles intersect then figure out how much to move the second one by.
if (geomSupport.intersects(r1, r2)) {
// TODO (?), instead of moving neither, the other node should move.
uncleanRun = true;
adjustBy = _calculateSpacingAdjustment(r1, r2);
constrainedAdjustment = constrain(positionArray[j][1], o2, adjustBy);
_move(positionArray[j][1], o2, constrainedAdjustment.left, constrainedAdjustment.top);
}
}
}
}
}
if (updateOnStep)
stepCallback();
if (uncleanRun && iteration < iterations) {
uncleanRun = false;
iteration++;
if (updateOnStep) {
window.setTimeout(step, stepInterval);
}
else
step();
}
};
step();
return _movedElements;
};
var _convertElements = function(l) {
if (l == null) return null;
else if (Object.prototype.toString.call(l) === "[object Array]") {
var a = [];
a.push.apply(a, l);
return a;
}
else {
var o = [];
for (var i in l) o.push(l[i]);
}
return o;
};
/**
* Applies repulsive magnetism to a set of elements relative to a given point, with a specified
* amount of padding around the point.
* @class FaraheyInstance
* @constructor
* @param {Object} params Constructor parameters.
* @param {Selector|Element} [params.container] Element that contains the elements to magnetize. Only required if you intend to use the `executeAtEvent` method.
* @param {Function} [params.getContainerPosition] Function that returns the position of the container (as an object of the form `{left:.., top:..}`) when requested. Only required if you intend to use the `executeAtEvent` method.
* @param {Function} params.getPosition A function that takes an element and returns its position. It does not matter to which element this position is computed as long as you remain consistent with this method, `setPosition` and the `origin` property.
* @param {Function} params.setPosition A function that takes an element and position, and sets it. See note about offset parent above.
* @param {Function} params.getSize A function that takes an element and returns its size, in pixels.
* @param {Number[]} [params.padding] Optional padding for x and y directions. Defaults to 20 pixels in each direction.
* @param {Function} [params.constrain] Optional function that takes an id and a proposed amount of movement in each axis, and returns the allowed amount of movement in each axis. You can use this to constrain your elements to a grid, for instance, or a path, etc.
* @param {Number[]} [params.origin] The origin of magnetization, in pixels. Defaults to 0,0. You can also supply this to the `execute` call.
* @param {Selector|String[]|Element[]} params.elements List, or object hash, of elements on which to operate.
* @param {Boolean} [params.executeNow=false] Whether or not to execute the routine immediately.
* @param {Function} [params.filter] Optional function that takes an element id and returns whether or not that element can be moved.
* @param {Boolean} [params.orderByDistanceFromOrigin=false] Whether or not to sort elements first by distance from origin. Can have better results but takes more time.
*/
var FaraheyInstance = function(params) {
var getPosition = params.getPosition,
getSize = params.getSize,
getId = params.getId,
setPosition = params.setPosition,
padding = params.padding || [20, 20],
// expects a { left:.., top:... } object. returns how far it can actually go.
constrain = params.constrain || function(id, current, delta) { return delta; },
positionArray = [],
positions = {},
sizes = {},
elements = _convertElements(params.elements || []),
origin = params.origin || [0,0],
executeNow = params.executeNow,
//minx, miny, maxx, maxy,
getOrigin = this.getOrigin = function() { return origin; },
filter = params.filter || function(_) { return true; },
exclude = params.exclude || function(_) { return false;},
orderByDistanceFromOrigin = params.orderByDistanceFromOrigin,
comparator = new EntryComparator(origin, getSize),
updateOnStep = params.updateOnStep,
stepInterval = params.stepInterval || 350,
originDebugMarker,
debug = params.debug,
createOriginDebugger = function() {
var d = document.createElement("div");
d.style.position = "absolute";
d.style.width = "10px";
d.style.height = "10px";
d.style.backgroundColor = "red";
document.body.appendChild(d);
originDebugMarker = d;
},
_addToPositionArray = function(p) {
if (!orderByDistanceFromOrigin || positionArray.length == 0)
positionArray.push(p);
else {
insertSorted(positionArray, p, comparator.compare);
}
},
_computeExtents = function(els) {
var minx, miny, maxx, maxy;
minx = miny = Infinity;
maxx = maxy = -Infinity;
for (var i = 0; i < els.length; i++) {
var p = getPosition(els[i]),
s = getSize(els[i]),
id = getId(els[i]);
positions[id] = [p.left, p.top];
_addToPositionArray([ [p.left, p.top], id, els[i]]);
sizes[id] = s;
minx = Math.min(minx, p.left);
miny = Math.min(miny, p.top);
maxx = Math.max(maxx, p.left + s[0]);
maxy = Math.max(maxy, p.top + s[1]);
}
return [ minx, maxx, miny, maxy ];
},
_updatePositions = function() {
comparator.setOrigin(origin);
positionArray = []; positions = {}; sizes = {};
return _computeExtents(elements);
},
_run = function(options) {
if (elements.length > 1) {
options = options || {};
var f = options.filter || filter;
var p = options.padding || padding;
var i = options.iterations;
var e = options.exclude || exclude;
var ef = options.excludeFocus;
var _movedElements = _magnetize(positionArray, positions, sizes, p, constrain, origin, f, updateOnStep, stepInterval, _positionElements, i, e, ef);
_positionElements(_movedElements);
}
},
_positionElements = function(_movedElements) {
for (var i = 0; i < elements.length; i++) {
var id = getId(elements[i]);
if (_movedElements[id])
setPosition(elements[i], { left:positions[id][0], top:positions[id][1] });
}
},
setOrigin = function(o) {
if (o != null) {
origin = o;
comparator.setOrigin(o);
}
};
/**
* Runs the magnetize routine.
* @method execute
* @param {Number[]} [o] Optional origin to use. You may have set this in the constructor and do not wish to supply it, or you may be happy with the default of [0,0].
* @param {Function} [options] Options to override defaults.
* @param {Function} [options.filter] Optional function to indicate whether a given element may be moved or not. Returning boolean false indicates it may not.
* @param {Number[]} [options.padding] Optional [x,y] padding values for elements.
* @param {Number} [options.iterations] Optional max number of iterations to run. The greater this number, the more comprehensive the magnetisation,
* but the slower it runs. The default is 2.
* @param {Function} [options.exclude] Optional function to return whether or not a given element should be completely excluded from the magnetisation: it neither
* moves, nor has any bearing on the movement of other elements.
* @param {Boolean} [options.excludeFocus=false] If true, do not pad any elements around the focus point.
*/
this.execute = function(o, options) {
setOrigin(o);
_updatePositions();
_run(options);
};
/**
* Computes the center of all the nodes and then uses that as the magnetization origin when it runs the routine.
* @method executeAtCenter
* @param {Function} [options] Options to override defaults.
* @param {Function} [options.filter] Optional function to indicate whether a given element may be moved or not. Returning boolean false indicates it may not.
* @param {Number[]} [options.padding] Optional [x,y] padding values for elements.
* @param {Number} [options.iterations] Optional max number of iterations to run. The greater this number, the more comprehensive the magnetisation,
* but the slower it runs. The default is 2.
* @param {Function} [options.exclude] Optional function to return whether or not a given element should be completely excluded from the magnetisation: it neither
* moves, nor has any bearing on the movement of other elements.
* @param {Boolean} [options.excludeFocus=false] If true, do not pad any elements around the focus point.
*/
this.executeAtCenter = function(options) {
var extents = _updatePositions();
setOrigin([
(extents[0] + extents[1]) / 2,
(extents[2] + extents[3]) / 2
]);
_run(options);
};
/**
* Runs the magnetize routine using the location of the given event as the origin. To use this
* method you need to have provided a `container`, and a `getContainerPosition` function to the
* constructor.
* @method executeAtEvent
* @param {Event} e Event to get origin location from.
* @param {Function} [options] Options to override defaults.
* @param {Function} [options.filter] Optional function to indicate whether a given element may be moved or not. Returning boolean false indicates it may not.
* @param {Number[]} [options.padding] Optional [x,y] padding values for elements.
* @param {Number} [options.iterations] Optional max number of iterations to run. The greater this number, the more comprehensive the magnetisation,
* but the slower it runs. The default is 2.
* @param {Function} [options.exclude] Optional function to return whether or not a given element should be completely excluded from the magnetisation: it neither
* moves, nor has any bearing on the movement of other elements.
* @param {Boolean} [options.excludeFocus=false] If true, do not pad any elements around the focus point.
*/
this.executeAtEvent = function(e, options) {
var c = params.container,
o = params.getContainerPosition(c),
x = e.pageX - o.left + c.scrollLeft,
y = e.pageY - o.top + c.scrollTop;
if (debug) {
originDebugMarker.style.left = e.pageX + "px";
originDebugMarker.style.top = e.pageY + "px";
}
this.execute([x,y], options);
};
/**
* Sets the current set of elements on which to operate.
* @method setElements
* @param {Object[]|Object} _els List, or object hash, of elements, in whatever format the Magnetizer is setup to use. If you supply an object hash then a list is generated from the hash's values (the keys are ignored).
*/
this.setElements = function(_els) {
elements = _convertElements(_els);
return this;
};
/**
* Adds the given element to the set of elements on which to operate.
* @method addElement
* @param el {Object} Element to add.
* @param {Boolean} [doNotTestForDuplicates=false] If true, we skip the check for duplicates. This makes
* for a much faster call when there are lots of elements, just use it with care.
*/
this.addElement = function(el, doNotTestForDuplicates) {
if (el != null && (doNotTestForDuplicates || elements.indexOf(el) === -1)) {
elements.push(el);
}
return this;
};
/**
* Adds the given elements to the set of elements on which to operate.
* @method addElements
* @param els {Object[]} Elements to add.
* @param {Boolean} [doNotTestForDuplicates=false] If true, we skip the check for duplicates. This makes
* for a much faster call when there are lots of elements, just use it with care.
*/
this.addElements = function(els, doNotTestForDuplicates) {
if (doNotTestForDuplicates) {
Array.prototype.push.apply(elements, els);
}
else {
for (var i = 0; i < els.length; i++) {
this.addElement(els[i]);
}
}
return this;
};
/**
* Gets the list of elements currently being managed.
* @method getElements
*/
this.getElements = function() {
return elements;
};
/**
* Removes the given element from the set of elements on which to operate.
* @method removeElement
* @param el {Object} Element to remove.
*/
this.removeElement = function(el) {
var idx = -1;
for (var i = 0; i < elements.length; i++) {
if (elements[i] == el) {
idx = i; break;
}
}
if (idx != -1) elements.splice(idx, 1);
return this;
};
/**
* Sets the padding to insert between magnetized elements.
* @method setPadding
* @param {Number[]} p Array of padding for each axis.
*/
this.setPadding = function(p) {
padding = p;
};
/**
* Sets the function used to constrain the movement of some element that the magnetizer wishes to relocate.
* The function is given an element ID and an array of [x,y] values, where each value indicates the proposed amount
* of movement in the given axis. The function is expected to return an array of [x,y] that indicates the allowed
* amount of movement in each axis.
* @method setConstrain
* @param {Function} c
*/
this.setConstrain = function(c) {
constrain = c;
};
/**
* Sets the function used to determine whether or not a given element should be considered during the magnetization process.
* @method setFilter
* @param {Function} f Filter function to use. Takes an element ID and returns whether or not that element can be moved.
*/
this.setFilter = function(f) {
filter = f;
};
/**
* Reset the Farahey instance. Use this to avoid memory leaks.
* @method reset
*/
this.reset = function() {
elements.length = 0;
};
if (debug)
createOriginDebugger();
if (executeNow) this.execute();
return this;
};
/**
* Gets a new FaraheyInstance
* @method
* @param {Object} params Method parameters.
* @param {Selector|Element} [params.container] Element that contains the elements to magnetize. Only required if you intend to use the `executeAtEvent` method.
* @param {Function} [params.getContainerPosition] Function that returns the position of the container (as an object of the form `{left:.., top:..}`) when requested. Only required if you intend to use the `executeAtEvent` method.
* @param {Function} params.getPosition A function that takes an element and returns its position. It does not matter to which element this position is computed as long as you remain consistent with this method, `setPosition` and the `origin` property.
* @param {Function} params.setPosition A function that takes an element and position, and sets it. See note about offset parent above.
* @param {Function} params.getSize A function that takes an element and returns its size, in pixels.
* @param {Number[]} [params.padding] Optional padding for x and y directions. Defaults to 20 pixels in each direction.
* @param {Function} [params.constrain] Optional function that takes an id and a proposed amount of movement in each axis, and returns the allowed amount of movement in each axis. You can use this to constrain your elements to a grid, for instance, or a path, etc.
* @param {Number[]} [params.origin] The origin of magnetization, in pixels. Defaults to 0,0. You can also supply this to the `execute` call.
* @param {Selector|String[]|Element[]} params.elements List, or object hash, of elements on which to operate.
* @param {Boolean} [params.executeNow=false] Whether or not to execute the routine immediately.
* @param {Function} [params.filter] Optional function that takes an element id and returns whether or not that element can be moved.
* @param {Boolean} [params.orderByDistanceFromOrigin=false] Whether or not to sort elements first by distance from origin. Can have better results but takes more time.
*/
Farahey.getInstance = function(params) {
return new FaraheyInstance(params);
};
}).call(typeof window !== 'undefined' ? window : this);