65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript中点在3D凸多边形内部的判断

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2016 年 1 月 15 日

CPOL

3分钟阅读

viewsIcon

11463

downloadIcon

289

在JavaScript中判断一个点是否在一个给定多边形顶点构成的3D凸多边形内部的算法。

引言

这是一个原始 C++ 算法的 JavaScript 版本,可以在这里找到。

背景

请参考原始 C++ 算法 这里

Using the Code

该算法被封装到一个 JavaScript 库文件夹 GeoProc 中。 主测试程序 JSLASProc 从 LAS 文件读取点云数据,然后将所有内部点写入一个新的 LAS 文件。 可以使用免费软件 FugroViewer 显示 LAS 文件。

GeoProc 库文件夹中的每个 JavaScript 文件都有用法和测试部分。 要测试每个函数,请取消注释测试部分并在浏览器中打开 GeoProc.htm

GeoPoint.js

Javascript 没有运算符重载,Add 方法用于模拟运算符重载。 有关其用法,请查看 GeoVector.jsAdd 方法的引用。

// Constructor
function GeoPoint(x, y, z)    // double x, y, z
{
   this.x = x;
   
   this.y = y;
   
   this.z = z;   
}

// public Instance method to simulate overloading binary operator add (GeoPoint + GeoPoint)
GeoPoint.prototype.Add = function(p) // GeoPoint p
{
   return new GeoPoint(this.x + p.x, this.y + p.y, this.z + p.z);
};

GeoVector.js

Multiple 方法用于模拟 GeoVector 叉积运算符。 有关其用法,请查看 GeoPlane.jsMultiple 方法的引用。

// Constructor
function GeoVector(p0, p1) // GeoPoint p0, p1
{
   // vector begin point
   this.p0 = p0;
   
   // vector end point
   this.p1 = p1;
   
   // vector x axis projection value
   this.x = p1.x - p0.x;
   
   // vector y axis projection value
   this.y = p1.y - p0.y;
   
   // vector z axis projection value
   this.z = p1.z - p0.z;
}

// public Instance method to simulate overloading 
// binary operator multiple (GeoVector * GeoVector)
GeoVector.prototype.Multiple = function(v) // GeoVector v
{
   var x = this.y * v.z - this.z * v.y;
   
   var y = this.z * v.x - this.x * v.z;
   
   var z = this.x * v.y - this.y * v.x;
 
   var p0 = v.p0; // GeoPoint
   
   var p1 = p0.Add(new GeoPoint(x, y, z)); // GeoPoint

   return new GeoVector(p0, p1);
};

GeoPlane.js

Negative 方法用于模拟一元负运算符,Multiple 方法用于模拟 GeoPlaneGeoPoint 的点积,它们的引用在 GeoPolygonProc.js 中。

// Constructor
function GeoPlane(a, b, c, d) // double a, b, c, d
{
   this.a = a;
   
   this.b = b;
   
   this.c = c;
   
   this.d = d;   
};

// public Static method to simulate the second constructor
GeoPlane.Create = function(p0, p1, p2) // GeoPoint p0, p1, p2
{
   var v = new GeoVector(p0, p1);

   var u = new GeoVector(p0, p2);

   // normal vector.
   var n = u.Multiple(v); // GeoVector

   var a = n.x; // double

   var b = n.y; // double

   var c = n.z; // double

   var d = - (a * p0.x + b * p0.y + c * p0.z); // double
   
   return new GeoPlane(a, b, c, d);
}

// public Instance method to simulate overloading unary operator negative - GeoPlane
GeoPlane.prototype.Negative = function()
{
   return new GeoPlane( - this.a, - this.b, - this.c, - this.d);
};

// public Instance method to simulate overloading binary operator multiple 
// (GeoPlane * GeoPoint)
GeoPlane.prototype.Multiple = function(p) // GeoPoint p
{
   return (this.a * p.x + this.b * p.y + this.c * p.z + this.d); // double   
};

GeoFace.js

如果使用 [] 创建 Array,则默认 Array 长度为 1,因此将长度设置为 0 是必要的,以帮助其引用使用默认的 0 length 假设。

