UNPKG

@antv/g6

Version:

graph visualization frame work

389 lines (376 loc) 11.1 kB
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); };