【问题标题】:What is the best way to programatically draw the border(s) of a group of squares?以编程方式绘制一组正方形的边界的最佳方法是什么?
【发布时间】:2016-07-28 03:23:28
【问题描述】:

我正在构建一个工具,该工具最终将利用 Google Maps API v3 在固定“网格”系统(例如,co-从地球的 0,0 点开始,每 0.0001 latlong 单位间隔的纵坐标)。

我编写了代码,用户可以在其中单击地图上的某个区域,该代码会绘制轮廓并填充找到的正方形。用户可以单击该正方形的其他相邻位置以构建一个越来越大的“块状”多边形,并且可以单击单个正方形将其删除。我自己在 HTML5 canvas/JavaScript 以及 Google Maps API 中测试了所有这些。

现在我想编写删除所有内部边/顶点的代码,以便它只绘制这个多边形的最外层边界,以便它实际上被绘制为一个大多边形,而不是一组正方形。可以这样想:尽管我们知道像澳大利亚、美国等国家是由多个州组成的,但当我们在该国周围绘制边界时,我们通常对所有州的边界并不感兴趣,可以删除那些之间的线,只画外边界。这与我想要完成的事情相同,只是使用正方形网格而不是复杂的多边形。

我当前的代码在这里:

https://jsfiddle.net/wyxcvmdf/14/

HTML:

<canvas id="myCanvas" width="500" height="250" style="border:1px solid #000000;"></canvas>

<!--etc.-->

JavaScript:

// don't forget to set load type in jsfiddle to no wrap in <body>

// define the global variable and some helper variables to make the code shorter
var gv = {};
gv.o = function(id) {
  return document.getElementById(id)
};
gv.i = 'innerHTML';

// etc.

关于我的代码的一些解释性说明:

• 每个正方形的“原点”是该正方形左下角的顶点。没有特别的原因。

• 就 HTML5 画布如何绘制轮廓而言,“绘制方向”是从原点逆时针方向。同样,没有特别的原因。

• 您还不能“单击”添加正方形,因为这只是概念验证,因此您可以通过在相关文本输入框中输入 x 和 y 坐标来添加正方形

我想到的证明我的代码所需的用例/测试是:

  • 将正方形添加到具有 1 个重复顶点的多边形(工作)

  • 在所有情况下将正方形添加到具有 2 个和 3 个重复顶点的多边形:相邻边、非相邻边、非连续顶点(目前仅适用于第一种情况)

  • 在所有情况下将正方形添加到具有 4 个重复顶点的多边形中:堵塞孔、堵塞孔的一部分、连接多个多边形(目前仅适用于第一种情况)

  • 在上述情况下从具有 1 个重复顶点的多边形中删除正方形(尚未开发,但应该有效地“反向”添加代码)

  • 在上述情况下从具有 2 个和 3 个重复顶点的多边形中删除正方形(尚未开发,但实际上应该是添加代码的“反向”)

  • 在上述情况下从具有 4 个重复顶点的多边形中删除正方形(尚未开发,但应该有效地“反向”添加代码)

  • 在具有多个内部边界的多边形外部进行正方形添加/删除,即孔(尚未开发,可能很棘手)

  • 在具有多个内边界的多边形内部进行正方形添加/删除,即孔(尚未开发,可能会很棘手)

注意 1:我使用“正方形”、“边”等,而不是“多边形”等,只是为了解释的简单。

注意 2:我已经对类似的问题和可能的解决方案进行了大量研究,但还没有真正找到能满足我需求的任何东西。我所做的研究是:

  • 旅行推销员问题。 但是,这不是关于优化路径 - 它是关于确保路径是“可绘制的”并因此朝着一个方向前进。只要最终的形状看起来像用户期望的那样,重叠的顶点就完全没问题。

  • 凸包算法。 不适用,因为包可能是凸的、凹的甚至是不连续的!另外,我认为通过简化为网格系统,我消除了存在许多分散顶点的问题,您需要确定它们与中心点的距离,使用三角法等。

  • 凹壳解决方案。这更接近于解决我的问题,但是我看到的是有很多商业工具(例如 ArcGIS)的插件可以做到这一点,但是没有涵盖我所有用例的通用代码(无论编程语言如何)。

  • 基于图块的游戏。您会认为任何需要在图块周围绘制边界的基于图块的游戏(例如,实时战略游戏中玩家的领土)都可以解决这个问题问题,但不是我能看到的。

【问题讨论】:

  • "删除任何内部边/顶点,使其仅绘制此多边形的最外边界" + "简化的固定网格/正方形系统,而不是复杂的多边形"(如何)这与低分辨率 flood filll 有何不同?
  • 泛洪填充的概念看起来很有趣,但是快速浏览一下,它似乎只填充了一系列像素,没有定义外边界,只绘制了这个边界。不过我会仔细看看,谢谢。

标签: javascript html algorithm google-maps html5-canvas


【解决方案1】:

您说“绘制”而不是计算外部顶点,所以...

您可以使用剪辑和合成来“挖空”您的一组正方形。

假设您已经确定这些正方形在您想要的边界内(部分或全部在里面):

var aInside=[ {x:60,y:60},{x:80,y:60},{x:40,y:60},{x:60,y:40},{x:60,y:80} ];

您想要的边界内的正方形插图。

然后,要仅绘制正方形集的边界,您可以:

  1. 描边(不填充)每个内部方块:context.rect

  1. 将进一步绘图限制为描边矩形:context.clip

  2. 使所有新绘图擦除现有像素:context.globalCompositeOperation = 'destination-out'

  3. 用纯色填充整个画布:context.fillRect(0,0,canvas.width,canvas.height)

诀窍: 描边一个矩形实际上是在矩形的一半内侧和一半外侧绘制一个描边,所以第 4 步将擦除矩形组的内侧,但(重要的是!)会离开半外冲程。