// Constructor
function GeoFace(p, idx) // List<GeoPoint> p, List<int> idx
{
   // new List<GeoPoint>()
   this.v = [];   
   this.v.length = 0;

   // new List<GeoPoint>()
   this.idx = [];   
   this.idx.length = 0;
   
   this.n = p.length;
   
   for(var i = 0; i < this.n; i ++ )
   {
      this.v.push(p[i]);
      this.idx.push(idx[i]);
   }
}

GeoPolygon.js

// Constructor
function GeoPolygon(p) // List<GeoPoint> p
{
   // new List<GeoPoint> ()
   this.v = [];
   this.v.length = 0;

   // new List<int> ()
   this.idx = [];   
   this.idx.length = 0;

   this.n = p.length;

   for(var i = 0; i < this.n; i ++ )
   {
      this.v.push(p[i]);
      this.idx.push(i);
   }
}

Utility.js

ContainsList 方法是如何在 JavaScript 中声明一个 public static 方法的示例。参数列表是一个 2D Array,listItem 是一个 1D Array,该函数的唯一引用在 GeoPolygonProc.js 中,它用于通过检查 2D Array faceVerticeIndex 是否已包含新找到的面顶点 1D Array verticeIndexInOneFace 来避免添加重复面。

// Constructor
function Utility()
{
}

// public Static method
Utility.ContainsList = function(list, listItem) // List<List<T>> list, List<T> listItem
{
   if(list == null || listItem == null)
   {
      return false;
   }

   listItem.sort();

   for (var i = 0; i < list.length; i ++ )
   {
      var temp = list[i]; // List<T>
      
      if (temp.length == listItem.length)
      {
         temp.sort();

         var itemEqual = true;
         for(var j = 0; j < listItem.length; j ++ )
         {
            if(temp[j] != listItem[j])
            {
               itemEqual = false;
               break;
            }
         }

         if(itemEqual)
         {
            return true;
         }
      }
      else
      {
         return false;
      }
   }

   return false;
}

GeoPolygonProc.js

构造函数使用 "call" 方法来引用外部 private 方法。 所有 private 方法都用于设置 GeoPolygonProc public 实例成员。

  • this.Faces: 所有面的 1D GeoFace Array
  • this.FacePlanes: 所有面平面的 1D GeoPlane Array
  • this.NumberOfFaces: 面的数量
  • this.x0, this.x1, this.y0, this.y1, this.z0, this.z1: 多边形边界

唯一的 public 方法是 PointInside3DPolygon。 所有 public 实例成员和方法都在 JSLASProc.js 中引用。

  • faceVerticeIndex: 2D Array,每个面中的索引。面索引是第一主要维度,一个面中的顶点索引是第二次要维度。 原始 3D 多边形顶点索引根据不同的面拆分为不同的组。
  • maxError: 此值是点到面三角形平面距离阈值,用于确定另一个顶点是否在面三角形平面中。
var MaxUnitMeasureError = 0.001;

// Constructor
function GeoPolygonProc(polygonInst) // GeoPolygon polygonInst
{
   // Set boundary
   Set3DPolygonBoundary.call(this, polygonInst);

   // Set maximum point to face plane distance error,
   Set3DPolygonUnitError.call(this);

   // Set faces and face planes
   SetConvex3DFaces.call(this, polygonInst);
}

// private Instance method
function Set3DPolygonBoundary(polygon) // GeoPolygon polygon
{
   // List<GeoPoint>
   var vertices = polygon.v;
   
   var n = polygon.n;

   var xmin, xmax, ymin, ymax, zmin, zmax;

   xmin = xmax = vertices[0].x;
   ymin = ymax = vertices[0].y;
   zmin = zmax = vertices[0].z;

   for (var i = 1; i < n; i ++ )
   {
      if (vertices[i].x < xmin) xmin = vertices[i].x;
      if (vertices[i].y < ymin) ymin = vertices[i].y;
      if (vertices[i].z < zmin) zmin = vertices[i].z;
      if (vertices[i].x > xmax) xmax = vertices[i].x;
      if (vertices[i].y > ymax) ymax = vertices[i].y;
      if (vertices[i].z > zmax) zmax = vertices[i].z;
   }
   
   // double
   this.x0 = xmin;  
   this.x1 = xmax;
   this.y0 = ymin;
   this.y1 = ymax;
   this.z0 = zmin;
   this.z1 = zmax;
}

