Click here to Skip to main content
15,867,308 members
Articles / Web Development / HTML5

Point Inside 3D Convex Polygon in JavaScript

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
14 Jan 2016CPOL3 min read 10.3K   201   2  
An algorithm to determine if a point is inside a 3D convex polygon for a given polygon vertices in JavaScript

Introduction

This is a JavaScript version of the original C++ algorithm which can be found here.

Background

Please refer to the original C++ algorithm here.

Using the Code

The algorithm is wrapped into a JavaScript library folder GeoProc. The main test program JSLASProc reads point cloud data from a LAS file, then writes all inside points into a new LAS file. A LAS file is able to be displayed with a freeware FugroViewer.

Each JavaScript in GeoProc library folder has an usage and test part. To test each function, uncomment the test part and open GeoProc.htm in browser.

GeoPoint.js

Javascript has no operator overloading, the Add method is to simulate operator overloading. Check the reference of Add method in GeoVector.js for its usage.

JavaScript
// 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

The method Multiple is to simulate GeoVector cross product operator. Check the reference of Multiple method in GeoPlane.js for its usage.

JavaScript
// 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

The method Negative is to simulate unary negative operator, the method Multiple is to simulate dot product of GeoPlane and GeoPoint, their references are in GeoPolygonProc.js.

JavaScript
// 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

The default Array length is 1 if using [] to create Array, so set length to 0 is neccessary to help their references to use default 0 length assumption.

JavaScript
// 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

JavaScript
// 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

The method ContainsList is an example for how to declare a public static method in JavaScript. The argument list is a 2D Array, the listItem is a 1D Array, the only reference of the function is in GeoPolygonProc.js, it is used to avoid adding duplicate face via checking if the 2D Array faceVerticeIndex already contains the newly found face vertices 1D Array verticeIndexInOneFace.

JavaScript
// 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

The constructor uses "call" method to reference outside private method. All private methods are used to set GeoPolygonProc public instance members:

  • this.Faces: 1D GeoFace Array for all faces
  • this.FacePlanes: 1D GeoPlane Array for all faces planes
  • this.NumberOfFaces: Number of faces
  • this.x0, this.x1, this.y0, this.y1, this.z0, this.z1: Polygon boundary

The only public method is PointInside3DPolygon. All public instance member and methods are referenced in JSLASProc.js.

  • faceVerticeIndex: 2D Array, indexes in each face for all faces. Face index is first major dimension, vertice indexes in one face is second minor dimension. The original 3D polygon vertices indexes are split to difference groups according to different faces.
  • maxError: This value is a point to face triangle plane distance threshold to determine if the other vertice is in the face triangle plane.
JavaScript
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

To read LAS file, html5 FileReader, DataView and ArrayBuffer are used, "slice" method is used to simulate Seek function for file pointer movement.

The headerBuffer is postponded to be added in front of the output Array lasInsideBytes since record number in the output LAS header needs to update with actual writting count insideCount. The "unshift" method is used to add buffer in front of Array.

FileReader is Asynchronous, the result of readAsArrayBuffer is only ready inside its "onloadend" event.

At the time, html5 FileSystem & FileWriter API are still not supported by Firefox and IE. So this latest technique is not chosen for file output. Another choice is ActiveXObject, same reason it is discarded, it is not browser friendly.

Blob and createObjectURL are finally used to create the outout text file and binary LAS file, then attach them to download links, output the files via clicking the download links.

An interesting thing here is an Array length is its items count, if "push" methods are used 10 times, then the Array length is 10, no matter how many bytes are pushed in for each time.

Another small trick is using "\r\n" as carrage return in text file output string variable lasTextStr. Both "\r" and "\n" do not work.

JavaScript
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

After select the LAS file cube.las via the file selector, click button "LAS Process". The two download links will appear in the page and some brief LAS info will show up as well.

HTML
<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>

Points of Interest

JavaScript version is fastest in the C#, Java and C++ versions.

History

  • 15th January, 2016: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Canada Canada
My name is Jiyang Hou (or John Hou). I was born in HeiLongJiang province in north east of China. I got all my educations in China. My university major is Geophysics, but my main professional role is software developer. My biggest accomplishment so far is quit smoking about 5 years ago after almost 20 years smoking history. I am still interested on programming beside making living with it like many other developers. I immigrated to Canada in 2003 and became a permanent resident till now. I live in Calgary, Alberta, Canada. You can reach me by jyhou69@gmail.com regarding to any questions, comments, advice, etc.

Comments and Discussions

 
-- There are no messages in this forum --