g6/plugins/edge.polyline/index.js
2018-07-18 10:12:45 +08:00

443 lines
12 KiB
JavaScript

/**
* @fileOverview auto polyline
* @author huangtonger@aliyun.com
*/
const G6 = require('@antv/g6');
const Util = G6.Util;
const DEFAULT_STYLE = {
offset: 10,
borderRadius: 10
};
function 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);
}
/**
* BBox Utils
* BBox:
* { centerX, centerY, height, maxX, maxY, minX, minY, width }
**/
function 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
};
}
function 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);
}
function getBBoxFromPoint(point) {
const { x, y } = point;
return {
centerX: x, centerY: y,
minX: x, minY: y, maxX: x, maxY: y,
height: 0, width: 0
};
}
function 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)
};
}
function 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
};
}
function 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 };
}
function 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 }
];
}
// function isPointInsideBBox(point, bbox) {
// const { x, y } = point;
// return x > bbox.minX && x < bbox.maxX && y > bbox.minY && y < bbox.maxY;
// }
function isPointOutsideBBox(point, bbox) {
const { x, y } = point;
return x < bbox.minX || x > bbox.maxX || y < bbox.minY || y > bbox.maxY;
}
// function isPointIntersectBBox(point, bbox) {
// return !isPointInsideBBox(point, bbox) && !isPointOutsideBBox(point, bbox);
// }
function getBBoxXCrossPoints(bbox, x) {
if (x < bbox.minX || x > bbox.maxX) {
return [];
}
return [
{ x, y: bbox.minY },
{ x, y: bbox.maxY }
];
}
function getBBoxYCrossPoints(bbox, y) {
if (y < bbox.minY || y > bbox.maxY) {
return [];
}
return [
{ x: bbox.minX, y },
{ x: bbox.maxX, y }
];
}
function getBBoxCrossPointsByPoint(bbox, point) {
return getBBoxXCrossPoints(bbox, point.x).concat(getBBoxYCrossPoints(bbox, point.y));
}
function 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);
}
function 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);
}
function filterHVPoints(points) {
const rst = [ points[0] ];
for (let index = 1; index < points.length; index++) {
const point = points[index];
const next = points[index + 1];
const last = points[index - 1];
if (next && last && ((next.x === point.x && last.x === point.x)
|| (next.y === point.y && last.y === point.y))) {
continue;
}
rst.push(point);
}
return rst;
}
/**
* Polyline Utils
* Polyline:
* [ p1, p2, p3, ..., pn ]
**/
function simplifyPolyline(points) {
points = filterConnectPoints(points);
points = filterHVPoints(points);
return points;
}
function getSimplePolyline(sPoint, tPoint) {
return [
sPoint,
{ x: sPoint.x, y: tPoint.y },
tPoint
];
}
function 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;
}
function distance(p1, p2) {
return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
}
function _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;
}
function heuristicCostEstimate(p, ps, pt, source, target) {
return (distance(p, ps) + distance(p, pt)) + _costByPoints(p, [ ps, pt, source, target ]);
}
function removeFrom(arr, item) {
const index = arr.indexOf(item);
if (index > -1) {
arr.splice(index, 1);
}
}
function 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);
}
function 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);
}
}
function 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');
console.error('cannot find path: ', points, start, goal);
return [ start, goal ];
}
function getPolylinePoints(start, end, sNode, tNode, offset) {
const sBBox = sNode && sNode.bbox ? sNode.bbox : getBBoxFromPoint(start);
const tBBox = tNode && tNode.bbox ? tNode.bbox : 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);
}
function isBending(p0, p1, p2) {
return !((p0.x === p1.x === p2.x) || (p0.y === p1.y === p2.y));
}
function 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 ];
}
function 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('');
}
G6.registerEdge('polyline', {
getPath(item) {
const points = item.getPoints();
const source = item.getSource();
const target = item.getTarget();
return this.getPathByPoints(points, source, target);
},
getPathByPoints(points, source, target) {
const { offset } = Util.merge({}, DEFAULT_STYLE);
const polylinePoints = getPolylinePoints(points[0], points[points.length - 1], source, target, offset);
// FIXME default
return Util.pointsToPolygon(polylinePoints);
}
});
G6.registerEdge('polyline-round', {
getPathByPoints(points, source, target) {
const { offset, borderRadius } = Util.merge({}, DEFAULT_STYLE);
const polylinePoints = simplifyPolyline(
getPolylinePoints(points[0], points[points.length - 1], source, target, offset)
);
// FIXME default
return getPathWithBorderRadiusByPolyline(polylinePoints, borderRadius);
}
}, 'polyline');