// private Instance method
function Set3DPolygonUnitError()
{
   // double
   this.MaxDisError = ((Math.abs(this.x0) + Math.abs(this.x1) +
                        Math.abs(this.y0) + Math.abs(this.y1) +
                        Math.abs(this.z0) + Math.abs(this.z1)) / 6 * MaxUnitMeasureError);   
}

// private Instance method
function SetConvex3DFaces(polygon) // GeoPolygon polygon
{
   // new List<GeoFace>()
   var faces = [];   
   faces.length = 0;

   // new List<GeoPlane>()
   var facePlanes = [];   
   facePlanes.length = 0;

   var numberOfFaces;

   var maxError = this.MaxDisError;

   // vertices of 3D polygon
   var vertices = polygon.v; // new List<GeoPoint>()   

   var n = polygon.n;

   // vertices indexes for all faces
   // vertices index is the original index value in the input polygon
   // new List<List<int>>()
   var faceVerticeIndex = [[]];   
   faceVerticeIndex.length = 0;

   // face planes for all faces
   // new List<GeoPlane> ()
   var fpOutward = [];   
   fpOutward.length = 0;

   for(var i = 0; i < n; i ++ )
   {
      // triangle point 1
      // GeoPoint
      var p0 = vertices[i];
      
      for(var j = i + 1; j < n; j ++ )
      {
         // triangle point 2
         // GeoPoint
         var p1 = vertices[j];
         
         for(var k = j + 1; k < n; k ++ )
         {
            // triangle point 3
            // GeoPoint
            var p2 = vertices[k];
            
            var trianglePlane = GeoPlane.Create(p0, p1, p2);

            var onLeftCount = 0;

            var onRightCount = 0;

            // indexes of points that lie in same plane with face triangle plane
            // new List<int>()
            var pointInSamePlaneIndex = [];            
            pointInSamePlaneIndex.length = 0;

            for(var l = 0; l < n ; l ++ )
            {
               // any point other than the 3 triangle points
               if(l != i && l != j && l != k)
               {
                  // GeoPoint
                  var p = vertices[l];
                  
                  // double
                  var dis = trianglePlane.Multiple(p);
                  
                  // next point is in the triangle plane
                  if(Math.abs(dis) < maxError)
                  {
                     pointInSamePlaneIndex.push(l);
                  }
                  else
                  {
                     if(dis < 0)
                     {
                        onLeftCount ++ ;
                     }
                     else
                     {
                        onRightCount ++ ;
                     }
                  }
               }
            }

            // This is a face for a CONVEX 3d polygon.
            // For a CONCAVE 3d polygon, this maybe not a face.
            if(onLeftCount == 0 || onRightCount == 0)
            {
               // new List<int>()
               var verticeIndexInOneFace = [];               
               verticeIndexInOneFace.length = 0;

               // triangle plane
               verticeIndexInOneFace.push(i);
               verticeIndexInOneFace.push(j);
               verticeIndexInOneFace.push(k);

               var m = pointInSamePlaneIndex.length;

               if(m > 0) // there are other vertices in this triangle plane
               {
                  for(var p = 0; p < m; p ++ )
                  {
                     verticeIndexInOneFace.push(pointInSamePlaneIndex[p]);
                  }
               }

               // if verticeIndexInOneFace is a new face               
               if ( ! Utility.ContainsList(faceVerticeIndex, verticeIndexInOneFace))
               {
                  // add it in the faceVerticeIndex list
                  faceVerticeIndex.push(verticeIndexInOneFace);

                  // add the trianglePlane in the face plane list fpOutward.
                  if (onRightCount == 0)
                  {
                     fpOutward.push(trianglePlane);
                  }
                  else if (onLeftCount == 0)
                  {
                     fpOutward.push(trianglePlane.Negative());
                  }
               }
            }
            else
            {
               // possible reasons :
               // 1. the plane is not a face of a convex 3d polygon,
               //    it is a plane crossing the convex 3d polygon.
               // 2. the plane is a face of a concave 3d polygon
            }

         } // k loop
         
      } // j loop
      
   } // i loop
   
   numberOfFaces = faceVerticeIndex.length;
   
   for (var i = 0; i < numberOfFaces; i ++ )
   {
      facePlanes.push(new GeoPlane(fpOutward[i].a, fpOutward[i].b,
      fpOutward[i].c, fpOutward[i].d));

      // new List<GeoPoint>()
      var gp = [];      
      gp.length = 0;

      // new List<int>()
      var vi = [];      
      vi.length = 0;

      var count = faceVerticeIndex[i].length;
    
      for (var j = 0; j < count; j ++ )
      {
         vi.push(faceVerticeIndex[i][j]);
         gp.push( new GeoPoint(vertices[ vi[j] ].x,
         vertices[ vi[j] ].y,
         vertices[ vi[j] ].z));
      }

      faces.push(new GeoFace(gp, vi));
   }

   // List<GeoFace>
   this.Faces = faces;
   
   // List<GeoPlane>
   this.FacePlanes = facePlanes;
      
   this.NumberOfFaces = numberOfFaces;   
}

