index.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import rbush from 'geojson-rbush';
  2. import square from '@turf/square';
  3. import bbox from '@turf/bbox';
  4. import truncate from '@turf/truncate';
  5. import lineSegment from '@turf/line-segment';
  6. import lineIntersect from '@turf/line-intersect';
  7. import nearestPointOnLine from '@turf/nearest-point-on-line';
  8. import { getType, getCoords, getCoord } from '@turf/invariant';
  9. import { flattenEach, featureEach, featureReduce } from '@turf/meta';
  10. import { featureCollection, lineString } from '@turf/helpers';
  11. /**
  12. * Split a LineString by another GeoJSON Feature.
  13. *
  14. * @name lineSplit
  15. * @param {Feature<LineString>} line LineString Feature to split
  16. * @param {Feature<any>} splitter Feature used to split line
  17. * @returns {FeatureCollection<LineString>} Split LineStrings
  18. * @example
  19. * var line = turf.lineString([[120, -25], [145, -25]]);
  20. * var splitter = turf.lineString([[130, -15], [130, -35]]);
  21. *
  22. * var split = turf.lineSplit(line, splitter);
  23. *
  24. * //addToMap
  25. * var addToMap = [line, splitter]
  26. */
  27. function lineSplit(line, splitter) {
  28. if (!line) throw new Error("line is required");
  29. if (!splitter) throw new Error("splitter is required");
  30. var lineType = getType(line);
  31. var splitterType = getType(splitter);
  32. if (lineType !== "LineString") throw new Error("line must be LineString");
  33. if (splitterType === "FeatureCollection")
  34. throw new Error("splitter cannot be a FeatureCollection");
  35. if (splitterType === "GeometryCollection")
  36. throw new Error("splitter cannot be a GeometryCollection");
  37. // remove excessive decimals from splitter
  38. // to avoid possible approximation issues in rbush
  39. var truncatedSplitter = truncate(splitter, { precision: 7 });
  40. switch (splitterType) {
  41. case "Point":
  42. return splitLineWithPoint(line, truncatedSplitter);
  43. case "MultiPoint":
  44. return splitLineWithPoints(line, truncatedSplitter);
  45. case "LineString":
  46. case "MultiLineString":
  47. case "Polygon":
  48. case "MultiPolygon":
  49. return splitLineWithPoints(line, lineIntersect(line, truncatedSplitter));
  50. }
  51. }
  52. /**
  53. * Split LineString with MultiPoint
  54. *
  55. * @private
  56. * @param {Feature<LineString>} line LineString
  57. * @param {FeatureCollection<Point>} splitter Point
  58. * @returns {FeatureCollection<LineString>} split LineStrings
  59. */
  60. function splitLineWithPoints(line, splitter) {
  61. var results = [];
  62. var tree = rbush();
  63. flattenEach(splitter, function (point) {
  64. // Add index/id to features (needed for filter)
  65. results.forEach(function (feature, index) {
  66. feature.id = index;
  67. });
  68. // First Point - doesn't need to handle any previous line results
  69. if (!results.length) {
  70. results = splitLineWithPoint(line, point).features;
  71. // Add Square BBox to each feature for GeoJSON-RBush
  72. results.forEach(function (feature) {
  73. if (!feature.bbox) feature.bbox = square(bbox(feature));
  74. });
  75. tree.load(featureCollection(results));
  76. // Split with remaining points - lines might needed to be split multiple times
  77. } else {
  78. // Find all lines that are within the splitter's bbox
  79. var search = tree.search(point);
  80. if (search.features.length) {
  81. // RBush might return multiple lines - only process the closest line to splitter
  82. var closestLine = findClosestFeature(point, search);
  83. // Remove closest line from results since this will be split into two lines
  84. // This removes any duplicates inside the results & index
  85. results = results.filter(function (feature) {
  86. return feature.id !== closestLine.id;
  87. });
  88. tree.remove(closestLine);
  89. // Append the two newly split lines into the results
  90. featureEach(splitLineWithPoint(closestLine, point), function (line) {
  91. results.push(line);
  92. tree.insert(line);
  93. });
  94. }
  95. }
  96. });
  97. return featureCollection(results);
  98. }
  99. /**
  100. * Split LineString with Point
  101. *
  102. * @private
  103. * @param {Feature<LineString>} line LineString
  104. * @param {Feature<Point>} splitter Point
  105. * @returns {FeatureCollection<LineString>} split LineStrings
  106. */
  107. function splitLineWithPoint(line, splitter) {
  108. var results = [];
  109. // handle endpoints
  110. var startPoint = getCoords(line)[0];
  111. var endPoint = getCoords(line)[line.geometry.coordinates.length - 1];
  112. if (
  113. pointsEquals(startPoint, getCoord(splitter)) ||
  114. pointsEquals(endPoint, getCoord(splitter))
  115. )
  116. return featureCollection([line]);
  117. // Create spatial index
  118. var tree = rbush();
  119. var segments = lineSegment(line);
  120. tree.load(segments);
  121. // Find all segments that are within bbox of splitter
  122. var search = tree.search(splitter);
  123. // Return itself if point is not within spatial index
  124. if (!search.features.length) return featureCollection([line]);
  125. // RBush might return multiple lines - only process the closest line to splitter
  126. var closestSegment = findClosestFeature(splitter, search);
  127. // Initial value is the first point of the first segments (beginning of line)
  128. var initialValue = [startPoint];
  129. var lastCoords = featureReduce(
  130. segments,
  131. function (previous, current, index) {
  132. var currentCoords = getCoords(current)[1];
  133. var splitterCoords = getCoord(splitter);
  134. // Location where segment intersects with line
  135. if (index === closestSegment.id) {
  136. previous.push(splitterCoords);
  137. results.push(lineString(previous));
  138. // Don't duplicate splitter coordinate (Issue #688)
  139. if (pointsEquals(splitterCoords, currentCoords))
  140. return [splitterCoords];
  141. return [splitterCoords, currentCoords];
  142. // Keep iterating over coords until finished or intersection is found
  143. } else {
  144. previous.push(currentCoords);
  145. return previous;
  146. }
  147. },
  148. initialValue
  149. );
  150. // Append last line to final split results
  151. if (lastCoords.length > 1) {
  152. results.push(lineString(lastCoords));
  153. }
  154. return featureCollection(results);
  155. }
  156. /**
  157. * Find Closest Feature
  158. *
  159. * @private
  160. * @param {Feature<Point>} point Feature must be closest to this point
  161. * @param {FeatureCollection<LineString>} lines Collection of Features
  162. * @returns {Feature<LineString>} closest LineString
  163. */
  164. function findClosestFeature(point, lines) {
  165. if (!lines.features.length) throw new Error("lines must contain features");
  166. // Filter to one segment that is the closest to the line
  167. if (lines.features.length === 1) return lines.features[0];
  168. var closestFeature;
  169. var closestDistance = Infinity;
  170. featureEach(lines, function (segment) {
  171. var pt = nearestPointOnLine(segment, point);
  172. var dist = pt.properties.dist;
  173. if (dist < closestDistance) {
  174. closestFeature = segment;
  175. closestDistance = dist;
  176. }
  177. });
  178. return closestFeature;
  179. }
  180. /**
  181. * Compares two points and returns if they are equals
  182. *
  183. * @private
  184. * @param {Array<number>} pt1 point
  185. * @param {Array<number>} pt2 point
  186. * @returns {boolean} true if they are equals
  187. */
  188. function pointsEquals(pt1, pt2) {
  189. return pt1[0] === pt2[0] && pt1[1] === pt2[1];
  190. }
  191. export default lineSplit;