index.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // Find self-intersections in geojson polygon (possibly with interior rings)
  2. var rbush = require('rbush');
  3. var merge = function(){
  4. var output = {};
  5. Array.prototype.slice.call(arguments).forEach(function(arg){
  6. if(arg){
  7. Object.keys(arg).forEach(function(key){
  8. output[key]=arg[key];
  9. });
  10. }
  11. });
  12. return output;
  13. };
  14. var defaults = {
  15. useSpatialIndex: true,
  16. epsilon: 0,
  17. reportVertexOnVertex: false,
  18. reportVertexOnEdge: false
  19. };
  20. module.exports = function(feature, filterFn, options0) {
  21. var options;
  22. if("object" === typeof options0){
  23. options = merge(defaults,options0);
  24. } else {
  25. options = merge(defaults,{useSpatialIndex:options0});
  26. }
  27. if (feature.geometry.type != "Polygon") throw new Error("The input feature must be a Polygon");
  28. var coord = feature.geometry.coordinates;
  29. var output = [];
  30. var seen = {};
  31. if (options.useSpatialIndex) {
  32. var allEdgesAsRbushTreeItems = [];
  33. for (var ring0 = 0; ring0 < coord.length; ring0++) {
  34. for (var edge0 = 0; edge0 < coord[ring0].length-1; edge0++) {
  35. allEdgesAsRbushTreeItems.push(rbushTreeItem(ring0, edge0))
  36. }
  37. }
  38. var tree = rbush();
  39. tree.load(allEdgesAsRbushTreeItems);
  40. }
  41. for (var ring0 = 0; ring0 < coord.length; ring0++) {
  42. for (var edge0 = 0; edge0 < coord[ring0].length-1; edge0++) {
  43. if (options.useSpatialIndex) {
  44. var bboxOverlaps = tree.search(rbushTreeItem(ring0, edge0));
  45. bboxOverlaps.forEach(function(bboxIsect) {
  46. var ring1 = bboxIsect.ring;
  47. var edge1 = bboxIsect.edge;
  48. ifIsectAddToOutput(ring0, edge0, ring1, edge1);
  49. });
  50. }
  51. else {
  52. for (var ring1 = 0; ring1 < coord.length; ring1++) {
  53. for (var edge1 = 0 ; edge1 < coord[ring1].length-1; edge1++) {
  54. // TODO: speedup possible if only interested in unique: start last two loops at ring0 and edge0+1
  55. ifIsectAddToOutput(ring0, edge0, ring1, edge1);
  56. }
  57. }
  58. }
  59. }
  60. }
  61. if (!filterFn) output = {type: "Feature", geometry: {type: "MultiPoint", coordinates: output}};
  62. return output;
  63. // true if frac is (almost) 1.0 or 0.0
  64. function isBoundaryCase(frac){
  65. var e2 = options.epsilon * options.epsilon;
  66. return e2 >= (frac-1)*(frac-1) || e2 >= frac*frac;
  67. }
  68. function isOutside(frac){
  69. return frac < 0 - options.epsilon || frac > 1 + options.epsilon;
  70. }
  71. // Function to check if two edges intersect and add the intersection to the output
  72. function ifIsectAddToOutput(ring0, edge0, ring1, edge1) {
  73. var start0 = coord[ring0][edge0];
  74. var end0 = coord[ring0][edge0+1];
  75. var start1 = coord[ring1][edge1];
  76. var end1 = coord[ring1][edge1+1];
  77. var isect = intersect(start0, end0, start1, end1);
  78. if (isect == null) return; // discard parallels and coincidence
  79. frac0, frac1;
  80. if (end0[0] != start0[0]) {
  81. var frac0 = (isect[0]-start0[0])/(end0[0]-start0[0]);
  82. } else {
  83. var frac0 = (isect[1]-start0[1])/(end0[1]-start0[1]);
  84. };
  85. if (end1[0] != start1[0]) {
  86. var frac1 = (isect[0]-start1[0])/(end1[0]-start1[0]);
  87. } else {
  88. var frac1 = (isect[1]-start1[1])/(end1[1]-start1[1]);
  89. };
  90. // There are roughly three cases we need to deal with.
  91. // 1. If at least one of the fracs lies outside [0,1], there is no intersection.
  92. if (isOutside(frac0) || isOutside(frac1)) {
  93. return; // require segment intersection
  94. }
  95. // 2. If both are either exactly 0 or exactly 1, this is not an intersection but just
  96. // two edge segments sharing a common vertex.
  97. if (isBoundaryCase(frac0) && isBoundaryCase(frac1)){
  98. if(! options.reportVertexOnVertex) return;
  99. }
  100. // 3. If only one of the fractions is exactly 0 or 1, this is
  101. // a vertex-on-edge situation.
  102. if (isBoundaryCase(frac0) || isBoundaryCase(frac1)){
  103. if(! options.reportVertexOnEdge) return;
  104. }
  105. var key = isect;
  106. var unique = !seen[key];
  107. if (unique) {
  108. seen[key] = true;
  109. }
  110. if (filterFn) {
  111. output.push(filterFn(isect, ring0, edge0, start0, end0, frac0, ring1, edge1, start1, end1, frac1, unique));
  112. } else {
  113. output.push(isect);
  114. }
  115. }
  116. // Function to return a rbush tree item given an ring and edge number
  117. function rbushTreeItem(ring, edge) {
  118. var start = coord[ring][edge];
  119. var end = coord[ring][edge+1];
  120. if (start[0] < end[0]) {
  121. var minX = start[0], maxX = end[0];
  122. } else {
  123. var minX = end[0], maxX = start[0];
  124. };
  125. if (start[1] < end[1]) {
  126. var minY = start[1], maxY = end[1];
  127. } else {
  128. var minY = end[1], maxY = start[1];
  129. }
  130. return {minX: minX, minY: minY, maxX: maxX, maxY: maxY, ring: ring, edge: edge};
  131. }
  132. }
  133. // Function to compute where two lines (not segments) intersect. From https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
  134. function intersect(start0, end0, start1, end1) {
  135. if (equalArrays(start0,start1) || equalArrays(start0,end1) || equalArrays(end0,start1) || equalArrays(end1,start1)) return null;
  136. var x0 = start0[0],
  137. y0 = start0[1],
  138. x1 = end0[0],
  139. y1 = end0[1],
  140. x2 = start1[0],
  141. y2 = start1[1],
  142. x3 = end1[0],
  143. y3 = end1[1];
  144. var denom = (x0 - x1) * (y2 - y3) - (y0 - y1) * (x2 - x3);
  145. if (denom == 0) return null;
  146. var x4 = ((x0 * y1 - y0 * x1) * (x2 - x3) - (x0 - x1) * (x2 * y3 - y2 * x3)) / denom;
  147. var y4 = ((x0 * y1 - y0 * x1) * (y2 - y3) - (y0 - y1) * (x2 * y3 - y2 * x3)) / denom;
  148. return [x4, y4];
  149. }
  150. // Function to compare Arrays of numbers. From http://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript
  151. function equalArrays(array1, array2) {
  152. // if the other array is a falsy value, return
  153. if (!array1 || !array2)
  154. return false;
  155. // compare lengths - can save a lot of time
  156. if (array1.length != array2.length)
  157. return false;
  158. for (var i = 0, l=array1.length; i < l; i++) {
  159. // Check if we have nested arrays
  160. if (array1[i] instanceof Array && array2[i] instanceof Array) {
  161. // recurse into the nested arrays
  162. if (!equalArrays(array1[i],array2[i]))
  163. return false;
  164. }
  165. else if (array1[i] != array2[i]) {
  166. // Warning - two different object instances will never be equal: {x:20} != {x:20}
  167. return false;
  168. }
  169. }
  170. return true;
  171. }