// public Instance method
GeoPolygonProc.prototype.PointInside3DPolygon = function(x, y, z) // GeoPoint x, y, z
{
   var P = new GeoPoint(x, y, z);

   for (var i = 0; i < this.NumberOfFaces; i ++ )
   {
      var dis = this.FacePlanes[i].Multiple(P);

      // If the point is in the same half space with normal vector for any face of the cube,
      // then it is outside of the 3D polygon
      if (dis > 0)
      {
         return false;
      }
   }

   // If the point is in the opposite half space with normal vector for all 6 faces,
   // then it is inside of the 3D polygon
   return true;
}

JSLASProc.js

要读取 LAS 文件,使用 html5 FileReaderDataViewArrayBuffer,"slice" 方法用于模拟文件指针移动的 Seek 函数。

headerBuffer 被延后添加到输出 Array lasInsideBytes 的前面,因为输出 LAS 标头中的记录数需要使用实际写入计数 insideCount 进行更新。"unshift" 方法用于在 Array 前面添加缓冲区。

FileReader 是异步的,readAsArrayBuffer 的结果仅在其 "onloadend" 事件中准备就绪。

目前,Firefox 和 IE 仍然不支持 html5 FileSystem & FileWriter API。 因此,未选择这种最新技术进行文件输出。 另一个选择是 ActiveXObject,同样的原因被丢弃,它对浏览器不友好。

BlobcreateObjectURL 最终用于创建输出文本文件和二进制 LAS 文件,然后将它们附加到下载链接,通过单击下载链接输出文件。

这里有趣的一件事是 Array 的长度是它的项目计数,如果使用 "push" 方法 10 次,那么 Array 的长度是 10,无论每次推送多少字节。

另一个小技巧是在文本文件输出字符串变量 lasTextStr 中使用 "\r\n" 作为回车符。 单独使用 "\r" 和 "\n" 都不起作用。

var offset_to_point_data_value, record_num, record_len;
var x_scale, y_scale, z_scale, x_offset, y_offset, z_offset;

function GetLASFile()
{
   var files = document.getElementById("LASFiles").files;

   if ( ! files.length)
   {
      alert('Please select a LAS file!');
      return;
   }

   var file = files[0];

   return file;
}

function AppendInfoInPage(infoStr)
{
   var node = document.getElementById('infoID');
   var newNode = document.createElement('div');
   newNode.appendChild(document.createTextNode(infoStr));
   node.appendChild(newNode);
}

function MakeTextFile(str)
{
   var blobData = new Blob([str],
   {
      type : 'text/plain'
   }
   );
   textFile = window.URL.createObjectURL(blobData);
   return textFile;
}