所以你最终得到了这个:

这是示例代码和演示:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");

var aInside=[ {x:60,y:60},{x:80,y:60},{x:40,y:60},{x:60,y:40},{x:60,y:80} ];

// stroke all inside squares
ctx.save();
ctx.beginPath();
for(var i=0;i<aInside.length;i++){
    var s=aInside[i];
    ctx.rect(s.x,s.y,20,20);
}
ctx.stroke();

// clip to cause all new drawing to be inside the stroked squares
ctx.clip();

// set compositing to use new drawings to  "erase" existing drawings
ctx.globalCompositeOperation='destination-out';

// Fill (===erase!) the entire canvas 
// Clipping causes only the clipping area to be erased
//     so the inside of the rects set is "hollowed out"
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.restore();
body{ background-color: ivory; }
#canvas{border:1px solid red; }
&lt;canvas id="canvas" width=150 height=150&gt;&lt;/canvas&gt;

算法说明:如果您想要一组幸存的顶点而不是绘图,您可以修改Marching Squares Algorithm 以仅返回拐点。这些拐点是您的外部边界的顶点。

【讨论】:

  • 非常感谢您的回答,但请参阅我对上一个回答的评论 - 我不相信 Google Maps API v3 支持合成。
  • 如果没有,则在地图上方的第二个覆盖画布上绘制您的镂空多边形。设置画布pointer-events:none,使其不会干扰谷歌地图。完毕!干杯!
  • 嗨,markE,请参阅我对上面 K3N 的回复 - 另一个画布层不允许与谷歌地图画布交互(有效地使我的画布层静态,这不是预期的结果)。
  • 您可以告诉覆盖画布不要监听事件 (pointer-events:none;),这样底层的谷歌地图就能响应事件。
【解决方案2】:

此方法仅处理绘图/外观 - 它不会产生任何新的多边形。但它允许您使用一组多边形(任何形状,这里是矩形)并在视觉上合并它们以产生合并的轮廓。我将这个答案基于我的earlier answers 之一,但经过修改和采用以适应这里的场景:

  • 将所有矩形绘制为实体
  • 围绕所有边缘和角落重新绘制偏移,挤出到您想要的厚度
  • 重绘原始矩形,但全局复合模式设置为destination-out并居中于顶部

有几个步骤,但它工作得很快。

几点说明:

  • 如果您有现有背景,则有必要使用屏幕外画布作为临时舞台。此处未显示,但步骤相同,只是您将在屏幕外上下文中执行这些步骤,最后您将从屏幕外画布复制内容到显示画布的现有内容之上。
  • 如果您有很多矩形,则可以通过将每个矩形绘制到单独的屏幕外画布上进行优化,而无需重新绘制任何其他内容。然后,当您执行如下所示的挤压过程时,只需将此屏幕外画布用作源(例如,请参见上面的链接,只需将图像替换为屏幕外画布本身作为源)。
  • 可以通过检查是否嵌入了矩形,如果是,则将其从集合中删除,从而进一步优化它。

演示

var ctx = c.getContext("2d"),
    rw = 50, rh = 50,                               // some demo size
    rectangles = [];                                // rectangle collection

function render(ctx) {
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.fillStyle = "#a00";
  ctx.globalCompositeOperation = "source-over";     // draw using standard mode3

  // we will draw the same rects on top of each other eight times
  // this will extrude the edge so we can in the next step punch a
  // hole in the drawing and leave only the extrusion -
  // offset array (x,y) pairs
  var i, d = 2,                                // d = number of pixels to offset
      offsets = [-d, -d,  0, -d,  d, -d,  d, 0,  d, d,  0, d, -d, d, -d, 0];
  
  for(i = 0; i < offsets.length; i += 2) {
    ctx.setTransform(1,0,0,1,  offsets[i], offsets[i+1]);
    drawRects()
  }
  
  // punch hole in the center
  ctx.setTransform(1,0,0,1,0,0);                    // reset transformatons
  ctx.globalCompositeOperation = "destination-out"; // "erase" mode
  drawRects();                                      // draw a final time, wo/ extrusion
  
  function drawRects() {
    ctx.beginPath();
    rectangles.forEach(function(r) {
      ctx.rect(r.x, r.y, r.w, r.h)
    });                                             // loop through collection and draw
    ctx.fill()
  }
}

// demo stuff --
c.onclick = function(e) {
  var r = this.getBoundingClientRect(),             // for demo, get mouse position
      x = e.clientX - r.left,
      y = e.clientY - r.top;
  
  // add rectangle to list
  rectangles.push({                                 // generate a rect. from center
    x: x - rw*0.5,
    y: y - rh*0.5,
    w: rw,
    h: rh
  });
  
  render(ctx);                                      // the key process
};
canvas {border:1px solid #000}
Click on the canvas below to place rectangles:<br>
<canvas width=600 height=600 id=c></canvas>

【讨论】:

  • 非常感谢您的回答。我熟悉画布合成工具,但据我阅读 Google Maps API v3 参考资料可知,它不支持合成。除非我错过了什么?
  • @Arj 也许你可以把它画在谷歌地图画布上,或者作为一个选项,通过位图并检测角变化以生成点,如果线条在同一组中生成点已连接,然后将这些组转换为多边形。
  • 我已经对此进行了调查,虽然您可以在 google 地图图层的顶部使用另一个画布图层进行绘制,但该画布图层无法移动、缩放或响应谷歌地图上发生的事件画布层(至少,不是没有很多额外的工作)。
猜你喜欢
  • 2022-08-03
  • 2022-01-17
  • 2010-11-01
  • 2011-09-13
  • 2012-09-03
  • 1970-01-01
  • 2017-03-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多