我有一个地图的嵌入式SVG,其中每个区域都绘制为单独的形状。我想做的就是获取所有共享该形状边界的邻居的列表。
这是我目前拥有的东西:
https://jsfiddle.net/kzscxj30/
$( ".map" ).click(function( event ) {
var $mapTarget = $( event.target );
if ($mapTarget.is( "path" )) {
$( ".region" ).attr("class", "region");
$mapTarget.attr("class", "region active");
} else {
return;
}
})
这个想法是,一旦单击形状,它将为该触摸形状添加一个“活动”类和一个“邻居”类。例如,Region1是Region2和Region3的邻居,因此单击Region1应该将“邻居”类添加到Region2和Region3。 Region2和Region3不是彼此的邻居,因此单击任意一个只会将邻居类添加到Region1。 Region4不是任何其他形状的邻居,因此不会将邻居类应用于任何形状。
我已经考虑过比较每个形状的坐标以找到匹配项,但是我意识到可以有两个具有共享边界但没有共享坐标的形状。
这可能是一个更大的问题,就是不知道如何表达我要寻找的东西!
答案 0 :(得分:0)
(据我所知)没有内置的方法可以做到……
现在,您可以自己设置一种算法来进行检查,但是可能会占用一些计算量。
第一步,您只需检查路径的边界框,消除所有不会以任何方式触及路径的边界框:
function intersectBoxes(bb1, bb2, padding = 0) {
// half padding (we'll add it in every direction of our rects)
const pad = padding / 2;
// helper function to get clean labels
const getCorners = (bb) => ({
left: bb.x - pad,
top: bb.y - pad,
right: bb.x + bb.width + pad,
bottom: bb.x + bb.width + pad,
});
const r1 = getCorners(bb1);
const r2 = getCorners(bb2);
// check intersection
return r1.left <= r2.right &&
r1.right >= r2.left &&
r1.top <= r2.bottom &&
r1.bottom >= r2.top;
}
// usage
intersectBoxes(path1.getBBox(), path2.getBBox(), 2);
// @return Boolean
完成此操作后,您可以开始进行更严格的检查。 SVG2引入了新的isPointInStroke方法,但是目前仅在少数浏览器中受支持。因此,您可能需要填充它。我没有找到一个,所以我很快制作了一个猴子补丁,而不是使用2DContext等效。
借助该方法,我们仅需沿其笔触获取一条路径的x,y坐标,然后重复调用此方法即可。
function slowHitCheck(p1, p2) {
// we will walk along the smallest of both paths
const smallest = p1.getTotalLength() < p2.getTotalLength() ? p1 : p2;
const longest = smallest === p1 ? p2 : p1;
const length = smallest.getTotalLength();
let pos = 0;
while(pos < length) {
const pt = smallest.getPointAtLength(pos);
if(longest.isPointInStroke(pt)) return true;
pos += stroke_width;
}
return false;
}
$(".map").click(function(event) {
var $mapTarget = $(event.target);
if ($mapTarget.is("path")) {
$(".region").attr("class", "region");
const neighbors = getNeighbors($mapTarget[0], $('#Map')[0]);
neighbors.forEach(node => {
node.classList.add('neighbour');
})
$mapTarget.addClass("active");
} else {
return;
}
})
function getNeighbors(target, root, stroke_width = 1) {
const targetBB = target.getBBox();
return [...root.querySelectorAll('path')]
.filter(path =>
path !== target && // not the target
// fast check BBoxes
intersectBoxes(path.getBBox(), targetBB, stroke_width / 2) &&
// walk the path
slowHitCheck(path, target, stroke_width)
);
}
function intersectBoxes(bb1, bb2, padding) {
const pad = padding / 2;
const getCorners = (bb) => ({
left: bb.x - pad,
top: bb.y - pad,
right: bb.x + bb.width + pad,
bottom: bb.x + bb.width + pad,
});
const r1 = getCorners(bb1);
const r2 = getCorners(bb2);
return r1.left <= r2.right &&
r1.right >= r2.left &&
r1.top <= r2.bottom &&
r1.bottom >= r2.top;
}
function slowHitCheck(p1, p2, stroke_width) {
const smallest = p1.getTotalLength() < p2.getTotalLength() ? p1 : p2;
const longest = smallest === p1 ? p2 : p1;
const length = smallest.getTotalLength();
let pos = 0;
while (pos < length) {
const pt = smallest.getPointAtLength(pos);
if (longest.isPointInStroke(pt)) return true;
pos += stroke_width;
}
return false;
}
/* Half related code below:
* Monkey Patches SVGGeometryElement's isPointInStroke
* and is isPointInFill.
* You can check the revision history
* for a simpler version that only worked for SVGPathElements
*/
// Beware untested code below
// There may very well be a lot of cases where it will not work at all
if (window.SVGGeometryElement && !window.SVGGeometryElement.prototype.isPointInStroke) {
monkeyPatchSVGIsPointIn();
}
function monkeyPatchSVGIsPointIn() {
const ctx = get2DContext(0, 0);
const default_ctx = get2DContext(0, 0);
Object.defineProperty(SVGGeometryElement.prototype, 'isPointInStroke', {
value: function isPointInStroke(point) {
returnIfAbrupt(point);
const path = generatePath2DFromSVGElement(this);
setUpContextToSVGElement(ctx, this);
ctx.stroke(path);
return ctx.isPointInStroke(path, point.x, point.y);
}
});
Object.defineProperty(SVGGeometryElement.prototype, 'isPointInFill', {
value: function isPointInFill(point) {
returnIfAbrupt(point);
const path = generatePath2DFromSVGElement(this);
setUpContextToSVGElement(ctx, this);
ctx.fill(path, this.getAttribute('fill-rule') || "nonzero")
return ctx.isPointInPath(path, point.x, point.y, this.getAttribute('fill-rule') || 'nonzero');
}
});
function returnIfAbrupt(svgPoint) {
if (svgPoint instanceof SVGPoint === false) {
throw new TypeError("Failed to execute 'isPointInStroke' on 'SVGGeometryElement':" +
"parameter 1 is not of type 'SVGPoint'.")
}
}
function generatePath2DFromSVGElement(el) {
const def = el instanceof SVGPathElement ?
el.getAttribute('d') :
(el instanceof SVGPolygonElement ||
el instanceof SVGPolylineElement) ?
("M" + el.getAttribute('points').split(' ').filter(Boolean).join('L')) :
"";
const path = new Path2D(def);
if (!def) {
if (el instanceof SVGLineElement) {
path.lineTo(el.getAttribute('x1'), el.getAttribute('y1'))
path.lineTo(el.getAttribute('x2'), el.getAttribute('y2'))
}
if (el instanceof SVGRectElement) {
path.rect(el.getAttribute('x'), el.getAttribute('y'), el.getAttribute('width'), el.getAttribute('height'));
} else if (el instanceof SVGCircleElement) {
path.arc(el.getAttribute('cx'), el.getAttribute('cy'), el.getAttribute('r'), Math.PI * 2, 0);
} else if (el instanceof SVGEllipseElement) {
path.ellipse(el.getAttribute('cx'), el.getAttribute('cy'), el.getAttribute('rx'), el.getAttribute('ry'), 0, Math.PI * 2, 0);
}
}
return path;
}
function setUpContextToSVGElement(ctx, svgEl) {
const default_ctx = get2DContext();
const dict = {
"stroke-width": "lineWidth",
"stroke-linecap": "lineCap",
"stroke-linejoin": "lineJoin",
"stroke-miterlimit": "miterLimit",
"stroke-dashoffset": "lineDashOffset"
};
for (const [key, value] of Object.entries(dict)) {
ctx[value] = svgEl.getAttribute(key) || default_ctx[value];
}
ctx.setLineDash((svgEl.getAttribute("stroke-dasharray") || "").split(' '));
}
function get2DContext(width = 0, height = 0) {
return Object.assign(
document.createElement("canvas"),
{ width, height }
).getContext('2d');
}
}
body {
background: lightblue;
}
.region {
fill: green;
stroke: white;
stroke-width: 0.5;
}
.region:hover {
fill: lightgreen;
}
.active {
fill: red;
}
.neighbour {
fill: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg id="Map" class="map" viewBox="0 0 125 75">
<g>
<path id="Region01" class="region" d="M0,0 L 0,50 L 50,50Z"/>
<path id="Region02" class="region" d="M25,25 L 75,25 L 75,75Z"/>
<path id="Region03" class="region" d="M0,50 L 25,50 L 25,75 L 0,75Z"/>
<path id="Region04" class="region" d="M100,0 L 125,0 L 125,25 L 100,25Z"/>
</g>
</svg>