400 lines
17 KiB
JavaScript
400 lines
17 KiB
JavaScript
define(() => {
|
|
'use strict';
|
|
|
|
class Position {
|
|
|
|
constructor(config){
|
|
this._defaultConfig = {
|
|
container: null, // parent DOM container element
|
|
center: null, // DOM elements that works as center
|
|
elementClass: 'pf-system', // class for all elements
|
|
defaultGapX: 50,
|
|
defaultGapY: 50,
|
|
gapX: 50, // leave gap between elements (x-axis)
|
|
gapY: 50, // leave gap between elements (y-axis)
|
|
loops: 2, // max loops around "center" for search
|
|
grid: false, // set to [20, 20] to force grid snapping
|
|
newElementWidth: 100, // width for new element
|
|
newElementHeight: 22, // height for new element
|
|
debug: false, // render debug elements
|
|
debugElementClass: 'pf-system-debug' // class for debug elements
|
|
};
|
|
|
|
this._config = Object.assign({}, this._defaultConfig, config);
|
|
|
|
/**
|
|
* convert degree into radial unit
|
|
* @param deg
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
this._degToRad = (deg) => {
|
|
return deg * Math.PI / 180;
|
|
};
|
|
|
|
/**
|
|
* get element dimension/position of a DOM element
|
|
* @param element
|
|
* @returns {*}
|
|
* @private
|
|
*/
|
|
this._getElementDimension = (element) => {
|
|
let dim = null;
|
|
|
|
let left = 0;
|
|
let top = 0;
|
|
let a = 0;
|
|
let b = 0;
|
|
let width = this._config.newElementWidth;
|
|
let height = this._config.newElementHeight;
|
|
|
|
if(Array.isArray(element)){
|
|
// xy coordinates
|
|
let point = [
|
|
element[0] ? parseInt(element[0], 10) : 0,
|
|
element[1] ? parseInt(element[1], 10) : 0
|
|
];
|
|
|
|
if(this._config.grid){
|
|
point = this._transformPointToGrid(point);
|
|
}
|
|
|
|
left = point[0];
|
|
top = point[1];
|
|
a = this._config.gapX;
|
|
b = this._config.gapY;
|
|
}else if(element){
|
|
// DOM element
|
|
left = element.style.left ? parseInt(element.style.left, 10) : 0;
|
|
top = element.style.top ? parseInt(element.style.top, 10) : 0;
|
|
a = parseInt((element.offsetWidth / 2).toString(), 10) + this._config.gapX;
|
|
b = parseInt((element.offsetHeight / 2).toString(), 10) + this._config.gapY;
|
|
width = element.offsetWidth;
|
|
height = element.offsetHeight;
|
|
}
|
|
|
|
// add "gap" to a and b in order to have some space around elements
|
|
return {
|
|
left: left,
|
|
top: top,
|
|
a: a,
|
|
b: b,
|
|
width: width,
|
|
height: height
|
|
};
|
|
};
|
|
|
|
/**
|
|
* get x/y coordinate on an eclipse around a 2D area by a given radial angle
|
|
* @param dim
|
|
* @param angle
|
|
* @returns {*}
|
|
* @private
|
|
*/
|
|
this._getEllipseCoordinates = (dim, angle) => {
|
|
let coordinates = null;
|
|
if(dim){
|
|
angle = this._degToRad(angle);
|
|
coordinates = {
|
|
x: Math.round((dim.a * dim.b) / Math.sqrt(Math.pow(dim.b, 2) + Math.pow(dim.a, 2) * Math.pow(Math.tan(angle), 2) )),
|
|
y: Math.round((dim.a * dim.b) / Math.sqrt(Math.pow(dim.a, 2) + Math.pow(dim.b, 2) / Math.pow(Math.tan(angle), 2) ))
|
|
};
|
|
|
|
// invert coordinate based on quadrant ------------------------------------------------------------
|
|
if( angle > (Math.PI / 2) && angle < (3 * Math.PI / 2) ){
|
|
coordinates.x = coordinates.x * -1;
|
|
}
|
|
|
|
if( angle > Math.PI && angle < (2 * Math.PI) ){
|
|
coordinates.y = coordinates.y * -1;
|
|
}
|
|
}
|
|
return coordinates;
|
|
};
|
|
|
|
/**
|
|
* get dimensions of all surrounding elements
|
|
* @returns {Array}
|
|
* @private
|
|
*/
|
|
this._getAllElementDimensions = () => {
|
|
let dimensions = [];
|
|
let surroundingElements = this._getContainer().getElementsByClassName(this._config.elementClass);
|
|
for(let element of surroundingElements){
|
|
dimensions.push(this._getElementDimension(element));
|
|
}
|
|
return dimensions;
|
|
};
|
|
|
|
/**
|
|
* transform a x/y point into a x/y point that snaps to grid
|
|
* @param point
|
|
* @returns {*}
|
|
* @private
|
|
*/
|
|
this._transformPointToGrid = (point) => {
|
|
point[0] = Math.floor(point[0] / this._config.grid[0]) * this._config.grid[0];
|
|
point[1] = Math.floor(point[1] / this._config.grid[1]) * this._config.grid[1];
|
|
return point;
|
|
};
|
|
|
|
/**
|
|
* Transform a x/y coordinate into a 2D element with width/height
|
|
* @param centerDimension
|
|
* @param coordinate
|
|
* @returns {*}
|
|
* @private
|
|
*/
|
|
this._transformCoordinate = (centerDimension, coordinate) => {
|
|
let dim = null;
|
|
if(centerDimension && coordinate){
|
|
let left = 0;
|
|
let top = 0;
|
|
|
|
// calculate left/top based on coordinate system quadrant -----------------------------------------
|
|
// -> flip horizontal in Q2 and Q3
|
|
if(coordinate.x >= 0 && coordinate.y > 0){
|
|
// 1. quadrant
|
|
left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x;
|
|
top = centerDimension.top + 2 * (centerDimension.b - this._config.gapY) - Math.abs(coordinate.y) - this._config.newElementHeight;
|
|
}else if(coordinate.x < 0 && coordinate.y > 0){
|
|
// 2. quadrant
|
|
left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x - this._config.newElementWidth;
|
|
top = centerDimension.top + 2 * (centerDimension.b - this._config.gapY) - Math.abs(coordinate.y) - this._config.newElementHeight;
|
|
}else if(coordinate.x < 0 && coordinate.y <= 0){
|
|
// 3. quadrant
|
|
left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x - this._config.newElementWidth;
|
|
top = centerDimension.top + Math.abs(coordinate.y);
|
|
}else{
|
|
// 4. quadrant
|
|
left = centerDimension.left + centerDimension.a - this._config.gapX + coordinate.x;
|
|
top = centerDimension.top + Math.abs(coordinate.y);
|
|
}
|
|
|
|
// center horizontal for x = 0 coordinate (top and bottom element) --------------------------------
|
|
if(coordinate.x === 0){
|
|
left -= Math.round(this._config.newElementWidth / 2);
|
|
}
|
|
|
|
// transform to grid coordinates (if grid snapping is enabled) ------------------------------------
|
|
if(this._config.grid){
|
|
let point = this._transformPointToGrid([left, top]);
|
|
left = point[0];
|
|
top = point[1];
|
|
}
|
|
|
|
dim = {
|
|
left: left,
|
|
top: top,
|
|
width: this._config.newElementWidth,
|
|
height: this._config.newElementHeight
|
|
};
|
|
}
|
|
|
|
return dim;
|
|
};
|
|
|
|
/**
|
|
* calc overlapping percent of two given dimensions
|
|
* @param dim1
|
|
* @param dim2
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
this._percentCovered = (dim1, dim2) => {
|
|
let percent = 0;
|
|
|
|
if(
|
|
(dim1.left <= dim2.left) &&
|
|
(dim1.top <= dim2.top) &&
|
|
((dim1.left + dim1.width) >= (dim2.left + dim2.width)) &&
|
|
((dim1.top + dim1.height) > (dim2.top + dim2.height))
|
|
){
|
|
// The whole thing is covering the whole other thing
|
|
percent = 100;
|
|
}else{
|
|
// Only parts may be covered, calculate percentage
|
|
dim1.right = dim1.left + dim1.width;
|
|
dim1.bottom = dim1.top + dim1.height;
|
|
dim2.right = dim2.left + dim2.width;
|
|
dim2.bottom = dim2.top + dim2.height;
|
|
|
|
let l = Math.max(dim1.left, dim2.left);
|
|
let r = Math.min(dim1.right, dim2.right);
|
|
let t = Math.max(dim1.top, dim2.top);
|
|
let b = Math.min(dim1.bottom, dim2.bottom);
|
|
|
|
if(b >= t && r >= l){
|
|
percent = (((r - l) * (b - t)) / (dim2.width * dim2.height)) * 100;
|
|
}
|
|
}
|
|
return percent;
|
|
};
|
|
|
|
/**
|
|
* checks whether dim1 is partially overlapped by any other element
|
|
* @param dim1
|
|
* @param dimensionContainer
|
|
* @param allDimensions
|
|
* @returns {boolean}
|
|
* @private
|
|
*/
|
|
this._isOverlapping = (dim1, dimensionContainer, allDimensions) => {
|
|
let isOverlapping = false;
|
|
if(dim1){
|
|
if(this._percentCovered(dimensionContainer, dim1 ) === 100){
|
|
// element is within parent container
|
|
for(let dim2 of allDimensions){
|
|
let percentCovered = this._percentCovered(dim1, dim2);
|
|
if(percentCovered){
|
|
isOverlapping = true;
|
|
// render debug element
|
|
this._showDebugElement(dim1, percentCovered);
|
|
break;
|
|
}
|
|
}
|
|
}else{
|
|
isOverlapping = true;
|
|
this._showDebugElement(dim1, 100);
|
|
}
|
|
}else{
|
|
isOverlapping = true;
|
|
}
|
|
|
|
return isOverlapping;
|
|
};
|
|
|
|
/**
|
|
* find all dimensions around a centerDimension that are not overlapped by other elements
|
|
* @param maxResults
|
|
* @param steps
|
|
* @param allDimensions
|
|
* @param loops
|
|
* @returns {Array}
|
|
* @private
|
|
*/
|
|
this._findDimensions = (maxResults, steps, allDimensions, loops) => {
|
|
let dimensions = [];
|
|
let start = 0;
|
|
let end = 360;
|
|
let angle = end / steps;
|
|
let dimensionContainer = this._getElementDimension(this._getContainer());
|
|
steps = steps || 1;
|
|
loops = loops || 1;
|
|
|
|
if(loops === 1){
|
|
// check center element
|
|
let centerDimension = this._getElementDimension(this._config.center);
|
|
if(!this._isOverlapping(centerDimension, dimensionContainer, allDimensions)){
|
|
dimensions.push({
|
|
left: centerDimension.left,
|
|
top: centerDimension.top,
|
|
width: centerDimension.width,
|
|
height: centerDimension.height
|
|
});
|
|
// render debug element
|
|
this._showDebugElement(centerDimension, 0);
|
|
|
|
maxResults--;
|
|
}
|
|
}
|
|
|
|
// increase the "gab" between center element and potential found dimensions...
|
|
// ... for each recursive loop call, to get an elliptical cycle beyond
|
|
this._config.gapX = this._config.defaultGapX + (loops - 1) * 20;
|
|
this._config.gapY = this._config.defaultGapY + (loops - 1) * 20;
|
|
let centerDimension = this._getElementDimension(this._config.center);
|
|
|
|
while(maxResults > 0 && start < end){
|
|
// get all potential coordinates on an eclipse around a given "centerElementDimension"
|
|
let coordinate = this._getEllipseCoordinates(centerDimension, end);
|
|
// transform relative x/y coordinate into a absolute 2D area
|
|
let checkDimension = this._transformCoordinate(centerDimension, coordinate);
|
|
if(!this._isOverlapping(checkDimension, dimensionContainer, allDimensions)){
|
|
dimensions.push({
|
|
left: checkDimension.left,
|
|
top: checkDimension.top,
|
|
width: checkDimension.width,
|
|
height: checkDimension.height
|
|
});
|
|
// render debug element
|
|
this._showDebugElement(checkDimension, 0);
|
|
|
|
maxResults--;
|
|
}
|
|
end -= angle;
|
|
}
|
|
|
|
if(maxResults > 0 && loops < this._config.loops){
|
|
loops++;
|
|
steps *= 2;
|
|
dimensions = dimensions.concat(this._findDimensions(maxResults, steps, allDimensions, loops));
|
|
}
|
|
|
|
return dimensions;
|
|
};
|
|
|
|
/**
|
|
* get container (parent) element
|
|
* @returns {*}
|
|
* @private
|
|
*/
|
|
this._getContainer = () => {
|
|
return this._config.container ? this._config.container : document.body;
|
|
};
|
|
|
|
/**
|
|
* render debug element into parent container
|
|
* -> checks overlapping dimension with other elements
|
|
* @param dimension
|
|
* @param percentCovered
|
|
* @private
|
|
*/
|
|
this._showDebugElement = (dimension, percentCovered) => {
|
|
if(this._config.debug){
|
|
let element = document.createElement('div');
|
|
element.style.left = dimension.left + 'px';
|
|
element.style.top = dimension.top + 'px';
|
|
element.style.width = dimension.width + 'px';
|
|
element.style.height = dimension.height + 'px';
|
|
element.style.backgroundColor = Boolean(percentCovered) ? 'rgba(255,0,0,0.1)' : 'rgba(0,255,0,0.1)';
|
|
element.innerHTML = Math.round(percentCovered * 100) / 100 + '%';
|
|
element.classList.add(this._config.debugElementClass);
|
|
this._getContainer().appendChild(element);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* hide all debug elements
|
|
* @private
|
|
*/
|
|
this._hideDebugElements = () => {
|
|
let debugElements = this._getContainer().getElementsByClassName(this._config.debugElementClass);
|
|
while(debugElements.length > 0){
|
|
debugElements[0].parentNode.removeChild(debugElements[0]);
|
|
}
|
|
};
|
|
|
|
|
|
// public functions ---------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* search for surrounding, non overlapping dimensions
|
|
* @param maxResults
|
|
* @param steps
|
|
* @returns {Array}
|
|
*/
|
|
this.findNonOverlappingDimensions = (maxResults, steps) => {
|
|
this._hideDebugElements();
|
|
// element dimensions that exist and should be checked for overlapping
|
|
let allDimensions = this._getAllElementDimensions();
|
|
|
|
return this._findDimensions(maxResults, steps, allDimensions);
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
Position: Position
|
|
};
|
|
}); |