@antv/g6
Version:
graph visualization frame work
389 lines (376 loc) • 11.1 kB
JavaScript
const Util = require('../../util/index');
export const getBBoxFromPoint = point => {
const {
x,
y
} = point;
return {
centerX: x,
centerY: y,
minX: x,
minY: y,
maxX: x,
maxY: y,
height: 0,
width: 0
};
};
export const getBBoxFromPoints = (points = []) => {
const xs = [];
const ys = [];
points.forEach(p => {
xs.push(p.x);
ys.push(p.y);
});
const minX = Math.min.apply(Math, xs);
const maxX = Math.max.apply(Math, xs);
const minY = Math.min.apply(Math, ys);
const maxY = Math.max.apply(Math, ys);
return {
centerX: (minX + maxX) / 2,
centerY: (minY + maxY) / 2,
maxX,
maxY,
minX,
minY,
height: (maxY - minY),
width: (maxX - minX)
};
};
export const isBBoxesOverlapping = (b1, b2) => {
return Math.abs(b1.centerX - b2.centerX) * 2 < (b1.width + b2.width) && Math.abs(b1.centerY - b2.centerY) * 2 < (b1.height + b2.height);
};
export const filterConnectPoints = points => {
// pre-process: remove duplicated points
const result = [];
const pointsMap = {};
points.forEach(p => {
const id = p.id = `${p.x}-${p.y}`;
pointsMap[ id ] = p;
});
Util.each(pointsMap, p => {
result.push(p);
});
return result;
};
export const simplifyPolyline = points => {
points = filterConnectPoints(points);
return points;
};
export const getSimplePolyline = (sPoint, tPoint) => {
return [
sPoint,
{ x: sPoint.x, y: tPoint.y },
tPoint
];
};
export const getExpandedBBox = (bbox, offset) => {
if (bbox.width === 0 && bbox.height === 0) { // when it is a point
return bbox;
}
return {
centerX: bbox.centerX,
centerY: bbox.centerY,
minX: bbox.minX - offset,
minY: bbox.minY - offset,
maxX: bbox.maxX + offset,
maxY: bbox.maxY + offset,
height: bbox.height + 2 * offset,
width: bbox.width + 2 * offset
};
};
export const isHorizontalPort = (port, bbox) => {
const dx = Math.abs(port.x - bbox.centerX);
const dy = Math.abs(port.y - bbox.centerY);
return (dx / bbox.width) > (dy / bbox.height);
};
export const getExpandedBBoxPoint = (bbox, point) => {
const isHorizontal = isHorizontalPort(point, bbox);
if (isHorizontal) {
return {
x: point.x > bbox.centerX ? bbox.maxX : bbox.minX,
y: point.y
};
}
return {
x: point.x,
y: point.y > bbox.centerY ? bbox.maxY : bbox.minY
};
};
/**
* @param {object} b1 bbox1
* @param {object} b2 bbox2
* @return {object} { centerX, centerY, height, maxX, maxY, minX, minY, width }
**/
export const mergeBBox = (b1, b2) => {
const minX = Math.min(b1.minX, b2.minX);
const minY = Math.min(b1.minY, b2.minY);
const maxX = Math.max(b1.maxX, b2.maxX);
const maxY = Math.max(b1.maxY, b2.maxY);
return {
centerX: (minX + maxX) / 2,
centerY: (minY + maxY) / 2,
minX,
minY,
maxX,
maxY,
height: maxY - minY,
width: maxX - minX
};
};
export const getPointsFromBBox = bbox => {
// anticlockwise
const {
minX,
minY,
maxX,
maxY
} = bbox;
return [{
x: minX,
y: minY
},
{
x: maxX,
y: minY
},
{
x: maxX,
y: maxY
},
{
x: minX,
y: maxY
}];
};
export const isPointOutsideBBox = (point, bbox) => {
const {
x,
y
} = point;
return x < bbox.minX || x > bbox.maxX || y < bbox.minY || y > bbox.maxY;
};
export const getBBoxXCrossPoints = (bbox, x) => {
if (x < bbox.minX || x > bbox.maxX) {
return [];
}
return [{
x,
y: bbox.minY
},
{
x,
y: bbox.maxY
}];
};
export const getBBoxYCrossPoints = (bbox, y) => {
if (y < bbox.minY || y > bbox.maxY) {
return [];
}
return [{
x: bbox.minX,
y
},
{
x: bbox.maxX,
y
}];
};
export const getBBoxCrossPointsByPoint = (bbox, point) => {
return getBBoxXCrossPoints(bbox, point.x).concat(getBBoxYCrossPoints(bbox, point.y));
};
export const distance = (p1, p2) => {
return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
};
export const _costByPoints = (p, points) => {
const offset = -2;
let result = 0;
points.forEach(point => {
if (point) {
if (p.x === point.x) result += offset;
if (p.y === point.y) result += offset;
}
});
return result;
};
export const heuristicCostEstimate = (p, ps, pt, source, target) => {
return (distance(p, ps) + distance(p, pt)) + _costByPoints(p, [ ps, pt, source, target ]);
};
export const reconstructPath = (pathPoints, pointById, cameFrom, currentId, iterator = 0) => {
pathPoints.unshift(pointById[ currentId ]);
if (cameFrom[ currentId ] && cameFrom[ currentId ] !== currentId && iterator <= 100) {
reconstructPath(pathPoints, pointById, cameFrom, cameFrom[ currentId ], iterator + 1);
}
};
export const removeFrom = (arr, item) => {
const index = arr.indexOf(item);
if (index > -1) {
arr.splice(index, 1);
}
};
export const isSegmentsIntersected = (p0, p1, p2, p3) => {
const s1_x = p1.x - p0.x;
const s1_y = p1.y - p0.y;
const s2_x = p3.x - p2.x;
const s2_y = p3.y - p2.y;
const s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / (-s2_x * s1_y + s1_x * s2_y);
const t = (s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / (-s2_x * s1_y + s1_x * s2_y);
return (s >= 0 && s <= 1 && t >= 0 && t <= 1);
};
export const isSegmentCrossingBBox = (p1, p2, bbox) => {
if (bbox.width === bbox.height === 0) {
return false;
}
const [ pa, pb, pc, pd ] = getPointsFromBBox(bbox);
return isSegmentsIntersected(p1, p2, pa, pb) || isSegmentsIntersected(p1, p2, pa, pd) || isSegmentsIntersected(p1, p2, pb, pc) || isSegmentsIntersected(p1, p2, pc, pd);
};
export const getNeighborPoints = (points, point, bbox1, bbox2) => {
const neighbors = [];
points.forEach(p => {
if (p !== point) {
if (p.x === point.x || p.y === point.y) {
if (!isSegmentCrossingBBox(p, point, bbox1) && !isSegmentCrossingBBox(p, point, bbox2)) {
neighbors.push(p);
}
}
}
});
return filterConnectPoints(neighbors);
};
export const pathFinder = (points, start, goal, sBBox, tBBox, os, ot) => { // A-Star Algorithm
const closedSet = [];
const openSet = [ start ];
const cameFrom = {};
const gScore = {}; // all default values are Infinity
const fScore = {}; // all default values are Infinity
gScore[ start.id ] = 0;
fScore[ start.id ] = heuristicCostEstimate(start, goal, start);
const pointById = {};
points.forEach(p => {
pointById[ p.id ] = p;
});
while (openSet.length) {
let current;
let lowestFScore = Infinity;
openSet.forEach(p => {
if (fScore[ p.id ] < lowestFScore) {
lowestFScore = fScore[ p.id ];
current = p;
}
});
if (current === goal) { // ending condition
const pathPoints = [];
reconstructPath(pathPoints, pointById, cameFrom, goal.id);
return pathPoints;
}
removeFrom(openSet, current);
closedSet.push(current);
getNeighborPoints(points, current, sBBox, tBBox).forEach(neighbor => {
if (closedSet.indexOf(neighbor) !== -1) return;
if (openSet.indexOf(neighbor) === -1) {
openSet.push(neighbor);
}
const tentativeGScore = fScore[ current.id ] + distance(current, neighbor); // + distance(neighbor, goal);
if (gScore[ neighbor.id ] && (tentativeGScore >= gScore[ neighbor.id ])) return;
cameFrom[ neighbor.id ] = current.id;
gScore[ neighbor.id ] = tentativeGScore;
fScore[ neighbor.id ] = gScore[ neighbor.id ] + heuristicCostEstimate(neighbor, goal, start, os, ot);
});
}
// throw new Error('Cannot find path');
return [ start, goal ];
};
export const isBending = (p0, p1, p2) => {
return !((p0.x === p1.x === p2.x) || (p0.y === p1.y === p2.y));
};
export const getBorderRadiusPoints = (p0, p1, p2, r) => {
const d0 = distance(p0, p1);
const d1 = distance(p2, p1);
if (d0 < r) {
r = d0;
}
if (d1 < r) {
r = d1;
}
const ps = {
x: p1.x - r / d0 * (p1.x - p0.x),
y: p1.y - r / d0 * (p1.y - p0.y)
};
const pt = {
x: p1.x - r / d1 * (p1.x - p2.x),
y: p1.y - r / d1 * (p1.y - p2.y)
};
return [ ps, pt ];
};
export const getPathWithBorderRadiusByPolyline = (points, borderRadius) => {
// TODO
const pathSegments = [];
const startPoint = points[0];
pathSegments.push(`M${startPoint.x} ${startPoint.y}`);
points.forEach((p, i) => {
const p1 = points[ i + 1 ];
const p2 = points[ i + 2 ];
if (p1 && p2) {
if (isBending(p, p1, p2)) {
const [ ps, pt ] = getBorderRadiusPoints(p, p1, p2, borderRadius);
pathSegments.push(`L${ps.x} ${ps.y}`);
pathSegments.push(`Q${p1.x} ${p1.y} ${pt.x} ${pt.y}`);
pathSegments.push(`L${pt.x} ${pt.y}`);
} else {
pathSegments.push(`L${p1.x} ${p1.y}`);
}
} else if (p1) {
pathSegments.push(`L${p1.x} ${p1.y}`);
}
});
return pathSegments.join('');
};
export const getPolylinePoints = (start, end, sNode, tNode, offset) => {
const sBBox = sNode && sNode.getBBox() ? sNode.getBBox() : getBBoxFromPoint(start);
const tBBox = tNode && tNode.getBBox() ? tNode.getBBox() : getBBoxFromPoint(end);
if (isBBoxesOverlapping(sBBox, tBBox)) { // source and target nodes are overlapping
return simplifyPolyline(getSimplePolyline(start, end));
}
const sxBBox = getExpandedBBox(sBBox, offset);
const txBBox = getExpandedBBox(tBBox, offset);
if (isBBoxesOverlapping(sxBBox, txBBox)) { // the expanded bounding boxes of source and target nodes are overlapping
return simplifyPolyline(getSimplePolyline(start, end));
}
const sPoint = getExpandedBBoxPoint(sxBBox, start);
const tPoint = getExpandedBBoxPoint(txBBox, end);
const lineBBox = getBBoxFromPoints([ sPoint, tPoint ]);
const outerBBox = mergeBBox(sxBBox, txBBox);
const sMixBBox = mergeBBox(sxBBox, lineBBox);
const tMixBBox = mergeBBox(txBBox, lineBBox);
let connectPoints = [];
connectPoints = connectPoints.concat(getPointsFromBBox(sMixBBox) // .filter(p => !isPointIntersectBBox(p, txBBox))
);
connectPoints = connectPoints.concat(getPointsFromBBox(tMixBBox) // .filter(p => !isPointIntersectBBox(p, sxBBox))
);
const centerPoint = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2
}; [ lineBBox, sMixBBox, tMixBBox ].forEach(bbox => {
connectPoints = connectPoints.concat(getBBoxCrossPointsByPoint(bbox, centerPoint).filter(p => isPointOutsideBBox(p, sxBBox) && isPointOutsideBBox(p, txBBox)));
}); [{
x: sPoint.x,
y: tPoint.y
},
{
x: tPoint.x,
y: sPoint.y
}].forEach(p => {
if (isPointOutsideBBox(p, sxBBox) && isPointOutsideBBox(p, txBBox) // &&
// isPointInsideBBox(p, sMixBBox) && isPointInsideBBox(p, tMixBBox)
) {
connectPoints.push(p);
}
});
connectPoints.unshift(sPoint);
connectPoints.push(tPoint);
connectPoints = filterConnectPoints(connectPoints, sxBBox, txBBox, outerBBox);
const pathPoints = pathFinder(connectPoints, sPoint, tPoint, sBBox, tBBox, start, end);
pathPoints.unshift(start);
pathPoints.push(end);
return simplifyPolyline(pathPoints);
};