function MakeBinaryFile(bytesArray)
{
   var blobData = new Blob(bytesArray,
   {
      type : "octet/stream"
   }
   );
   binFile = window.URL.createObjectURL(blobData);
   return binFile;
}

function ActiveTextDownloadLink(downloadID, downloadStr)
{
   var downloadLink = document.getElementById(downloadID);
   downloadLink.href = MakeTextFile(downloadStr);
   downloadLink.style.display = 'block';
}

function ActiveBinaryDownloadLink(downloadID, downloadBytesArray)
{
   var downloadLink = document.getElementById(downloadID);
   downloadLink.href = MakeBinaryFile(downloadBytesArray);
   downloadLink.style.display = 'block';
}

function WriteInsidePointsFile(procInst, lasBuffer)
{
   AppendInfoInPage("First 10 Inside Points:");

   var insideCount = 0;

   var lasTextStr = "";

   var lasInsideBytes = [];
   lasInsideBytes.length = 0;

   for(var i = 0; i < record_num; i ++ )
   {

      var record_loc = offset_to_point_data_value + record_len * i;

      var coorBuffer = lasBuffer.slice(record_loc, record_loc + 12);

      var dv = new DataView(coorBuffer);
      var littleEndian = true;

      // Record coordinates
      var pos = 0;
      var x = dv.getInt32(pos, littleEndian);
      pos += 4;
      var y = dv.getInt32(pos, littleEndian);
      pos += 4;
      var z = dv.getInt32(pos, littleEndian);

      // Actual coordinates
      var ax = (x * x_scale) + x_offset;
      var ay = (y * y_scale) + y_offset;
      var az = (z * z_scale) + z_offset;

      // Filter out the points outside of boundary to reduce computing
      if(ax > procInst.x0 && ax < procInst.x1 &&
         ay > procInst.y0 && ay < procInst.y1 &&
         az > procInst.z0 && az < procInst.z1)
      {
         // Main Process to check if the point is inside of the cube
         if(procInst.PointInside3DPolygon(ax, ay, az))
         {
            var coorStr = ax.toFixed(6) + ', ' + ay.toFixed(6) + ', ' + az.toFixed(6);

            if(insideCount < 10)
            {
               AppendInfoInPage(coorStr);
            }

            // Write the actual coordinates of inside point to text file
            lasTextStr += coorStr + "\r\n";

            // Get point record
            var recordBuffer = lasBuffer.slice(record_loc, record_loc + record_len);

            // Write inside point LAS record
            lasInsideBytes.push(recordBuffer);

            insideCount ++ ;
         }
      }
   }

   AppendInfoInPage("Total Inside Points Count: " + insideCount);
   AppendInfoInPage("Total Points Count: " + record_num);

   // Get LAS header
   var headerBuffer = lasBuffer.slice(0, offset_to_point_data_value);

   // Update total point number with actual writting count
   var dv = new DataView(headerBuffer);
   dv.setUint32(107, insideCount, true);

   // Write LAS header in front
   lasInsideBytes.unshift(headerBuffer);

   ActiveTextDownloadLink('insideTextLink', lasTextStr);

   ActiveBinaryDownloadLink('insideLASLink', lasInsideBytes);
}

