可以在嵌入式SVG中检查各种形状之间的共享边界吗?

时间:2019-04-11 01:34:23

标签: javascript jquery svg

我有一个地图的嵌入式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不是任何其他形状的邻居,因此不会将邻居类应用于任何形状。

我已经考虑过比较每个形状的坐标以找到匹配项,但是我意识到可以有两个具有共享边界但没有共享坐标的形状。

这可能是一个更大的问题,就是不知道如何表达我要寻找的东西!

1 个答案:

答案 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>