diff --git a/ext/GeometryOpsProjExt/GeometryOpsProjExt.jl b/ext/GeometryOpsProjExt/GeometryOpsProjExt.jl index c0747db70..2c368f7ae 100644 --- a/ext/GeometryOpsProjExt/GeometryOpsProjExt.jl +++ b/ext/GeometryOpsProjExt/GeometryOpsProjExt.jl @@ -7,5 +7,6 @@ using GeometryOps, Proj include("reproject.jl") include("segmentize.jl") +include("arclength.jl") end \ No newline at end of file diff --git a/ext/GeometryOpsProjExt/arclength.jl b/ext/GeometryOpsProjExt/arclength.jl new file mode 100644 index 000000000..01d980a61 --- /dev/null +++ b/ext/GeometryOpsProjExt/arclength.jl @@ -0,0 +1,172 @@ +#= + +Geodesic arclength functionality via PROJ. + +```@meta +CollapsedDocStrings = true +``` + +```@docs; canonical=false +GeometryOps.arclength_to_point +GeometryOps.point_at_arclength +``` + +Implementation + +The implementation uses PROJ's geodesic calculations to: +1. Compute the geodesic distance between points accurately on the ellipsoid +2. Find closest points on geodesic segments to target points +3. Interpolate positions along geodesic lines at specified distances + +Key features: +- Uses PROJ's geod_geodesic for accurate ellipsoidal calculations +- Configurable equatorial radius and flattening via Geodesic manifold +- Thread-safe implementation +- Supports both LineString and LinearRing geometries + +The function creates geodesic lines between each pair of points and +calculates distances and interpolated positions along those geodesic paths. +=# + +# This holds the `arclength` geodesic functionality. + +import GeometryOps: _arclength_to_point, _point_at_arclength, _point_distance, _closest_point_on_segment, _interpolate_point +import Proj + +# Geodesic implementations +function GeometryOps._arclength_to_point(method::Geodesic, trait1::Union{GI.LineStringTrait, GI.LinearRingTrait}, linestring, trait2::GI.PointTrait, target_point) + cumulative_distance = 0.0 + closest_distance = Inf + result_distance = 0.0 + + if GI.npoint(linestring) < 2 + return 0.0 + end + + proj_geodesic = Proj.geod_geodesic(method.semimajor_axis, 1/method.inv_flattening) + prev_point = GI.getpoint(linestring, 1) + + for i in 2:GI.npoint(linestring) + curr_point = GI.getpoint(linestring, i) + + # Calculate geodesic distance between consecutive points + segment_length = _point_distance(method, prev_point, curr_point, proj_geodesic) + + # Find closest point on this geodesic segment to target + closest_point_on_segment, t = _closest_point_on_segment(method, prev_point, curr_point, target_point, proj_geodesic) + distance_to_segment = _point_distance(method, target_point, closest_point_on_segment, proj_geodesic) + + # If this is the closest segment so far + if distance_to_segment < closest_distance + closest_distance = distance_to_segment + # Calculate distance to the closest point on this segment + result_distance = cumulative_distance + t * segment_length + end + + cumulative_distance += segment_length + prev_point = curr_point + end + + return result_distance +end + +function GeometryOps._point_at_arclength(method::Geodesic, trait1::Union{GI.LineStringTrait, GI.LinearRingTrait}, linestring, target_distance) + if GI.npoint(linestring) < 2 + return GI.npoint(linestring) > 0 ? GI.getpoint(linestring, 1) : nothing + end + + if target_distance <= 0 + return GI.getpoint(linestring, 1) + end + + proj_geodesic = Proj.geod_geodesic(method.semimajor_axis, 1/method.inv_flattening) + cumulative_distance = 0.0 + prev_point = GI.getpoint(linestring, 1) + + for i in 2:GI.npoint(linestring) + curr_point = GI.getpoint(linestring, i) + segment_length = _point_distance(method, prev_point, curr_point, proj_geodesic) + + if cumulative_distance + segment_length >= target_distance + # Target distance is within this segment + remaining_distance = target_distance - cumulative_distance + t = remaining_distance / segment_length + return _interpolate_point(method, prev_point, curr_point, t, proj_geodesic) + end + + cumulative_distance += segment_length + prev_point = curr_point + end + + # Distance exceeds total length, return last point + return GI.getpoint(linestring, GI.npoint(linestring)) +end + +# Helper functions for geodesic distance calculations +function GeometryOps._point_distance(::Geodesic, p1, p2, proj_geodesic) + x1, y1 = GI.x(p1), GI.y(p1) + x2, y2 = GI.x(p2), GI.y(p2) + geod_line = Proj.geod_inverseline(proj_geodesic, y1, x1, y2, x2) + return geod_line.s13 # Distance in meters +end + +# Find the closest point on a geodesic segment to a target point +function GeometryOps._closest_point_on_segment(::Geodesic, p1, p2, target, proj_geodesic) + x1, y1 = GI.x(p1), GI.y(p1) + x2, y2 = GI.x(p2), GI.y(p2) + tx, ty = GI.x(target), GI.y(target) + + # Create geodesic line from p1 to p2 + geod_line = Proj.geod_inverseline(proj_geodesic, y1, x1, y2, x2) + segment_length = geod_line.s13 + + if segment_length == 0 + # Degenerate segment + return p1, 0.0 + end + + # Sample points along the geodesic and find the closest to target + best_t = 0.0 + min_distance = Inf + + # Sample at regular intervals to find approximate closest point + n_samples = 100 + for i in 0:n_samples + t = i / n_samples + distance_along = t * segment_length + + # Get point at this position along geodesic + lat, lon, _ = Proj.geod_position(geod_line, distance_along) + sample_point = (lon, lat) + + # Calculate distance from sample point to target + dist_to_target = _point_distance(Geodesic(), target, sample_point, proj_geodesic) + + if dist_to_target < min_distance + min_distance = dist_to_target + best_t = t + end + end + + # Get the closest point coordinates + distance_along = best_t * segment_length + lat, lon, _ = Proj.geod_position(geod_line, distance_along) + closest_point = (lon, lat) + + return closest_point, best_t +end + +# Interpolate between two points along a geodesic +function GeometryOps._interpolate_point(::Geodesic, p1, p2, t, proj_geodesic) + x1, y1 = GI.x(p1), GI.y(p1) + x2, y2 = GI.x(p2), GI.y(p2) + + # Create geodesic line from p1 to p2 + geod_line = Proj.geod_inverseline(proj_geodesic, y1, x1, y2, x2) + distance_along = t * geod_line.s13 + + # Get point at this position along geodesic + lat, lon, _ = Proj.geod_position(geod_line, distance_along) + + return (lon, lat) +end \ No newline at end of file diff --git a/src/GeometryOps.jl b/src/GeometryOps.jl index 6ca4923c8..0508102e5 100644 --- a/src/GeometryOps.jl +++ b/src/GeometryOps.jl @@ -45,6 +45,7 @@ using .LoopStateMachine, .SpatialTreeInterface include("methods/angles.jl") +include("methods/arclength.jl") include("methods/area.jl") include("methods/barycentric.jl") include("methods/buffer.jl") diff --git a/src/methods/arclength.jl b/src/methods/arclength.jl new file mode 100644 index 000000000..cd09452e9 --- /dev/null +++ b/src/methods/arclength.jl @@ -0,0 +1,254 @@ +# # Arclength + +export arclength_to_point, point_at_arclength + +#= +## What is arclength functionality? + +Arclength functionality provides two key operations: +1. `arclength_to_point(manifold, linestring, point)` - calculates the cumulative + distance along a linestring from its start to a specified point on the line +2. `point_at_arclength(manifold, linestring, distance)` - finds the point at a + specified distance along the linestring from its start + +These functions are useful for: +- Parameterizing curves by arc length +- Finding positions along routes or paths +- Interpolating along geometric curves +- Measuring progress along linear features + +Both functions support multiple manifolds: +- `Planar()` - uses Euclidean distance calculations +- `Geodesic()` - uses geodesic distance calculations for accurate Earth-surface measurements + +## Examples + +```@example arclength +import GeometryOps as GO, GeoInterface as GI + +# Create a simple linestring +line = GI.LineString([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (2.0, 1.0)]) + +# Find the distance to a point on the line +distance_to_point = GO.arclength_to_point(GO.Planar(), line, (1.0, 0.5)) + +# Find a point at a specific distance along the line +point_at_distance = GO.point_at_arclength(GO.Planar(), line, 1.5) +``` + +For geographic coordinates, use the Geodesic manifold: + +```@example arclength +using Proj # required for Geodesic calculations +geo_line = GI.LineString([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]) +distance_geo = GO.arclength_to_point(GO.Geodesic(), geo_line, (0.5, 0.0)) +point_geo = GO.point_at_arclength(GO.Geodesic(), geo_line, 50000) # 50km +``` + +## Implementation +=# + +""" + arclength_to_point([method = Planar()], linestring, point; threaded = false) + +Calculate the cumulative distance along a linestring from its start to a +specified point. The point should lie on the linestring. + +## Arguments +- `method::Manifold = Planar()`: The manifold to use for distance calculations. + - `Planar()` uses Euclidean distance + - `Geodesic()` uses geodesic distance calculations +- `linestring`: A LineString or LinearRing geometry +- `point`: The target point on the linestring + +Returns the cumulative distance from the start of the linestring to the point. +If the point is not on the linestring, returns the distance to the closest point +on the linestring. + +## Example +```julia +import GeometryOps as GO, GeoInterface as GI + +line = GI.LineString([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]) +distance = GO.arclength_to_point(line, (1.0, 0.5)) +``` +""" +function arclength_to_point(linestring, point; threaded::Union{Bool, BoolsAsTypes} = False()) + return arclength_to_point(Planar(), linestring, point; threaded = booltype(threaded)) +end + +function arclength_to_point(method::Manifold, linestring, point; threaded::Union{Bool, BoolsAsTypes} = False()) + return _arclength_to_point(method, GI.trait(linestring), linestring, GI.trait(point), point) +end + +""" + point_at_arclength([method = Planar()], linestring, distance; threaded = false) + +Find the point at a specified distance along a linestring from its start. + +## Arguments +- `method::Manifold = Planar()`: The manifold to use for distance calculations. + - `Planar()` uses Euclidean distance + - `Geodesic()` uses geodesic distance calculations +- `linestring`: A LineString or LinearRing geometry +- `distance`: The target distance along the linestring + +Returns the point at the specified distance. If the distance exceeds the total +length of the linestring, returns the endpoint. + +## Example +```julia +import GeometryOps as GO, GeoInterface as GI + +line = GI.LineString([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]) +point = GO.point_at_arclength(line, 1.5) +``` +""" +function point_at_arclength(linestring, distance; threaded::Union{Bool, BoolsAsTypes} = False()) + return point_at_arclength(Planar(), linestring, distance; threaded = booltype(threaded)) +end + +function point_at_arclength(method::Manifold, linestring, distance; threaded::Union{Bool, BoolsAsTypes} = False()) + return _point_at_arclength(method, GI.trait(linestring), linestring, distance) +end + +# Implementation for LineString and LinearRing +function _arclength_to_point(method::Union{Planar, Spherical}, trait1::Union{GI.LineStringTrait, GI.LinearRingTrait}, linestring, trait2::GI.PointTrait, target_point) + cumulative_distance = 0.0 + closest_distance = Inf + result_distance = 0.0 + + if GI.npoint(linestring) < 2 + return 0.0 + end + + prev_point = GI.getpoint(linestring, 1) + + for i in 2:GI.npoint(linestring) + curr_point = GI.getpoint(linestring, i) + + # Calculate distance between consecutive points + segment_length = _point_distance(method, prev_point, curr_point) + + # Find closest point on this segment to target + closest_point_on_segment, t = _closest_point_on_segment(method, prev_point, curr_point, target_point) + distance_to_segment = _point_distance(method, target_point, closest_point_on_segment) + + # If this is the closest segment so far + if distance_to_segment < closest_distance + closest_distance = distance_to_segment + # Calculate distance to the closest point on this segment + result_distance = cumulative_distance + t * segment_length + end + + cumulative_distance += segment_length + prev_point = curr_point + end + + return result_distance +end + +# Geodesic implementation is in the Proj extension +function _arclength_to_point(method::Geodesic, trait1::Union{GI.LineStringTrait, GI.LinearRingTrait}, linestring, trait2::GI.PointTrait, target_point) + error("Geodesic arclength calculation requires Proj.jl to be loaded. Please run `using Proj`.") +end + +function _point_at_arclength(method::Union{Planar, Spherical}, trait1::Union{GI.LineStringTrait, GI.LinearRingTrait}, linestring, target_distance) + if GI.npoint(linestring) < 2 + return GI.npoint(linestring) > 0 ? GI.getpoint(linestring, 1) : nothing + end + + if target_distance <= 0 + return GI.getpoint(linestring, 1) + end + + cumulative_distance = 0.0 + prev_point = GI.getpoint(linestring, 1) + + for i in 2:GI.npoint(linestring) + curr_point = GI.getpoint(linestring, i) + segment_length = _point_distance(method, prev_point, curr_point) + + if cumulative_distance + segment_length >= target_distance + # Target distance is within this segment + remaining_distance = target_distance - cumulative_distance + t = remaining_distance / segment_length + return _interpolate_point(method, prev_point, curr_point, t) + end + + cumulative_distance += segment_length + prev_point = curr_point + end + + # Distance exceeds total length, return last point + return GI.getpoint(linestring, GI.npoint(linestring)) +end + +# Geodesic implementation is in the Proj extension +function _point_at_arclength(method::Geodesic, trait1::Union{GI.LineStringTrait, GI.LinearRingTrait}, linestring, target_distance) + error("Geodesic point at arclength calculation requires Proj.jl to be loaded. Please run `using Proj`.") +end + +# Helper functions for distance calculations +function _point_distance(::Planar, p1, p2) + x1, y1 = GI.x(p1), GI.y(p1) + x2, y2 = GI.x(p2), GI.y(p2) + return hypot(x2 - x1, y2 - y1) +end + +# Geodesic version is defined in the Proj extension +function _point_distance(::Geodesic, p1, p2, proj_geodesic) + error("Geodesic distance calculation requires Proj.jl to be loaded. Please run `using Proj`.") +end + +# Find the closest point on a line segment to a target point +function _closest_point_on_segment(::Planar, p1, p2, target) + x1, y1 = GI.x(p1), GI.y(p1) + x2, y2 = GI.x(p2), GI.y(p2) + tx, ty = GI.x(target), GI.y(target) + + # Vector from p1 to p2 + dx = x2 - x1 + dy = y2 - y1 + + # Vector from p1 to target + px = tx - x1 + py = ty - y1 + + # Project target onto line segment + segment_length_sq = dx * dx + dy * dy + + if segment_length_sq == 0 + # Degenerate segment + return p1, 0.0 + end + + t = (px * dx + py * dy) / segment_length_sq + t = clamp(t, 0.0, 1.0) # Clamp to segment bounds + + closest_x = x1 + t * dx + closest_y = y1 + t * dy + + return (closest_x, closest_y), t +end + +# Geodesic version is defined in the Proj extension +function _closest_point_on_segment(::Geodesic, p1, p2, target, proj_geodesic) + error("Geodesic closest point calculation requires Proj.jl to be loaded. Please run `using Proj`.") +end + +# Interpolate between two points +function _interpolate_point(::Planar, p1, p2, t) + x1, y1 = GI.x(p1), GI.y(p1) + x2, y2 = GI.x(p2), GI.y(p2) + + x = x1 + t * (x2 - x1) + y = y1 + t * (y2 - y1) + + return (x, y) +end + +# Geodesic version is defined in the Proj extension +function _interpolate_point(::Geodesic, p1, p2, t, proj_geodesic) + error("Geodesic interpolation requires Proj.jl to be loaded. Please run `using Proj`.") +end \ No newline at end of file diff --git a/test/methods/arclength.jl b/test/methods/arclength.jl new file mode 100644 index 000000000..6d4537830 --- /dev/null +++ b/test/methods/arclength.jl @@ -0,0 +1,115 @@ +using Test +import GeometryOps as GO +import GeoInterface as GI + +@testset "Arclength Functionality" begin + + # Test simple horizontal line + @testset "Simple horizontal line" begin + line = GI.LineString([(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)]) + + # Test arclength_to_point + @test GO.arclength_to_point(line, (0.0, 0.0)) ≈ 0.0 + @test GO.arclength_to_point(line, (1.0, 0.0)) ≈ 1.0 + @test GO.arclength_to_point(line, (2.0, 0.0)) ≈ 2.0 + @test GO.arclength_to_point(line, (0.5, 0.0)) ≈ 0.5 + @test GO.arclength_to_point(line, (1.5, 0.0)) ≈ 1.5 + + # Test point_at_arclength + point = GO.point_at_arclength(line, 0.0) + @test point[1] ≈ 0.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(line, 1.0) + @test point[1] ≈ 1.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(line, 2.0) + @test point[1] ≈ 2.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(line, 0.5) + @test point[1] ≈ 0.5 && point[2] ≈ 0.0 + point = GO.point_at_arclength(line, 1.5) + @test point[1] ≈ 1.5 && point[2] ≈ 0.0 + end + + # Test L-shaped line + @testset "L-shaped line" begin + line = GI.LineString([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]) + + # Test arclength_to_point + @test GO.arclength_to_point(line, (0.0, 0.0)) ≈ 0.0 + @test GO.arclength_to_point(line, (1.0, 0.0)) ≈ 1.0 + @test GO.arclength_to_point(line, (1.0, 1.0)) ≈ 2.0 + @test GO.arclength_to_point(line, (0.5, 0.0)) ≈ 0.5 + @test GO.arclength_to_point(line, (1.0, 0.5)) ≈ 1.5 + + # Test point_at_arclength + point = GO.point_at_arclength(line, 0.0) + @test point[1] ≈ 0.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(line, 1.0) + @test point[1] ≈ 1.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(line, 2.0) + @test point[1] ≈ 1.0 && point[2] ≈ 1.0 + point = GO.point_at_arclength(line, 0.5) + @test point[1] ≈ 0.5 && point[2] ≈ 0.0 + point = GO.point_at_arclength(line, 1.5) + @test point[1] ≈ 1.0 && point[2] ≈ 0.5 + end + + # Test with explicit Planar manifold + @testset "Explicit Planar manifold" begin + line = GI.LineString([(0.0, 0.0), (3.0, 4.0)]) # 3-4-5 triangle + + @test GO.arclength_to_point(GO.Planar(), line, (0.0, 0.0)) ≈ 0.0 + @test GO.arclength_to_point(GO.Planar(), line, (3.0, 4.0)) ≈ 5.0 + @test GO.arclength_to_point(GO.Planar(), line, (1.5, 2.0)) ≈ 2.5 + + point = GO.point_at_arclength(GO.Planar(), line, 0.0) + @test point[1] ≈ 0.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(GO.Planar(), line, 5.0) + @test point[1] ≈ 3.0 && point[2] ≈ 4.0 + point = GO.point_at_arclength(GO.Planar(), line, 2.5) + @test point[1] ≈ 1.5 && point[2] ≈ 2.0 + end + + # Test edge cases + @testset "Edge cases" begin + # Two point line (minimum valid linestring) + two_point_line = GI.LineString([(0.0, 0.0), (1.0, 0.0)]) + @test GO.arclength_to_point(two_point_line, (0.0, 0.0)) ≈ 0.0 + @test GO.arclength_to_point(two_point_line, (1.0, 0.0)) ≈ 1.0 + + # Distance beyond line length + line = GI.LineString([(0.0, 0.0), (1.0, 0.0)]) + point = GO.point_at_arclength(line, 10.0) + @test point[1] ≈ 1.0 && point[2] ≈ 0.0 + + # Negative distance + point = GO.point_at_arclength(line, -1.0) + @test point[1] ≈ 0.0 && point[2] ≈ 0.0 + end + + # Test with LinearRing + @testset "LinearRing" begin + ring = GI.LinearRing([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]) + + # Test some points on the ring + @test GO.arclength_to_point(ring, (0.0, 0.0)) ≈ 0.0 + @test GO.arclength_to_point(ring, (1.0, 0.0)) ≈ 1.0 + @test GO.arclength_to_point(ring, (1.0, 1.0)) ≈ 2.0 + @test GO.arclength_to_point(ring, (0.0, 1.0)) ≈ 3.0 + + # Test point_at_arclength + point = GO.point_at_arclength(ring, 0.0) + @test point[1] ≈ 0.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(ring, 1.0) + @test point[1] ≈ 1.0 && point[2] ≈ 0.0 + point = GO.point_at_arclength(ring, 2.5) + @test point[1] ≈ 0.5 && point[2] ≈ 1.0 + end + + # Test error handling for Geodesic without Proj + @testset "Geodesic error handling" begin + line = GI.LineString([(0.0, 0.0), (1.0, 0.0)]) + + @test_throws ErrorException GO.arclength_to_point(GO.Geodesic(), line, (0.5, 0.0)) + @test_throws ErrorException GO.point_at_arclength(GO.Geodesic(), line, 0.5) + end + +end \ No newline at end of file diff --git a/test/methods/arclength_geodesic.jl b/test/methods/arclength_geodesic.jl new file mode 100644 index 000000000..e8fd6df5b --- /dev/null +++ b/test/methods/arclength_geodesic.jl @@ -0,0 +1,56 @@ +using Test +using Proj +import GeometryOps as GO +import GeoInterface as GI + +@testset "Arclength Functionality with Geodesic" begin + + # Test with Geodesic manifold (requires Proj) + @testset "Geodesic manifold" begin + # Use geographic coordinates (lat/lon) + line = GI.LineString([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]) + + # These should now work without errors + @test_nowarn GO.arclength_to_point(GO.Geodesic(), line, (0.5, 0.0)) + @test_nowarn GO.point_at_arclength(GO.Geodesic(), line, 50000.0) # 50km + + # Test specific values - geodesic distances will be different from planar + distance_geo = GO.arclength_to_point(GO.Geodesic(), line, (1.0, 0.0)) + @test distance_geo > 0 + @test distance_geo != 1.0 # Should be different from planar distance + + point_geo = GO.point_at_arclength(GO.Geodesic(), line, distance_geo) + @test abs(point_geo[1] - 1.0) < 0.01 # Should be close to (1.0, 0.0) + @test abs(point_geo[2] - 0.0) < 0.01 + end + + # Test comparison between Planar and Geodesic for small distances + @testset "Planar vs Geodesic comparison" begin + # Small line segment where differences should be minimal + line = GI.LineString([(0.0, 0.0), (0.01, 0.0)]) # About 1.1km at equator + + distance_planar = GO.arclength_to_point(GO.Planar(), line, (0.005, 0.0)) + distance_geodesic = GO.arclength_to_point(GO.Geodesic(), line, (0.005, 0.0)) + + # For small distances, they should be fairly similar but not identical + @test distance_planar ≈ 0.005 + @test distance_geodesic > 0 + @test abs(distance_planar - distance_geodesic) < distance_planar # Geodesic should be somewhat different + end + + # Test LinearRing with Geodesic + @testset "Geodesic LinearRing" begin + ring = GI.LinearRing([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]) + + @test_nowarn GO.arclength_to_point(GO.Geodesic(), ring, (0.5, 0.0)) + @test_nowarn GO.point_at_arclength(GO.Geodesic(), ring, 100000.0) # 100km + + distance = GO.arclength_to_point(GO.Geodesic(), ring, (1.0, 0.0)) + @test distance > 0 + + point = GO.point_at_arclength(GO.Geodesic(), ring, distance) + @test abs(point[1] - 1.0) < 0.01 + @test abs(point[2] - 0.0) < 0.01 + end + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 613cdd992..4aa77d12f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,6 +20,7 @@ end # Methods @safetestset "Angles" begin include("methods/angles.jl") end +@safetestset "Arclength" begin include("methods/arclength.jl") end @safetestset "Area" begin include("methods/area.jl") end @safetestset "Barycentric coordinate operations" begin include("methods/barycentric.jl") end @safetestset "Orientation" begin include("methods/orientation.jl") end