function ProcLASFileHeader(lasBuffer)
{
   // LAS Header part 1
   var partBuffer = lasBuffer.slice(96, 96 + 15);

   // html5 DataView
   var dv = new DataView(partBuffer);
   var littleEndian = true;
   var pos = 0;
   offset_to_point_data_value = dv.getUint32(pos, littleEndian);
   pos += 4;
   var variable_len_num = dv.getUint32(pos, littleEndian);
   pos += 4;
   var record_type_c = dv.getUint8(pos, littleEndian);
   pos += 1;
   record_len = dv.getUint16(pos, littleEndian);
   pos += 2;
   record_num = dv.getUint32(pos, littleEndian);

   // LAS Header part 2
   partBuffer = lasBuffer.slice(131, 131 + 8 * 6);
   dv = new DataView(partBuffer);
   pos = 0;
   x_scale = dv.getFloat64(pos, littleEndian);
   pos += 8;
   y_scale = dv.getFloat64(pos, littleEndian);
   pos += 8;
   z_scale = dv.getFloat64(pos, littleEndian);
   pos += 8;
   x_offset = dv.getFloat64(pos, littleEndian);
   pos += 8;
   y_offset = dv.getFloat64(pos, littleEndian);
   pos += 8;
   z_offset = dv.getFloat64(pos, littleEndian);
   pos += 8;

   // Verify the result via displaying them in page
   AppendInfoInPage("offset_to_point_data_value: " + offset_to_point_data_value +
                    ", record_len: " + record_len +
                    ", record_num: " + record_num);
   AppendInfoInPage("x_scale: " + x_scale + ", y_scale: " + y_scale + ", z_scale: " + z_scale +
                    ", x_offset: " + x_offset + ", y_offset: " + y_offset + 
                    ", z_offset: " + z_offset);
}

function LASProc()
{
   var p1 = new GeoPoint( - 27.28046,  37.11775,  - 39.03485);
   var p2 = new GeoPoint( - 44.40014,  38.50727,  - 28.78860);
   var p3 = new GeoPoint( - 49.63065,  20.24757,  - 35.05160);
   var p4 = new GeoPoint( - 32.51096,  18.85805,  - 45.29785);
   var p5 = new GeoPoint( - 23.59142,  10.81737,  - 29.30445);
   var p6 = new GeoPoint( - 18.36091,  29.07707,  - 23.04144);
   var p7 = new GeoPoint( - 35.48060,  30.46659,  - 12.79519);
   var p8 = new GeoPoint( - 40.71110,  12.20689,  - 19.05819);

   var gp = [p1, p2, p3, p4, p5, p6, p7, p8];

   var gpInst = new GeoPolygon(gp);

   var procInst = new GeoPolygonProc(gpInst);

   // Get LAS file object via html file selector
   // Javascript does not be allowed to open a file from file system as default
   var lasFile = GetLASFile();

   // html5 FileReader is Asynchronous
   var lasFileReader = new FileReader();

   // Asynchronous read, process the result till the read is done
   lasFileReader.readAsArrayBuffer(lasFile);

   // process the result buffer till Asynchronous readAsArrayBuffer is done
   lasFileReader.onloadend = function(evt)
   {
      var lasBuffer = lasFileReader.result;

      ProcLASFileHeader(lasBuffer);

      WriteInsidePointsFile(procInst, lasBuffer);
   }
}

JSLASProc.htm

通过文件选择器选择 LAS 文件 cube.las 后,单击按钮 "LAS Process"。 这两个下载链接将出现在页面中,并且还会显示一些简短的 LAS 信息。

<html>

<!-- include all JavaScript in GeoProc JS library 
     with the same order of GeoProc\GeoProc.htm -->
<script src="..\GeoProc\Utility.js"> </script>
<script src="..\GeoProc\GeoPoint.js"> </script>
<script src="..\GeoProc\GeoVector.js"> </script>
<script src="..\GeoProc\GeoPlane.js"> </script>
<script src="..\GeoProc\GeoPolygon.js"> </script>
<script src="..\GeoProc\GeoFace.js"> </script>
<script src="..\GeoProc\GeoPolygonProc.js"> </script>

<head></head>

<body>
    
    <!-- include LAS Proc JavaScript -->
    <script src="JSLASProc.js"> </script>
    
    <div>
        <form>
            <input type="file" id="LASFiles" name="lasfile" />
            <input type="button" id="btnLASProc" value="LAS Process" onclick="LASProc();"/>   
            <a download="cube_inside.txt" id="insideTextLink" style="display: none">
                         Download Inside Text</a>     
            <a download="cube_inside.las" id="insideLASLink" style="display: none">
                         Download Inside LAS</a>     
        </form>
    </div>
    
    <div id=infoID></div>
    
</body>

</html>

关注点

JavaScript 版本在 C#、Java 和 C++ 版本中是最快的。

历史

  • 2016 年 1 月 15 日:初始版本
© . All rights reserved.