diff --git a/lib/turf/boolean_point_on_line.rb b/lib/turf/boolean_point_on_line.rb index 0e5ce4e..8e952ef 100644 --- a/lib/turf/boolean_point_on_line.rb +++ b/lib/turf/boolean_point_on_line.rb @@ -2,7 +2,127 @@ # :nodoc: module Turf - def boolean_point_on_line(*args) - raise NotImplementedError + # Returns true if a point is on a line. Accepts an optional parameter to ignore the + # start and end vertices of the linestring. + # + # @param [Hash] pt GeoJSON Point + # @param [Hash] line GeoJSON LineString + # @param [Hash] options Optional parameters + # @option options [Boolean] :ignore_end_vertices whether to ignore the start and end vertices. + # @option options [Float] :epsilon Fractional number to compare with the cross product result. + # Useful for dealing with floating points such as lng/lat points + # @return [Boolean] true/false + # @example + # pt = turf_point([0, 0]) + # line = turf_line_string([[-1, -1], [1, 1], [1.5, 2.2]]) + # is_point_on_line = boolean_point_on_line(pt, line) + # # => true + def boolean_point_on_line(point, line, options = nil) + options ||= {} + # Normalize inputs + pt_coords = get_coord(point) + line_coords = get_coords(line) + + # Main + line_coords.each_cons(2) do |line_segment_start, line_segment_end| + ignore_boundary = false + if options[:ignore_end_vertices] + first_segment = (line_coords.first == line_segment_start) + last_segment = (line_coords.last == line_segment_end) + + if first_segment && last_segment + ignore_boundary = :both + elsif first_segment + ignore_boundary = :start + elsif last_segment + ignore_boundary = :end + end + end + + return true if is_point_on_line_segment( + line_segment_start, + line_segment_end, + pt_coords, + ignore_boundary, + options[:epsilon], + ) + end + + false + end + + # Determines if a point is on a line segment. + # + # @param [Array] line_segment_start Coordinate pair of the start of the line segment [x1, y1]. + # @param [Array] line_segment_end Coordinate pair of the end of the line segment [x2, y2]. + # @param [Array] pt Coordinate pair of the point to check [px, py]. + # @param [Boolean, String] exclude_boundary Whether the point is allowed to fall on the line ends. + # Can be true, false, or one of "start", "end", or "both". + # @param [Float, NilClass] epsilon Fractional tolerance for cross-product + # comparison (useful for floating-point coordinates). + # @return [Boolean] true if the point is on the line segment, false otherwise. + def is_point_on_line_segment(line_segment_start, line_segment_end, point, exclude_boundary, epsilon = nil) + x, y = point + x1, y1 = line_segment_start + x2, y2 = line_segment_end + + dxc = x - x1 + dyc = y - y1 + dxl = x2 - x1 + dyl = y2 - y1 + cross = (dxc * dyl) - (dyc * dxl) + + if epsilon + return false if cross.abs > epsilon + elsif cross != 0 + return false + end + + # Special case: zero-length line segments + if dxl == 0 && dyl == 0 + return false if exclude_boundary + + return point == line_segment_start + end + + case exclude_boundary + when false + if dxl.abs >= dyl.abs + if dxl > 0 + x.between?(x1, + x2) + else + x.between?(x2, + x1) + end + else + (if dyl > 0 + y.between?(y1, + y2) + else + y.between?(y2, y1) + end) + end + when :start + if dxl.abs >= dyl.abs + dxl > 0 ? x1 < x && x <= x2 : x2 <= x && x < x1 + else + (dyl > 0 ? y1 < y && y <= y2 : y2 < y && y < y1) + end + when :end + if dxl.abs >= dyl.abs + dxl > 0 ? x1 <= x && x < x2 : x2 < x && x <= x1 + else + (dyl > 0 ? y1 <= y && y < y2 : y2 < y && y <= y1) + end + when :both + if dxl.abs >= dyl.abs + dxl > 0 ? x1 < x && x < x2 : x2 < x && x < x1 + else + (dyl > 0 ? y1 < y && y < y2 : y2 < y && y < y1) + end + else + false + end end end diff --git a/test/turf_boolean_clockwise/false/counter-clockwise-line.geojson b/test/boolean_clockwise/false/counter-clockwise-line.geojson similarity index 100% rename from test/turf_boolean_clockwise/false/counter-clockwise-line.geojson rename to test/boolean_clockwise/false/counter-clockwise-line.geojson diff --git a/test/turf_boolean_clockwise/true/clockwise-line.geojson b/test/boolean_clockwise/true/clockwise-line.geojson similarity index 100% rename from test/turf_boolean_clockwise/true/clockwise-line.geojson rename to test/boolean_clockwise/true/clockwise-line.geojson diff --git a/test/turf_concave/false/3vertices.geojson b/test/boolean_concave/false/3vertices.geojson similarity index 100% rename from test/turf_concave/false/3vertices.geojson rename to test/boolean_concave/false/3vertices.geojson diff --git a/test/turf_concave/false/diamond.geojson b/test/boolean_concave/false/diamond.geojson similarity index 100% rename from test/turf_concave/false/diamond.geojson rename to test/boolean_concave/false/diamond.geojson diff --git a/test/turf_concave/false/square.geojson b/test/boolean_concave/false/square.geojson similarity index 100% rename from test/turf_concave/false/square.geojson rename to test/boolean_concave/false/square.geojson diff --git a/test/turf_concave/true/polygon.geojson b/test/boolean_concave/true/polygon.geojson similarity index 100% rename from test/turf_concave/true/polygon.geojson rename to test/boolean_concave/true/polygon.geojson diff --git a/test/turf_concave/true/polygon2.geojson b/test/boolean_concave/true/polygon2.geojson similarity index 100% rename from test/turf_concave/true/polygon2.geojson rename to test/boolean_concave/true/polygon2.geojson diff --git a/test/boolean_point_on_line/false/LineWithOnly1SegmentIgnoreBoundary.geojson b/test/boolean_point_on_line/false/LineWithOnly1SegmentIgnoreBoundary.geojson new file mode 100644 index 0000000..1911d63 --- /dev/null +++ b/test/boolean_point_on_line/false/LineWithOnly1SegmentIgnoreBoundary.geojson @@ -0,0 +1,27 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/false/LineWithOnly1SegmentIgnoreBoundaryEnd.geojson b/test/boolean_point_on_line/false/LineWithOnly1SegmentIgnoreBoundaryEnd.geojson new file mode 100644 index 0000000..923bc0e --- /dev/null +++ b/test/boolean_point_on_line/false/LineWithOnly1SegmentIgnoreBoundaryEnd.geojson @@ -0,0 +1,27 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [3, 3] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/false/PointIsOnLineButFailsWithSmallEpsilonValue.geojson b/test/boolean_point_on_line/false/PointIsOnLineButFailsWithSmallEpsilonValue.geojson new file mode 100644 index 0000000..1fe3749 --- /dev/null +++ b/test/boolean_point_on_line/false/PointIsOnLineButFailsWithSmallEpsilonValue.geojson @@ -0,0 +1,27 @@ +{ + "type": "FeatureCollection", + "properties": { + "epsilon": 10e-18 + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-75.25737143565107, 39.99673377198139] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-75.2580499870244, 40.00180204907801], + [-75.25676601413157, 39.992211720827044] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/false/PointIsOnLineButFailsWithoutEpsilonForBackwardsCompatibility.geojson b/test/boolean_point_on_line/false/PointIsOnLineButFailsWithoutEpsilonForBackwardsCompatibility.geojson new file mode 100644 index 0000000..5ce33ec --- /dev/null +++ b/test/boolean_point_on_line/false/PointIsOnLineButFailsWithoutEpsilonForBackwardsCompatibility.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-75.25737143565107, 39.99673377198139] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-75.2580499870244, 40.00180204907801], + [-75.25676601413157, 39.992211720827044] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/false/PointOnEndFailsWhenIgnoreEndpoints.geojson b/test/boolean_point_on_line/false/PointOnEndFailsWhenIgnoreEndpoints.geojson new file mode 100644 index 0000000..83bcebc --- /dev/null +++ b/test/boolean_point_on_line/false/PointOnEndFailsWhenIgnoreEndpoints.geojson @@ -0,0 +1,28 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [38.3203125, 5.965753671065536] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [38.3203125, 5.965753671065536] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/false/PointOnStartFailsWhenIgnoreEndpoints.geojson b/test/boolean_point_on_line/false/PointOnStartFailsWhenIgnoreEndpoints.geojson new file mode 100644 index 0000000..bb8d12f --- /dev/null +++ b/test/boolean_point_on_line/false/PointOnStartFailsWhenIgnoreEndpoints.geojson @@ -0,0 +1,28 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [38.3203125, 5.965753671065536] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/false/notOnLine.geojson b/test/boolean_point_on_line/false/notOnLine.geojson new file mode 100644 index 0000000..641dd71 --- /dev/null +++ b/test/boolean_point_on_line/false/notOnLine.geojson @@ -0,0 +1,28 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [20, 20] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [38.3203125, 5.965753671065536] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/LineWithOnly1Segment.geojson b/test/boolean_point_on_line/true/LineWithOnly1Segment.geojson new file mode 100644 index 0000000..8d605f7 --- /dev/null +++ b/test/boolean_point_on_line/true/LineWithOnly1Segment.geojson @@ -0,0 +1,27 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [1, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/LineWithOnly1SegmentOnStart.geojson b/test/boolean_point_on_line/true/LineWithOnly1SegmentOnStart.geojson new file mode 100644 index 0000000..71722f5 --- /dev/null +++ b/test/boolean_point_on_line/true/LineWithOnly1SegmentOnStart.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/PointOnFirstSegment.geojson b/test/boolean_point_on_line/true/PointOnFirstSegment.geojson new file mode 100644 index 0000000..6b22e04 --- /dev/null +++ b/test/boolean_point_on_line/true/PointOnFirstSegment.geojson @@ -0,0 +1,28 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [1, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [4, 4] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/PointOnLastSegment.geojson b/test/boolean_point_on_line/true/PointOnLastSegment.geojson new file mode 100644 index 0000000..f9e7c58 --- /dev/null +++ b/test/boolean_point_on_line/true/PointOnLastSegment.geojson @@ -0,0 +1,28 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [3.5, 3.5] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [4, 4] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/PointOnLineEnd.geojson b/test/boolean_point_on_line/true/PointOnLineEnd.geojson new file mode 100644 index 0000000..b764193 --- /dev/null +++ b/test/boolean_point_on_line/true/PointOnLineEnd.geojson @@ -0,0 +1,25 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [38.3203125, 5.965753671065536] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [38.3203125, 5.965753671065536] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/PointOnLineMidVertice.geojson b/test/boolean_point_on_line/true/PointOnLineMidVertice.geojson new file mode 100644 index 0000000..8fc214d --- /dev/null +++ b/test/boolean_point_on_line/true/PointOnLineMidVertice.geojson @@ -0,0 +1,28 @@ +{ + "type": "FeatureCollection", + "properties": { + "ignore_end_vertices": true + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [3, 3] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [38.3203125, 5.965753671065536] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/PointOnLineMidpoint.geojson b/test/boolean_point_on_line/true/PointOnLineMidpoint.geojson new file mode 100644 index 0000000..7ff1725 --- /dev/null +++ b/test/boolean_point_on_line/true/PointOnLineMidpoint.geojson @@ -0,0 +1,25 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [2, 2] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [38.3203125, 5.965753671065536] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/PointOnLineStart.geojson b/test/boolean_point_on_line/true/PointOnLineStart.geojson new file mode 100644 index 0000000..23c6045 --- /dev/null +++ b/test/boolean_point_on_line/true/PointOnLineStart.geojson @@ -0,0 +1,25 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [3, 3], + [38.3203125, 5.965753671065536] + ] + } + } + ] +} diff --git a/test/boolean_point_on_line/true/PointOnLineWithEpsilon.geojson b/test/boolean_point_on_line/true/PointOnLineWithEpsilon.geojson new file mode 100644 index 0000000..0a32c86 --- /dev/null +++ b/test/boolean_point_on_line/true/PointOnLineWithEpsilon.geojson @@ -0,0 +1,27 @@ +{ + "type": "FeatureCollection", + "properties": { + "epsilon": 10e-17 + }, + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-75.25737143565107, 39.99673377198139] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-75.2580499870244, 40.00180204907801], + [-75.25676601413157, 39.992211720827044] + ] + } + } + ] +} diff --git a/test/turf_boolean_concave_test.rb b/test/turf_boolean_concave_test.rb index 6ceff50..e3557a5 100644 --- a/test/turf_boolean_concave_test.rb +++ b/test/turf_boolean_concave_test.rb @@ -7,7 +7,7 @@ def test_is_concave_fixtures # True Fixtures Dir.glob(File.join(__dir__, "boolean_concave", "true", "*.geojson")).each do |filepath| name = File.basename(filepath, ".geojson") - geojson = JSON.parse(File.read(filepath)) + geojson = JSON.parse(File.read(filepath), symbolize_names: true) feature = geojson[:features][0] assert(Turf.boolean_concave(feature), "[true] #{name}") end @@ -15,7 +15,7 @@ def test_is_concave_fixtures # False Fixtures Dir.glob(File.join(__dir__, "boolean_concave", "false", "*.geojson")).each do |filepath| name = File.basename(filepath, ".geojson") - geojson = JSON.parse(File.read(filepath)) + geojson = JSON.parse(File.read(filepath), symbolize_names: true) feature = geojson[:features][0] refute(Turf.boolean_concave(feature), "[false] #{name}") end diff --git a/test/turf_point_on_line_test.rb b/test/turf_point_on_line_test.rb new file mode 100644 index 0000000..e98bb69 --- /dev/null +++ b/test/turf_point_on_line_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "test_helper" + +class TurfBooleanPointOnLineTest < Minitest::Test + def test_turf_boolean_point_on_line_true_fixtures + # True Fixtures + Dir.glob(File.join(__dir__, "boolean_point_on_line", "true", "**", "*.geojson")).each do |filepath| + name = File.basename(filepath, ".geojson") + geojson = JSON.parse(File.read(filepath), symbolize_names: true) + options = geojson[:properties] + feature1 = geojson[:features][0] + feature2 = geojson[:features][1] + result = Turf.boolean_point_on_line(feature1, feature2, options) + + assert result, "[true] #{name}" + end + end + + def test_turf_boolean_point_on_line_false_fixtures + # False Fixtures + Dir.glob(File.join(__dir__, "boolean_point_on_line", "false", "**", "*.geojson")).each do |filepath| + name = File.basename(filepath, ".geojson") + geojson = JSON.parse(File.read(filepath), symbolize_names: true) + options = geojson[:properties] + feature1 = geojson[:features][0] + feature2 = geojson[:features][1] + result = Turf.boolean_point_on_line(feature1, feature2, options) + + refute result, "[false] #{name}" + end + end + + def test_turf_boolean_point_on_line_issue_2750 + # Issue 2750 Tests + point1 = Turf.point([2, 13]) + zero_length_line = Turf.line_string([[1, 1], [1, 1]]) + refute Turf.boolean_point_on_line(point1, zero_length_line), + "#2750 different longitude point not on zero length line" + + point2 = Turf.point([1, 13]) + refute Turf.boolean_point_on_line(point2, zero_length_line), + "#2750 same longitude point not on zero length line" + end +end